From 4d05f09613fbbc7bac597eb89857985c24405129 Mon Sep 17 00:00:00 2001 From: Vikram Rajkumar Date: Wed, 29 Jul 2015 13:53:21 -0400 Subject: [PATCH 1/5] Update submodules --- docs | 2 +- libraries/fc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs b/docs index 97435c1a..34eb3e3d 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 97435c1a622e41e0a5fc1be72aaadea62e1b7adb +Subproject commit 34eb3e3db200f5a193a61312679f08fa01b07afb diff --git a/libraries/fc b/libraries/fc index d11b48a0..088dadd1 160000 --- a/libraries/fc +++ b/libraries/fc @@ -1 +1 @@ -Subproject commit d11b48a015e0c339cc20f739ef1f8a5b3c57b913 +Subproject commit 088dadd12481e75829b83c098f3bc36b7e296ca0 From 03e16afdb2bd84fb061d7ce169f89218996f68eb Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Wed, 29 Jul 2015 15:50:12 -0400 Subject: [PATCH 2/5] [GUI] Progress circa wallet and keys - Fixes to wallet - Open or create wallet on startup - Add support for importing keys - Show user what level of ownership they have over accounts --- programs/light_client/Account.cpp | 76 +++++-- programs/light_client/Account.hpp | 15 +- programs/light_client/GrapheneApplication.cpp | 9 +- programs/light_client/GrapheneApplication.hpp | 2 + programs/light_client/Wallet.cpp | 212 +++++++++--------- programs/light_client/Wallet.hpp | 77 ++++--- programs/light_client/main.cpp | 2 + programs/light_client/qml/AccountPicker.qml | 9 +- programs/light_client/qml/Identicon.qml | 27 +++ programs/light_client/qml/TooltipArea.qml | 23 ++ .../qml/TransactionConfirmationForm.qml | 2 - programs/light_client/qml/TransferForm.qml | 2 +- programs/light_client/qml/main.qml | 40 +++- programs/light_client/qml/qml.qrc | 1 + 14 files changed, 317 insertions(+), 180 deletions(-) create mode 100644 programs/light_client/qml/TooltipArea.qml diff --git a/programs/light_client/Account.cpp b/programs/light_client/Account.cpp index 95d75e9e..c4677235 100644 --- a/programs/light_client/Account.cpp +++ b/programs/light_client/Account.cpp @@ -4,6 +4,19 @@ #include +void Account::setAccountObject(const graphene::chain::account_object& obj) +{ + auto old_name = m_account.name; + m_account = obj; + if (old_name != m_account.name) + Q_EMIT nameChanged(); + if (!m_loaded) { + m_loaded = true; + Q_EMIT loaded(); + qDebug() << name() << "loaded."; + } +} + QQmlListProperty Account::balances() { auto count = [](QQmlListProperty* list) { @@ -16,41 +29,60 @@ QQmlListProperty Account::balances() return QQmlListProperty(this, this, count, at); } -double Account::getActiveControl( Wallet* w )const +double Account::getActiveControl(Wallet* w, int depth)const { - if( m_account.active.num_auths() == 0 ) return 0; - if( m_account.active.weight_threshold == 0 ) return 0; + if (depth >= GRAPHENE_MAX_SIG_CHECK_DEPTH) return 0; + if (m_account.active.num_auths() == 0) return 0; + if (m_account.active.weight_threshold == 0) return 0; uint64_t weight = 0; - for( auto& key : m_account.active.key_auths ) + for (auto& key : m_account.active.key_auths) { - if( w->hasPrivateKey( toQString(key.first) ) ) weight += key.second; - } - for( auto& acnt : m_account.active.account_auths ) - { - // TODO: lookup Account, check to see if we have full control of it, and - // add its weight if we do. Be sure to limit recursion depth + if (w->hasPrivateKey(toQString(key.first))) weight += key.second; } - return double(weight) / double( m_account.active.weight_threshold ); + ChainDataModel* model = qobject_cast(parent()); + for (auto& acnt : m_account.active.account_auths) + { + Account* account = model->getAccount(acnt.first.instance.value); + if (!account->m_loaded) { + QEventLoop el; + connect(account, &Account::loaded, &el, &QEventLoop::quit); + QTimer::singleShot(1000, &el, SLOT(quit())); + el.exec(); + if (!account->m_loaded) + // We don't have this account loaded yet... Oh well, move along + continue; + } + if (account->getActiveControl(w, depth + 1) >= 1.0) + weight += acnt.second; + } + + return double(weight) / double(m_account.active.weight_threshold); } -double Account::getOwnerControl( Wallet* w )const +double Account::getOwnerControl(Wallet* w)const { - if( m_account.owner.num_auths() == 0 ) return 0; - if( m_account.owner.weight_threshold == 0 ) return 0; + if (m_account.owner.num_auths() == 0) return 0; + if (m_account.owner.weight_threshold == 0) return 0; uint64_t weight = 0; - for( auto& key : m_account.owner.key_auths ) + for (auto& key : m_account.owner.key_auths) { - if( w->hasPrivateKey( toQString(key.first) ) ) weight += key.second; - } - for( auto& acnt : m_account.owner.account_auths ) - { - // TODO: lookup Account, check to see if we have full *ACTIVE* control of it, and - // add its weight if we do. Be sure to limit recursion depth + if (w->hasPrivateKey(toQString(key.first))) weight += key.second; } - return double(weight) / double( m_account.owner.weight_threshold ); + ChainDataModel* model = qobject_cast(parent()); + for (auto& acnt : m_account.owner.account_auths) + { + Account* account = model->getAccount(acnt.first.instance.value); + if (!account->m_loaded) + // We don't have this account loaded yet... Oh well, move along + continue; + if (account->getActiveControl(w) >= 1.0) + weight += acnt.second; + } + + return double(weight) / double(m_account.owner.weight_threshold); } void Account::update(const graphene::chain::account_balance_object& balance) diff --git a/programs/light_client/Account.hpp b/programs/light_client/Account.hpp index 4840b3bc..6facefa2 100644 --- a/programs/light_client/Account.hpp +++ b/programs/light_client/Account.hpp @@ -21,9 +21,11 @@ class Account : public GrapheneObject { Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QQmlListProperty balances READ balances NOTIFY balancesChanged) + Q_PROPERTY(bool isLoaded MEMBER m_loaded NOTIFY loaded) account_object m_account; QList m_balances; + bool m_loaded = false; public: Account(ObjectId id = -1, QString name = QString(), QObject* parent = nullptr) @@ -31,13 +33,7 @@ public: { m_account.name = name.toStdString(); } - void setAccountObject(const account_object& obj) - { - auto old_name = m_account.name; - m_account = obj; - if (old_name != m_account.name) - Q_EMIT nameChanged(); - } + void setAccountObject(const account_object& obj); QString name()const { return QString::fromStdString(m_account.name); } QQmlListProperty balances(); @@ -58,10 +54,11 @@ public: * * @return the percent of direct control the wallet has over the account. */ - Q_INVOKABLE double getOwnerControl( Wallet* w )const; - Q_INVOKABLE double getActiveControl( Wallet* w )const; + Q_INVOKABLE double getOwnerControl(Wallet* w)const; + Q_INVOKABLE double getActiveControl(Wallet* w , int depth = 0)const; Q_SIGNALS: void nameChanged(); void balancesChanged(); + void loaded(); }; diff --git a/programs/light_client/GrapheneApplication.cpp b/programs/light_client/GrapheneApplication.cpp index d5cf050b..b0ca88b2 100644 --- a/programs/light_client/GrapheneApplication.cpp +++ b/programs/light_client/GrapheneApplication.cpp @@ -8,6 +8,8 @@ #include +#include + using graphene::app::login_api; using graphene::app::database_api; @@ -19,7 +21,7 @@ GrapheneApplication::GrapheneApplication(QObject* parent) m_model = new ChainDataModel(m_thread, this); m_operationBuilder = new OperationBuilder(*m_model, this); - m_wallet = new Wallet( this ); + m_wallet = new Wallet(this); connect(m_model, &ChainDataModel::queueExecute, this, &GrapheneApplication::execute); @@ -75,6 +77,11 @@ void GrapheneApplication::start(QString apiurl, QString user, QString pass) } } +QString GrapheneApplication::defaultDataPath() +{ + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); +} + Transaction* GrapheneApplication::createTransaction() const { return new Transaction; diff --git a/programs/light_client/GrapheneApplication.hpp b/programs/light_client/GrapheneApplication.hpp index 552179b8..4d8489a4 100644 --- a/programs/light_client/GrapheneApplication.hpp +++ b/programs/light_client/GrapheneApplication.hpp @@ -64,6 +64,8 @@ public: return m_isConnected; } + Q_INVOKABLE static QString defaultDataPath(); + /// Convenience method to get a Transaction in QML. Caller takes ownership of the new Transaction. Q_INVOKABLE Transaction* createTransaction() const; diff --git a/programs/light_client/Wallet.cpp b/programs/light_client/Wallet.cpp index dd62ca8d..4d218ab9 100644 --- a/programs/light_client/Wallet.cpp +++ b/programs/light_client/Wallet.cpp @@ -20,22 +20,22 @@ bool Wallet::open(QString file_path) fc::path p(file_path.toStdString()); if( !fc::exists(p) ) { - ilog( "Unable to open wallet file '${f}', it does not exist", ("f",p) ); + ilog("Unable to open wallet file '${f}'; it does not exist", ("f",p)); return false; } - _data = fc::json::from_file( p ).as(); + _data = fc::json::from_file(p).as(); for( const auto& key : _data.encrypted_private_keys ) { if( key.second.label != string() ) _label_to_key[QString::fromStdString(key.second.label)] = toQString(key.first); if( key.second.encrypted_private_key.size() ) - _available_private_keys.insert( key.first ); + _available_private_keys.insert(key.first); } _wallet_file_path = p; - Q_EMIT isOpenChanged( true ); + Q_EMIT isOpenChanged(true); return true; } @@ -49,7 +49,7 @@ bool Wallet::close() if( !isOpen() ) return false; save(); _wallet_file_path = fc::path(); - Q_EMIT isOpenChanged( false ); + Q_EMIT isOpenChanged(false); return false; } @@ -57,28 +57,28 @@ bool Wallet::save() { if( !isOpen() ) return false; /// TODO: backup existing wallet file first. - fc::json::save_to_file( _data, _wallet_file_path ); + fc::json::save_to_file(_data, _wallet_file_path); return false; } -bool Wallet::saveAs( QString file_path ) +bool Wallet::saveAs(QString file_path) { if( !isOpen() ) return false; fc::path p(file_path.toStdString()); if( fc::exists(p) ) return false; - fc::json::save_to_file( _data, _wallet_file_path ); + fc::json::save_to_file(_data, _wallet_file_path); return true; } -bool Wallet::create( QString file_path, QString password, QString brain_key ) +bool Wallet::create(QString file_path, QString password, QString brain_key) { if( isOpen() ) return false; if( password == QString() ) return false; - fc::path p( file_path.toStdString() ); - if( fc::exists( p ) ) + fc::path p(file_path.toStdString()); + if( fc::exists(p) ) { - ilog( "Unable to create wallet file '${f}' because a file with that name already exists.", ("f",p) ); + ilog("Unable to create wallet file '${f}' because a file with that name already exists.", ("f",p)); return false; } @@ -90,28 +90,30 @@ bool Wallet::create( QString file_path, QString password, QString brain_key ) auto brainkey = brain_key.toStdString(); auto pw_str = password.toStdString(); - auto password_hash = fc::sha512::hash( pw_str.c_str(), pw_str.size() ); - _decrypted_master_key = fc::sha512::hash( fc::ecc::private_key::generate().get_secret() ); - _data.master_key_digest = fc::sha512::hash( _decrypted_master_key ); - _data.encrypted_master_key = fc::aes_encrypt( password_hash, fc::raw::pack(_decrypted_master_key) ); + auto password_hash = fc::sha512::hash(pw_str.c_str(), pw_str.size()); + _decrypted_master_key = fc::sha512::hash(fc::ecc::private_key::generate().get_secret()); + _data.master_key_digest = fc::sha512::hash(_decrypted_master_key); + _data.encrypted_master_key = fc::aes_encrypt(password_hash, fc::raw::pack(_decrypted_master_key)); - _data.brain_key_digest = fc::sha512::hash( brainkey.c_str(), brainkey.size() ); - _data.encrypted_brain_key = fc::aes_encrypt( _decrypted_master_key, fc::raw::pack( brainkey ) ); + _data.brain_key_digest = fc::sha512::hash(brainkey.c_str(), brainkey.size()); + _data.encrypted_brain_key = fc::aes_encrypt(_decrypted_master_key, fc::raw::pack(brainkey)); - fc::json::save_to_file( _data, p ); + QFileInfo(file_path).absoluteDir().mkpath("."); + fc::json::save_to_file(_data, p); _wallet_file_path = p; + ilog("Created wallet file '${f}'", ("f", p)); - return false; + return true; } -bool Wallet::loadBrainKey( QString brain_key ) +bool Wallet::loadBrainKey(QString brain_key) { if( !isOpen() ) return false; if( brain_key == QString() ) return false; auto brainkey = brain_key.toStdString(); - if( _data.brain_key_digest != fc::sha512::hash( brainkey.c_str(), brainkey.size() ) ) + if( _data.brain_key_digest != fc::sha512::hash(brainkey.c_str(), brainkey.size()) ) return false; _brain_key = brain_key; @@ -140,29 +142,34 @@ QString Wallet::getBrainKey() if( _brain_key != QString() ) return _brain_key; - auto dec_brain_key = fc::aes_decrypt( _decrypted_master_key, _data.encrypted_brain_key ); + auto dec_brain_key = fc::aes_decrypt(_decrypted_master_key, _data.encrypted_brain_key); auto dec_brain_key_str = fc::raw::unpack(dec_brain_key); - _brain_key.fromStdString( dec_brain_key_str ); + _brain_key.fromStdString(dec_brain_key_str); return _brain_key; } bool Wallet::isLocked()const { if( !isOpen() ) return true; - return false; + return _decrypted_master_key == fc::sha512(); } -bool Wallet::unlock( QString password ) +bool Wallet::unlock(QString password) { - if( !isLocked() ) return true; - auto pw_str = password.toStdString(); - auto password_hash = fc::sha512::hash( pw_str.c_str(), pw_str.size() ); - auto plain_txt = fc::aes_decrypt( password_hash, _data.encrypted_master_key ); - _decrypted_master_key = fc::raw::unpack(plain_txt); - if( _data.master_key_digest != fc::sha512::hash(_decrypted_master_key) ) - _decrypted_master_key = fc::sha512(); + try { + if( !isLocked() ) return true; + auto pw_str = password.toStdString(); + auto password_hash = fc::sha512::hash(pw_str.c_str(), pw_str.size()); + auto plain_txt = fc::aes_decrypt(password_hash, _data.encrypted_master_key); + _decrypted_master_key = fc::raw::unpack(plain_txt); + if( _data.master_key_digest != fc::sha512::hash(_decrypted_master_key) ) + _decrypted_master_key = fc::sha512(); - Q_EMIT isLockedChanged( isLocked() ); - return !isLocked(); + Q_EMIT isLockedChanged(isLocked()); + return !isLocked(); + } catch (const fc::exception& e) { + elog(e.to_detail_string()); + return false; + } } bool Wallet::lock() @@ -171,25 +178,25 @@ bool Wallet::lock() _brain_key = QString(); _decrypted_master_key = fc::sha512(); - Q_EMIT isLockedChanged( isLocked() ); + Q_EMIT isLockedChanged(isLocked()); return true; } -bool Wallet::changePassword( QString new_password ) +bool Wallet::changePassword(QString new_password) { if( !isOpen() ) return false; if( isLocked() ) return false; auto pw_str = new_password.toStdString(); - auto password_hash = fc::sha512::hash( pw_str.c_str(), pw_str.size() ); - _data.encrypted_master_key = fc::aes_encrypt( password_hash, fc::raw::pack(_decrypted_master_key) ); + auto password_hash = fc::sha512::hash(pw_str.c_str(), pw_str.size()); + _data.encrypted_master_key = fc::aes_encrypt(password_hash, fc::raw::pack(_decrypted_master_key)); save(); return true; } -bool Wallet::hasPrivateKey( QString pubkey, bool include_with_brain_key ) +bool Wallet::hasPrivateKey(QString pubkey, bool include_with_brain_key) { - auto pub = fc::variant( pubkey.toStdString() ).as(); + auto pub = fc::variant(pubkey.toStdString()).as(); auto itr = _data.encrypted_private_keys.find(pub); if( itr == _data.encrypted_private_keys.end() ) return false; @@ -199,61 +206,61 @@ bool Wallet::hasPrivateKey( QString pubkey, bool include_with_brain_key ) { if( !itr->second.owner ) return true; - return hasPrivateKey( toQString( *itr->second.owner ), include_with_brain_key ); + return hasPrivateKey(toQString(*itr->second.owner), include_with_brain_key); } return false; } -QString Wallet::getPrivateKey( QString pubkey ) +QString Wallet::getPrivateKey(QString pubkey) { if( !isOpen() ) return QString(); if( isLocked() ) return QString(); - auto pub = fc::variant( pubkey.toStdString() ).as(); - auto itr = _data.encrypted_private_keys.find( pub ); + auto pub = fc::variant(pubkey.toStdString()).as(); + auto itr = _data.encrypted_private_keys.find(pub); if( itr == _data.encrypted_private_keys.end() ) return QString(); if( itr->second.encrypted_private_key.size() == 0 ) return QString(); - auto plain = fc::aes_decrypt( _decrypted_master_key, itr->second.encrypted_private_key ); - return QString::fromStdString( fc::raw::unpack(plain) ); + auto plain = fc::aes_decrypt(_decrypted_master_key, itr->second.encrypted_private_key); + return QString::fromStdString(fc::raw::unpack(plain)); } -QString Wallet::getPublicKey( QString wif_private_key )const +QString Wallet::getPublicKey(QString wif_private_key)const { - auto priv = graphene::utilities::wif_to_key( wif_private_key.toStdString() ); + auto priv = graphene::utilities::wif_to_key(wif_private_key.toStdString()); if( !priv ) return QString(); auto pub = public_key_type(priv->get_public_key()); - return QString::fromStdString( fc::variant( pub ).as_string() ); + return QString::fromStdString(fc::variant(pub).as_string()); } -QString Wallet::getActivePrivateKey( QString owner_pub_key, uint32_t seq ) +QString Wallet::getActivePrivateKey(QString owner_pub_key, uint32_t seq) { if( !isOpen() ) return QString(); if( isLocked() ) return QString(); - auto owner_wif_private_key = getPrivateKey( owner_pub_key ); + auto owner_wif_private_key = getPrivateKey(owner_pub_key); if( owner_wif_private_key == QString() ) return QString(); auto seed = (owner_wif_private_key + " " + QString::number(seq)).toStdString(); - auto secret = fc::sha256::hash( fc::sha512::hash( seed.c_str(), seed.size() ) ); + auto secret = fc::sha256::hash(fc::sha512::hash(seed.c_str(), seed.size())); - auto wif = graphene::utilities::key_to_wif( secret ); - auto priv_key = graphene::utilities::wif_to_key( wif ); + auto wif = graphene::utilities::key_to_wif(secret); + auto priv_key = graphene::utilities::wif_to_key(wif); if( !priv_key ) return QString(); public_key_type active_pub_key(priv_key->get_public_key()); - _data.encrypted_private_keys[active_pub_key].encrypted_private_key = fc::aes_encrypt( _decrypted_master_key, fc::raw::pack( wif ) ); - _data.encrypted_private_keys[active_pub_key].owner = fc::variant( owner_pub_key.toStdString() ).as(); + _data.encrypted_private_keys[active_pub_key].encrypted_private_key = fc::aes_encrypt(_decrypted_master_key, fc::raw::pack(wif)); + _data.encrypted_private_keys[active_pub_key].owner = fc::variant(owner_pub_key.toStdString()).as(); _data.encrypted_private_keys[active_pub_key].brain_sequence = seq; _available_private_keys.insert( active_pub_key ); return QString::fromStdString(wif); } -QString Wallet::getOwnerPrivateKey( uint32_t seq ) +QString Wallet::getOwnerPrivateKey(uint32_t seq) { if( !isOpen() ) return QString(); if( isLocked() ) return QString(); @@ -261,39 +268,39 @@ QString Wallet::getOwnerPrivateKey( uint32_t seq ) auto seed = (getBrainKey() + " " + QString::number(seq)).toStdString(); - auto secret = fc::sha256::hash( fc::sha512::hash( seed.c_str(), seed.size() ) ); + auto secret = fc::sha256::hash(fc::sha512::hash(seed.c_str(), seed.size())); - auto wif = graphene::utilities::key_to_wif( secret ); - auto priv_key = graphene::utilities::wif_to_key( wif ); + auto wif = graphene::utilities::key_to_wif(secret); + auto priv_key = graphene::utilities::wif_to_key(wif); if( !priv_key ) return QString(); public_key_type owner_pub_key(priv_key->get_public_key()); - _data.encrypted_private_keys[owner_pub_key].encrypted_private_key = fc::aes_encrypt( _decrypted_master_key, fc::raw::pack( wif ) ); + _data.encrypted_private_keys[owner_pub_key].encrypted_private_key = fc::aes_encrypt(_decrypted_master_key, fc::raw::pack(wif)); _data.encrypted_private_keys[owner_pub_key].brain_sequence = seq; - _available_private_keys.insert( owner_pub_key ); + _available_private_keys.insert(owner_pub_key); - return QString::fromStdString( wif ); + return QString::fromStdString(wif); } -QString Wallet::getActivePublicKey( QString active_pub, uint32_t seq ) +QString Wallet::getActivePublicKey(QString active_pub, uint32_t seq) { - return getPublicKey( getActivePrivateKey( active_pub, seq ) ); + return getPublicKey(getActivePrivateKey(active_pub, seq)); } -QString Wallet::getOwnerPublicKey( uint32_t seq ) +QString Wallet::getOwnerPublicKey(uint32_t seq) { - return getPublicKey( getOwnerPrivateKey( seq ) ); + return getPublicKey(getOwnerPrivateKey(seq)); } -QString Wallet::getKeyLabel( QString pubkey ) +QString Wallet::getKeyLabel(QString pubkey) { if( !isOpen() ) return QString(); public_key_type key = fc::variant( pubkey.toStdString() ).as(); - auto itr = _data.encrypted_private_keys.find( key ); + auto itr = _data.encrypted_private_keys.find(key); if( itr == _data.encrypted_private_keys.end() ) return QString(); - return QString::fromStdString( itr->second.label ); + return QString::fromStdString(itr->second.label); } /** * The same label may not be assigned to more than one key, this method will @@ -301,86 +308,87 @@ QString Wallet::getKeyLabel( QString pubkey ) * * @return true if the label was set */ -bool Wallet::setKeyLabel( QString pubkey, QString label ) +bool Wallet::setKeyLabel(QString pubkey, QString label) { if( label == QString() ) // clear the label { - auto pub = fc::variant( pubkey.toStdString() ).as(); + auto pub = fc::variant(pubkey.toStdString()).as(); auto old_label = _data.encrypted_private_keys[pub].label; _data.encrypted_private_keys[pub].label = string(); if( old_label.size() ) - _label_to_key.erase( QString::fromStdString( old_label ) ); + _label_to_key.erase(QString::fromStdString(old_label)); return true; } - auto itr = _label_to_key.find( label ); + auto itr = _label_to_key.find(label); if( itr != _label_to_key.end() ) return false; _label_to_key[label] = pubkey; - auto pub = fc::variant( pubkey.toStdString() ).as(); + auto pub = fc::variant(pubkey.toStdString()).as(); _data.encrypted_private_keys[pub].label = label.toStdString(); return true; } -QList > Wallet::getAllPublicKeys( bool only_if_private )const +QList> Wallet::getAllPublicKeys(bool only_if_private)const { - QList< QPair > result; + QList< QPair> result; if( !isOpen() ) return result; for( const auto& item : _data.encrypted_private_keys ) { if( only_if_private && !item.second.encrypted_private_key.size() ) continue; - result.push_back( qMakePair( toQString( item.first ), QString::fromStdString( item.second.label ) ) ); + result.push_back(qMakePair(toQString(item.first), QString::fromStdString(item.second.label))); } return result; } -QString Wallet::getPublicKey( QString label ) +QString Wallet::getPublicKey(QString label) { - if( !isOpen() ) return QString(); + if( !isOpen() ) return QString::null; - auto itr = _label_to_key.find( label ); + auto itr = _label_to_key.find(label); if( itr != _label_to_key.end() ) - return QString(); + return QString::null; return itr->second; } /** imports a public key and assigns it a label */ -bool Wallet::importPublicKey( QString pubkey, QString label ) +bool Wallet::importPublicKey(QString pubkey, QString label) { - return setKeyLabel( pubkey, label ); + return setKeyLabel(pubkey, label); } /** * @param wifkey a private key in (WIF) Wallet Import Format * @pre !isLocked() **/ -bool Wallet::importPrivateKey( QString wifkey, QString label ) +bool Wallet::importPrivateKey(QString wifkey, QString label) { if( !isOpen() ) return false; if( isLocked() ) return false; - auto pub = getPublicKey( wifkey ); - if( pub == QString() ) return false; + auto priv = graphene::utilities::wif_to_key(wifkey.toStdString()); + if (!priv) return false; - importPublicKey( pub, label ); + auto p = priv->get_public_key(); + auto pub = toQString(p); + importPublicKey(pub, label); - auto p = fc::variant( pub.toStdString() ).as(); - _data.encrypted_private_keys[p].encrypted_private_key = fc::aes_encrypt( _decrypted_master_key, fc::raw::pack( wifkey.toStdString() ) ); + _data.encrypted_private_keys[p].encrypted_private_key = fc::aes_encrypt(_decrypted_master_key, fc::raw::pack(wifkey.toStdString())); _available_private_keys.insert(p); return true; } /** removes the key, its lablel and its private key */ -bool Wallet::removePublicKey( QString pubkey ) +bool Wallet::removePublicKey(QString pubkey) { if( !isOpen() ) return false; auto pub = fc::variant( pubkey.toStdString() ).as(); @@ -389,7 +397,7 @@ bool Wallet::removePublicKey( QString pubkey ) auto itr = _data.encrypted_private_keys.find(pub); if( itr != _data.encrypted_private_keys.end() ) { - _label_to_key.erase( QString::fromStdString(itr->second.label) ); + _label_to_key.erase(QString::fromStdString(itr->second.label)); _data.encrypted_private_keys.erase(itr); return true; } @@ -400,12 +408,12 @@ bool Wallet::removePublicKey( QString pubkey ) * * @pre isOpen() && !isLocked() **/ -bool Wallet::removePrivateKey( QString pubkey ) +bool Wallet::removePrivateKey(QString pubkey) { if( !isOpen() ) return false; if( isLocked() ) return false; - auto pub = fc::variant( pubkey.toStdString() ).as(); + auto pub = fc::variant(pubkey.toStdString()).as(); _data.encrypted_private_keys[pub].encrypted_private_key.resize(0); _available_private_keys.erase(pub); @@ -415,8 +423,7 @@ bool Wallet::removePrivateKey( QString pubkey ) /** * @pre !isLocked() */ -vector Wallet::signDigest( const digest_type& d, - const set& keys )const +vector Wallet::signDigest(const digest_type& d, const set& keys)const { vector result; if( !isOpen() ) return result; @@ -424,21 +431,20 @@ vector Wallet::signDigest( const digest_type& d, result.reserve( keys.size() ); - vector priv_keys; for( const auto& key : keys ) { - auto itr = _data.encrypted_private_keys.find( key ); + auto itr = _data.encrypted_private_keys.find(key); if( itr == _data.encrypted_private_keys.end() ) return vector(); if( itr->second.encrypted_private_key.size() == 0 ) return vector(); - auto plain_wif = fc::aes_decrypt( _decrypted_master_key, itr->second.encrypted_private_key ); + auto plain_wif = fc::aes_decrypt(_decrypted_master_key, itr->second.encrypted_private_key); auto wif = fc::raw::unpack(plain_wif); - auto priv = graphene::utilities::wif_to_key( wif ); + auto priv = graphene::utilities::wif_to_key(wif); if( !priv ) return vector(); - result.push_back( priv->sign_compact( d ) ); + result.push_back(priv->sign_compact(d)); } return result; @@ -448,5 +454,3 @@ const flat_set& Wallet::getAvailablePrivateKeys()const { return _available_private_keys; } - - diff --git a/programs/light_client/Wallet.hpp b/programs/light_client/Wallet.hpp index 070634df..a7a5d3e4 100644 --- a/programs/light_client/Wallet.hpp +++ b/programs/light_client/Wallet.hpp @@ -2,9 +2,11 @@ #pragma GCC diagnostic ignored "-Wunknown-pragmas" #include + #include #include #include +#include using std::string; using std::vector; @@ -16,7 +18,7 @@ using graphene::chain::digest_type; using graphene::chain::signature_type; using fc::optional; -QString toQString( public_key_type k ); +QString toQString(public_key_type k); struct key_data { @@ -26,7 +28,6 @@ struct key_data int32_t brain_sequence = -1; optional owner; /// if this key was derived from an owner key + sequence }; -FC_REFLECT( key_data, (label)(encrypted_private_key)(brain_sequence)(owner) ); struct wallet_file { @@ -37,14 +38,6 @@ struct wallet_file map encrypted_private_keys; }; -FC_REFLECT( wallet_file, - (encrypted_brain_key) - (brain_key_digest) - (encrypted_master_key) - (master_key_digest) - (encrypted_private_keys) - ); - /** * @class Wallet @@ -62,15 +55,15 @@ class Wallet : public QObject Wallet( QObject* parent = nullptr ); ~Wallet(); - Q_INVOKABLE bool open( QString file_path ); + Q_INVOKABLE bool open(QString file_path); Q_INVOKABLE bool close(); - Q_INVOKABLE bool isOpen()const; + bool isOpen()const; Q_INVOKABLE bool save(); - Q_INVOKABLE bool saveAs( QString file_path ); - Q_INVOKABLE bool create( QString file_path, QString password, QString brain_key = QString() ); + Q_INVOKABLE bool saveAs(QString file_path); + Q_INVOKABLE bool create(QString file_path, QString password, QString brain_key = QString()); /** required to generate new owner keys */ - Q_INVOKABLE bool loadBrainKey( QString brain_key ); + Q_INVOKABLE bool loadBrainKey(QString brain_key); /** removes brain key to secure owner keys */ Q_INVOKABLE bool purgeBrainKey(); @@ -79,18 +72,18 @@ class Wallet : public QObject /** @pre hasBrainKey() */ Q_INVOKABLE QString getBrainKey(); - Q_INVOKABLE bool isLocked()const; - Q_INVOKABLE bool unlock( QString password ); + bool isLocked()const; + Q_INVOKABLE bool unlock(QString password); Q_INVOKABLE bool lock(); - Q_INVOKABLE bool changePassword( QString new_password ); + Q_INVOKABLE bool changePassword(QString new_password); /** * @pre !isLocked(); * @post save() * @return WIF private key */ - Q_INVOKABLE QString getActivePrivateKey( QString owner_public_key, uint32_t sequence ); - Q_INVOKABLE QString getActivePublicKey( QString owner_public_key, uint32_t sequence ); + Q_INVOKABLE QString getActivePrivateKey(QString owner_public_key, uint32_t sequence); + Q_INVOKABLE QString getActivePublicKey(QString owner_public_key, uint32_t sequence); /** * @pre !isLocked(); @@ -98,48 +91,48 @@ class Wallet : public QObject * @post save() * @return WIF private key */ - Q_INVOKABLE QString getOwnerPrivateKey( uint32_t sequence ); - Q_INVOKABLE QString getOwnerPublicKey( uint32_t sequence ); - Q_INVOKABLE QString getPublicKey( QString wif_private_key )const; + Q_INVOKABLE QString getOwnerPrivateKey(uint32_t sequence); + Q_INVOKABLE QString getOwnerPublicKey(uint32_t sequence); + Q_INVOKABLE QString getPublicKey(QString wif_private_key)const; - Q_INVOKABLE QString getKeyLabel( QString pubkey ); - Q_INVOKABLE bool setKeyLabel( QString pubkey, QString label ); - Q_INVOKABLE QString getPublicKey( QString label ); - Q_INVOKABLE QString getPrivateKey( QString pubkey ); - Q_INVOKABLE bool hasPrivateKey( QString pubkey, bool include_with_brain_key = false ); + Q_INVOKABLE QString getKeyLabel(QString pubkey); + Q_INVOKABLE bool setKeyLabel(QString pubkey, QString label); + Q_INVOKABLE QString getPublicKey(QString label); + Q_INVOKABLE QString getPrivateKey(QString pubkey); + Q_INVOKABLE bool hasPrivateKey(QString pubkey, bool include_with_brain_key = false); /** imports a public key and assigns it a label */ - Q_INVOKABLE bool importPublicKey( QString pubkey, QString label = QString() ); + Q_INVOKABLE bool importPublicKey(QString pubkey, QString label = QString()); /** * @param wifkey a private key in (WIF) Wallet Import Format * @pre !isLocked() **/ - Q_INVOKABLE bool importPrivateKey( QString wifkey, QString label = QString() ); + Q_INVOKABLE bool importPrivateKey(QString wifkey, QString label = QString()); /** removes the key, its lablel and its private key */ - Q_INVOKABLE bool removePublicKey( QString pubkey ); + Q_INVOKABLE bool removePublicKey(QString pubkey); /** removes only the private key, keeping the public key and label */ - Q_INVOKABLE bool removePrivateKey( QString pubkey ); + Q_INVOKABLE bool removePrivateKey(QString pubkey); /** * @param only_if_private filter any public keys for which the wallet lacks a private key * @return a list of PUBLICKEY, LABEL for all known public keys */ - Q_INVOKABLE QList > getAllPublicKeys( bool only_if_private )const; + Q_INVOKABLE QList> getAllPublicKeys(bool only_if_private)const; /** * @pre !isLocked() */ - vector signDigest( const digest_type& d, - const set& keys )const; + vector signDigest(const digest_type& d, + const set& keys)const; const flat_set& getAvailablePrivateKeys()const; Q_SIGNALS: - void isLockedChanged( bool state ); - void isOpenChanged( bool state ); + void isLockedChanged(bool state); + void isOpenChanged(bool state); private: fc::path _wallet_file_path; @@ -150,3 +143,13 @@ class Wallet : public QObject map _label_to_key; QString _brain_key; }; +QML_DECLARE_TYPE(Wallet) + +FC_REFLECT( key_data, (label)(encrypted_private_key)(brain_sequence)(owner) ) +FC_REFLECT( wallet_file, + (encrypted_brain_key) + (brain_key_digest) + (encrypted_master_key) + (master_key_digest) + (encrypted_private_keys) + ) diff --git a/programs/light_client/main.cpp b/programs/light_client/main.cpp index 5be59d5a..dc0085f0 100644 --- a/programs/light_client/main.cpp +++ b/programs/light_client/main.cpp @@ -7,6 +7,7 @@ #include "Transaction.hpp" #include "Operations.hpp" #include "Balance.hpp" +#include "Wallet.hpp" class Crypto { Q_GADGET @@ -37,6 +38,7 @@ int main(int argc, char *argv[]) qmlRegisterType("Graphene.Client", 0, 1, "Balance"); qmlRegisterType("Graphene.Client", 0, 1, "Account"); qmlRegisterType("Graphene.Client", 0, 1, "DataModel"); + qmlRegisterType("Graphene.Client", 0, 1, "Wallet"); qmlRegisterType("Graphene.Client", 0, 1, "GrapheneApplication"); qmlRegisterType("Graphene.Client", 0, 1, "Transaction"); diff --git a/programs/light_client/qml/AccountPicker.qml b/programs/light_client/qml/AccountPicker.qml index f3dc4287..2b682928 100644 --- a/programs/light_client/qml/AccountPicker.qml +++ b/programs/light_client/qml/AccountPicker.qml @@ -17,6 +17,9 @@ RowLayout { /// The Account object the user has selected property Account account + /// A real in the range [0,1] representing the amount of control the wallet has over this account + property real accountControlLevel: account && account.isLoaded? account.getActiveControl(app.wallet) : 0 + onAccountControlLevelChanged: console.log("New acl: " + accountControlLevel) /// An Array of Balance objects held by account property var balances: account? Object.keys(account.balances).map(function(key){return account.balances[key]}) : null @@ -32,6 +35,8 @@ RowLayout { name: account && account.name == accountNameField.text? accountNameField.text : "" width: Scaling.cm(2) height: Scaling.cm(2) + showOwnership: accountControlLevel > 0 + fullOwnership: accountControlLevel >= 1 } Column { Layout.fillWidth: true @@ -65,8 +70,8 @@ RowLayout { text = Qt.binding(function() { if (account == null) return qsTr("Account does not exist.") - var text = qsTr("Account ID: %1").arg(account.id < 0? qsTr("Loading...") - : account.id) + var text = qsTr("Account ID: %1").arg(!account.isLoaded? qsTr("Loading...") + : account.id) if (showBalance >= 0) { var bal = balances[showBalance] text += "
" + qsTr("Balance: %1 %2").arg(String(bal.amountReal())) diff --git a/programs/light_client/qml/Identicon.qml b/programs/light_client/qml/Identicon.qml index fb674e55..0189bdbe 100644 --- a/programs/light_client/qml/Identicon.qml +++ b/programs/light_client/qml/Identicon.qml @@ -7,6 +7,9 @@ Canvas { contextType: "2d" property var name + property bool showOwnership: false + property bool fullOwnership: false + onNameChanged: requestPaint() onPaint: { @@ -36,4 +39,28 @@ Canvas { draw_circle(context, 2*radius, centerY, radius) } } + + Rectangle { + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + height: parent.height / 4 + color: fullOwnership? "green" : "blue" + opacity: .6 + visible: showOwnership + + Text { + anchors.centerIn: parent + color: "white" + font.pixelSize: parent.height * 2/3 + font.bold: true + text: fullOwnership? qsTr("FULL") : qsTr("PARTIAL") + } + TooltipArea { + text: fullOwnership? qsTr("You have full control of this account, and can use it to perform actions directly.") + : qsTr("You have partial control of this account, and can vote for it to take an action.") + } + } } diff --git a/programs/light_client/qml/TooltipArea.qml b/programs/light_client/qml/TooltipArea.qml new file mode 100644 index 00000000..5dda449e --- /dev/null +++ b/programs/light_client/qml/TooltipArea.qml @@ -0,0 +1,23 @@ +import QtQuick 2.4 +import QtQuick.Controls.Private 1.0 + +// TooltipArea.qml +// This file contains private Qt Quick modules that might change in future versions of Qt +// Tested on: Qt 5.4.1 + +MouseArea { + id: _root + property string text: "" + + anchors.fill: parent + hoverEnabled: _root.enabled + + onExited: Tooltip.hideText() + onCanceled: Tooltip.hideText() + + Timer { + interval: 1000 + running: _root.enabled && _root.containsMouse && _root.text.length + onTriggered: Tooltip.showText(_root, Qt.point(_root.mouseX, _root.mouseY), _root.text) + } +} diff --git a/programs/light_client/qml/TransactionConfirmationForm.qml b/programs/light_client/qml/TransactionConfirmationForm.qml index 49d2d3c4..e0be7ccf 100644 --- a/programs/light_client/qml/TransactionConfirmationForm.qml +++ b/programs/light_client/qml/TransactionConfirmationForm.qml @@ -24,10 +24,8 @@ FormBase { onDisplay: { trx = app.createTransaction() - console.log(JSON.stringify(arg)) for (var op in arg) trx.appendOperation(arg[op]) - console.log(JSON.stringify(trx)) } Component { diff --git a/programs/light_client/qml/TransferForm.qml b/programs/light_client/qml/TransferForm.qml index 00780e69..21ffd3b7 100644 --- a/programs/light_client/qml/TransferForm.qml +++ b/programs/light_client/qml/TransferForm.qml @@ -100,7 +100,7 @@ FormBase { Item { Layout.fillWidth: true } Button { text: qsTr("Cancel") - onClicked: canceled() + onClicked: canceled({}) } Button { id: transferButton diff --git a/programs/light_client/qml/main.qml b/programs/light_client/qml/main.qml index 74ffae61..e3a39948 100644 --- a/programs/light_client/qml/main.qml +++ b/programs/light_client/qml/main.qml @@ -31,13 +31,21 @@ ApplicationWindow { statusBar: StatusBar { Label { anchors.right: parent.right - text: app.isConnected? qsTr("Connected") : qsTr("Disconnected") + text: qsTr("Network: %1 Wallet: %2").arg(app.isConnected? qsTr("Connected") : qsTr("Disconnected")) + .arg(app.wallet.isLocked? qsTr("Locked") : qsTr("Unlocked")) } } GrapheneApplication { id: app - } + Component.onCompleted: { + var walletFile = appSettings.walletPath + "/wallet.json" + if (!wallet.open(walletFile)) { + // TODO: onboarding experience + wallet.create(walletFile, "default password", "default brain key") + } + } + } Timer { running: !app.isConnected interval: 5000 @@ -48,6 +56,8 @@ ApplicationWindow { Settings { id: appSettings category: "appSettings" + + property string walletPath: app.defaultDataPath() } Connections { target: app @@ -152,6 +162,32 @@ ApplicationWindow { } } } + TextField { + id: passwordField + echoMode: TextInput.Password + } + Button { + text: app.wallet.isLocked? "Unlock wallet" : "Lock wallet" + onClicked: { + if (app.wallet.isLocked) + app.wallet.unlock(passwordField.text) + else + app.wallet.lock() + } + } + TextField { + id: keyField + placeholderText: "Private key" + } + TextField { + id: keyLabelField + placeholderText: "Key label" + } + Button { + text: "Import key" + enabled: !app.wallet.isLocked && keyField.text && keyLabelField.text + onClicked: app.wallet.importPrivateKey(keyField.text, keyLabelField.text) + } } FormBox { diff --git a/programs/light_client/qml/qml.qrc b/programs/light_client/qml/qml.qrc index 3e9ec224..ae4c86f6 100644 --- a/programs/light_client/qml/qml.qrc +++ b/programs/light_client/qml/qml.qrc @@ -9,6 +9,7 @@ Scaling.qml Identicon.qml AccountPicker.qml + TooltipArea.qml jdenticon/jdenticon-1.0.1.min.js From 75f05a8b1f16cf442d95b70dc7abf864df7f0d02 Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Wed, 29 Jul 2015 15:59:15 -0400 Subject: [PATCH 3/5] [GUI] Fixes in TransferForm.qml --- programs/light_client/qml/AccountPicker.qml | 2 +- programs/light_client/qml/TransferForm.qml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/programs/light_client/qml/AccountPicker.qml b/programs/light_client/qml/AccountPicker.qml index 2b682928..91ed1dd0 100644 --- a/programs/light_client/qml/AccountPicker.qml +++ b/programs/light_client/qml/AccountPicker.qml @@ -19,9 +19,9 @@ RowLayout { property Account account /// A real in the range [0,1] representing the amount of control the wallet has over this account property real accountControlLevel: account && account.isLoaded? account.getActiveControl(app.wallet) : 0 - onAccountControlLevelChanged: console.log("New acl: " + accountControlLevel) /// An Array of Balance objects held by account property var balances: account? Object.keys(account.balances).map(function(key){return account.balances[key]}) + .filter(function(balance){return balance.amount > 0}) : null /// Set the name field to have active focus diff --git a/programs/light_client/qml/TransferForm.qml b/programs/light_client/qml/TransferForm.qml index 21ffd3b7..636e3ad5 100644 --- a/programs/light_client/qml/TransferForm.qml +++ b/programs/light_client/qml/TransferForm.qml @@ -92,9 +92,9 @@ FormBase { Text { font.pixelSize: assetField.height / 2.5 text: { - if (!senderPicker.account) + if (!senderPicker.account || !amountField.maxBalance) return "" - return qsTr("Fee:
") + operation().fee / amountField.precisionAdjustment + " CORE" + return qsTr("Fee:
") + amountField.maxBalance.type.formatAmount(operation().fee) + " CORE" } } Item { Layout.fillWidth: true } From 391cb5e6279f6f0fc822156c659151f29e4a2a60 Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Wed, 29 Jul 2015 16:36:03 -0400 Subject: [PATCH 4/5] [GUI] Fix args to callback in FormBox.qml --- programs/light_client/qml/FormBox.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/light_client/qml/FormBox.qml b/programs/light_client/qml/FormBox.qml index 8d6e41a5..137173aa 100644 --- a/programs/light_client/qml/FormBox.qml +++ b/programs/light_client/qml/FormBox.qml @@ -90,7 +90,7 @@ Rectangle { greySheet.closed() formContainer.data = [] if (internal.callback instanceof Function) - internal.callback() + internal.callback.apply(this, internal.callbackArgs) internal.callback = undefined internal.callbackArgs = [] } From 82ea3c1edd75d86ab27a2819f68b15782986758d Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Wed, 29 Jul 2015 17:56:37 -0400 Subject: [PATCH 5/5] [GUI] Add transaction signing TODO: encrypt the memo (some work to facilitate this is done in this commit) --- libraries/chain/protocol/memo.cpp | 2 +- programs/light_client/Account.cpp | 14 +++++- programs/light_client/Account.hpp | 3 ++ programs/light_client/Operations.cpp | 44 +++++++++++++++++++ programs/light_client/Operations.hpp | 9 +++- programs/light_client/Transaction.hpp | 6 ++- programs/light_client/Wallet.cpp | 14 ++++++ programs/light_client/Wallet.hpp | 3 ++ .../qml/TransactionConfirmationForm.qml | 33 ++++++++++++++ programs/light_client/qml/TransferForm.qml | 4 +- programs/light_client/qml/main.qml | 4 +- 11 files changed, 127 insertions(+), 9 deletions(-) diff --git a/libraries/chain/protocol/memo.cpp b/libraries/chain/protocol/memo.cpp index d6399220..9435bf4a 100644 --- a/libraries/chain/protocol/memo.cpp +++ b/libraries/chain/protocol/memo.cpp @@ -33,7 +33,7 @@ void memo_data::set_message(const fc::ecc::private_key& priv, const fc::ecc::pub string memo_data::get_message(const fc::ecc::private_key& priv, const fc::ecc::public_key& pub)const { - if( from != public_key_type() ) + if( from != public_key_type() ) { auto secret = priv.get_shared_secret(pub); auto nonce_plus_secret = fc::sha512::hash(fc::to_string(nonce) + secret.str()); diff --git a/programs/light_client/Account.cpp b/programs/light_client/Account.cpp index c4677235..60740b38 100644 --- a/programs/light_client/Account.cpp +++ b/programs/light_client/Account.cpp @@ -6,10 +6,15 @@ void Account::setAccountObject(const graphene::chain::account_object& obj) { - auto old_name = m_account.name; + auto oldName = m_account.name; + auto oldMemoKey = memoKey(); + m_account = obj; - if (old_name != m_account.name) + if (oldName != m_account.name) Q_EMIT nameChanged(); + if (oldMemoKey != memoKey()) + Q_EMIT memoKeyChanged(); + if (!m_loaded) { m_loaded = true; Q_EMIT loaded(); @@ -17,6 +22,11 @@ void Account::setAccountObject(const graphene::chain::account_object& obj) } } +QString Account::memoKey() const +{ + return toQString(m_account.options.memo_key); +} + QQmlListProperty Account::balances() { auto count = [](QQmlListProperty* list) { diff --git a/programs/light_client/Account.hpp b/programs/light_client/Account.hpp index 6facefa2..943f0d06 100644 --- a/programs/light_client/Account.hpp +++ b/programs/light_client/Account.hpp @@ -21,6 +21,7 @@ class Account : public GrapheneObject { Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QQmlListProperty balances READ balances NOTIFY balancesChanged) + Q_PROPERTY(QString memoKey READ memoKey NOTIFY memoKeyChanged) Q_PROPERTY(bool isLoaded MEMBER m_loaded NOTIFY loaded) account_object m_account; @@ -36,6 +37,7 @@ public: void setAccountObject(const account_object& obj); QString name()const { return QString::fromStdString(m_account.name); } + QString memoKey()const; QQmlListProperty balances(); void setBalances(QList balances) { @@ -60,5 +62,6 @@ public: Q_SIGNALS: void nameChanged(); void balancesChanged(); + void memoKeyChanged(); void loaded(); }; diff --git a/programs/light_client/Operations.cpp b/programs/light_client/Operations.cpp index 6c502972..bca91508 100644 --- a/programs/light_client/Operations.cpp +++ b/programs/light_client/Operations.cpp @@ -1,4 +1,7 @@ #include "Operations.hpp" +#include "Wallet.hpp" + +#include #include @@ -31,6 +34,38 @@ QString TransferOperation::memo() const { return memo; } +bool TransferOperation::canEncryptMemo(Wallet* wallet, ChainDataModel* model) const +{ + if (!m_op.memo) return false; + auto pub = model->getAccount(sender())->memoKey(); + if (!wallet->hasPrivateKey(pub)) return false; + return graphene::utilities::wif_to_key(wallet->getPrivateKey(pub).toStdString()).valid(); +} + +bool TransferOperation::canDecryptMemo(Wallet* wallet, ChainDataModel* model) const +{ + if (!m_op.memo) return false; + auto pub = model->getAccount(receiver())->memoKey(); + if (!wallet->hasPrivateKey(pub)) return false; + return graphene::utilities::wif_to_key(wallet->getPrivateKey(pub).toStdString()).valid(); +} + +QString TransferOperation::decryptedMemo(Wallet* wallet, ChainDataModel* model) const +{ + fc::ecc::private_key privateKey; + fc::ecc::public_key publicKey; + + if (canEncryptMemo(wallet, model)) { + privateKey = *graphene::utilities::wif_to_key(wallet->getPrivateKey(model->getAccount(sender())->memoKey()).toStdString()); + publicKey = m_op.memo->to; + } else if (canDecryptMemo(wallet, model)) { + privateKey = *graphene::utilities::wif_to_key(wallet->getPrivateKey(model->getAccount(receiver())->memoKey()).toStdString()); + publicKey = m_op.memo->from; + } else return QString::null; + + return QString::fromStdString(m_op.memo->get_message(privateKey, publicKey)); +} + void TransferOperation::setMemo(QString memo) { if (memo == this->memo()) return; @@ -41,3 +76,12 @@ void TransferOperation::setMemo(QString memo) { m_op.memo->set_message({}, {}, memo.toStdString()); Q_EMIT memoChanged(); } + +void TransferOperation::encryptMemo(Wallet* wallet, ChainDataModel* model) +{ + if (!canEncryptMemo(wallet, model)) return; + auto privateKey = graphene::utilities::wif_to_key(wallet->getPrivateKey(model->getAccount(sender())->memoKey()).toStdString()); + if (!privateKey) return; + m_op.memo->set_message(*privateKey, public_key_type(model->getAccount(receiver())->memoKey().toStdString()), memo().toStdString()); + Q_EMIT memoChanged(); +} diff --git a/programs/light_client/Operations.hpp b/programs/light_client/Operations.hpp index a01e2d19..2a226ed6 100644 --- a/programs/light_client/Operations.hpp +++ b/programs/light_client/Operations.hpp @@ -57,9 +57,13 @@ public: qint64 amount() const { return m_op.amount.amount.value; } ObjectId amountType() const { return m_op.amount.asset_id.instance.value; } /// This does not deal with encrypted memos. The memo stored here is unencrypted. The encryption step must be - /// performed elsewhere. + /// performed by calling encryptMemo() QString memo() const; + Q_INVOKABLE bool canEncryptMemo(Wallet* wallet, ChainDataModel* model) const; + Q_INVOKABLE bool canDecryptMemo(Wallet* wallet, ChainDataModel* model) const; + Q_INVOKABLE QString decryptedMemo(Wallet* wallet, ChainDataModel* model) const; + const graphene::chain::transfer_operation& operation() const { return m_op; } graphene::chain::transfer_operation& operation() { return m_op; } @@ -101,8 +105,9 @@ public Q_SLOTS: Q_EMIT amountTypeChanged(); } /// This does not deal with encrypted memos. The memo stored here is unencrypted. The encryption step must be - /// performed elsewhere. + /// performed by calling encryptMemo() void setMemo(QString memo); + void encryptMemo(Wallet* wallet, ChainDataModel* model); Q_SIGNALS: void feeChanged(); diff --git a/programs/light_client/Transaction.hpp b/programs/light_client/Transaction.hpp index 69b2bc2a..861aaa07 100644 --- a/programs/light_client/Transaction.hpp +++ b/programs/light_client/Transaction.hpp @@ -22,6 +22,10 @@ public: return m_transaction.operations.size(); } + graphene::chain::signed_transaction& internalTransaction() { + return m_transaction; + } + public slots: void setStatus(Status status) { @@ -51,5 +55,5 @@ private: Q_PROPERTY(QQmlListProperty operations READ operations NOTIFY operationsChanged) Status m_status = Unbroadcasted; - graphene::chain::transaction m_transaction; + graphene::chain::signed_transaction m_transaction; }; diff --git a/programs/light_client/Wallet.cpp b/programs/light_client/Wallet.cpp index 4d218ab9..c46b9e16 100644 --- a/programs/light_client/Wallet.cpp +++ b/programs/light_client/Wallet.cpp @@ -1,5 +1,9 @@ +#include "Transaction.hpp" #include "Wallet.hpp" + #include +#include + #include #include @@ -348,6 +352,16 @@ QList> Wallet::getAllPublicKeys(bool only_if_private)cons return result; } +void Wallet::sign(Transaction* transaction) const +{ + if (transaction == nullptr) return; + + auto& trx = transaction->internalTransaction(); + flat_set pubKeys = getAvailablePrivateKeys(); + trx.signatures = signDigest(trx.digest(), set(pubKeys.begin(), pubKeys.end())); + idump((trx)); +} + QString Wallet::getPublicKey(QString label) { if( !isOpen() ) return QString::null; diff --git a/programs/light_client/Wallet.hpp b/programs/light_client/Wallet.hpp index a7a5d3e4..e8a206e2 100644 --- a/programs/light_client/Wallet.hpp +++ b/programs/light_client/Wallet.hpp @@ -18,6 +18,8 @@ using graphene::chain::digest_type; using graphene::chain::signature_type; using fc::optional; +class Transaction; + QString toQString(public_key_type k); struct key_data @@ -125,6 +127,7 @@ class Wallet : public QObject /** * @pre !isLocked() */ + Q_INVOKABLE void sign(Transaction* transaction) const; vector signDigest(const digest_type& d, const set& keys)const; diff --git a/programs/light_client/qml/TransactionConfirmationForm.qml b/programs/light_client/qml/TransactionConfirmationForm.qml index e0be7ccf..2924665c 100644 --- a/programs/light_client/qml/TransactionConfirmationForm.qml +++ b/programs/light_client/qml/TransactionConfirmationForm.qml @@ -62,4 +62,37 @@ FormBase { Loader { sourceComponent: trx && Array.prototype.slice.call(trx.operations).length > 0? transactionDelegate : undefined } + RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + Button { + text: qsTr("Cancel") + onClicked: { + canceled({}) + trx = null + } + } + TextField { + id: passwordField + Layout.preferredWidth: app.wallet.isLocked? Scaling.cm(4) : 0 + echoMode: TextInput.Password + placeholderText: qsTr("Wallet password") + visible: width > 0 + onAccepted: finishButton.clicked() + + Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + } + Button { + id: finishButton + text: app.wallet.isLocked? qsTr("Unlock") : qsTr("Finish") + onClicked: { + if (app.wallet.isLocked) + app.wallet.unlock(passwordField.text) + else { + app.wallet.sign(trx) + completed(trx) + } + } + } + } } diff --git a/programs/light_client/qml/TransferForm.qml b/programs/light_client/qml/TransferForm.qml index 636e3ad5..310a2765 100644 --- a/programs/light_client/qml/TransferForm.qml +++ b/programs/light_client/qml/TransferForm.qml @@ -104,7 +104,9 @@ FormBase { } Button { id: transferButton - text: qsTr("Transfer") + text: !senderAccount || + !senderAccount.isLoaded || + senderPicker.accountControlLevel >= 1? qsTr("Transfer") : qsTr("Propose") enabled: senderPicker.account && recipientPicker.account && senderPicker.account !== recipientPicker.account && amountField.value onClicked: completed([operation()]) } diff --git a/programs/light_client/qml/main.qml b/programs/light_client/qml/main.qml index e3a39948..0c25d019 100644 --- a/programs/light_client/qml/main.qml +++ b/programs/light_client/qml/main.qml @@ -75,8 +75,8 @@ ApplicationWindow { // TODO: make back into a preview and confirm dialog var back = Qt.createComponent("TransactionConfirmationForm.qml") formBox.showForm(Qt.createComponent("FormFlipper.qml"), {frontComponent: front, backComponent: back}, - function() { - console.log("Closed form") + function(arg) { + console.log("Closed form: " + JSON.stringify(arg)) }) } }