From 82ea3c1edd75d86ab27a2819f68b15782986758d Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Wed, 29 Jul 2015 17:56:37 -0400 Subject: [PATCH] [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)) }) } }