From 5d7ae4e6a836b8169fa2e2987b0a122de7192733 Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Mon, 27 Jul 2015 16:01:16 -0400 Subject: [PATCH] [GUI] More work to support transactions - Refactor GUI with FormBase.qml - Fix memo handling in TransferOperation - Add TransactionConfirmationForm.qml which will eventually display a transaction for confirmation --- libraries/chain/protocol/memo.cpp | 4 +- programs/light_client/Account.hpp | 1 - programs/light_client/GrapheneApplication.cpp | 6 + programs/light_client/GrapheneApplication.hpp | 5 + programs/light_client/Operations.cpp | 49 ++++-- programs/light_client/Operations.hpp | 22 +-- programs/light_client/Transaction.cpp | 5 + programs/light_client/Transaction.hpp | 2 +- programs/light_client/main.cpp | 7 +- programs/light_client/qml/FormBase.qml | 37 ++++ programs/light_client/qml/FormBox.qml | 2 + programs/light_client/qml/FormFlipper.qml | 19 ++- .../qml/TransactionConfirmationForm.qml | 32 ++++ programs/light_client/qml/TransferForm.qml | 161 ++++++++---------- programs/light_client/qml/main.qml | 2 +- programs/light_client/qml/qml.qrc | 2 + 16 files changed, 227 insertions(+), 129 deletions(-) create mode 100644 programs/light_client/qml/FormBase.qml create mode 100644 programs/light_client/qml/TransactionConfirmationForm.qml diff --git a/libraries/chain/protocol/memo.cpp b/libraries/chain/protocol/memo.cpp index 620ef108..d6399220 100644 --- a/libraries/chain/protocol/memo.cpp +++ b/libraries/chain/protocol/memo.cpp @@ -6,8 +6,10 @@ namespace graphene { namespace chain { void memo_data::set_message(const fc::ecc::private_key& priv, const fc::ecc::public_key& pub, const string& msg, uint64_t custom_nonce) { - if( from != public_key_type() ) + if( priv != fc::ecc::private_key() && public_key_type(pub) != public_key_type() ) { + from = priv.get_public_key(); + to = pub; if( custom_nonce == 0 ) { uint64_t entropy = fc::sha224::hash(fc::ecc::private_key::generate())._hash[0]; diff --git a/programs/light_client/Account.hpp b/programs/light_client/Account.hpp index 6f190f21..4840b3bc 100644 --- a/programs/light_client/Account.hpp +++ b/programs/light_client/Account.hpp @@ -51,7 +51,6 @@ public: void update(const account_balance_object& balance); - /** * Anything greater than 1.0 means full authority. * Anything between (0 and 1.0) means partial authority diff --git a/programs/light_client/GrapheneApplication.cpp b/programs/light_client/GrapheneApplication.cpp index 4a91bb71..d5cf050b 100644 --- a/programs/light_client/GrapheneApplication.cpp +++ b/programs/light_client/GrapheneApplication.cpp @@ -2,6 +2,7 @@ #include "ChainDataModel.hpp" #include "Wallet.hpp" #include "Operations.hpp" +#include "Transaction.hpp" #include @@ -74,6 +75,11 @@ void GrapheneApplication::start(QString apiurl, QString user, QString pass) } } +Transaction* GrapheneApplication::createTransaction() const +{ + return new Transaction; +} + Q_SLOT void GrapheneApplication::execute(const std::function& func)const { func(); diff --git a/programs/light_client/GrapheneApplication.hpp b/programs/light_client/GrapheneApplication.hpp index 4d40386a..552179b8 100644 --- a/programs/light_client/GrapheneApplication.hpp +++ b/programs/light_client/GrapheneApplication.hpp @@ -14,6 +14,8 @@ class websocket_client; class ChainDataModel; class OperationBuilder; +class OperationBase; +class Transaction; class Wallet; class GrapheneApplication : public QObject { Q_OBJECT @@ -62,6 +64,9 @@ public: return m_isConnected; } + /// Convenience method to get a Transaction in QML. Caller takes ownership of the new Transaction. + Q_INVOKABLE Transaction* createTransaction() const; + Q_SIGNALS: void exceptionThrown(QString message); void loginFailed(); diff --git a/programs/light_client/Operations.cpp b/programs/light_client/Operations.cpp index 2aa2bf48..6c502972 100644 --- a/programs/light_client/Operations.cpp +++ b/programs/light_client/Operations.cpp @@ -5,18 +5,39 @@ TransferOperation* OperationBuilder::transfer(ObjectId sender, ObjectId receiver, qint64 amount, ObjectId amountType, QString memo, ObjectId feeType) { - static fc::ecc::private_key dummyPrivate = fc::ecc::private_key::generate(); - static fc::ecc::public_key dummyPublic = fc::ecc::private_key::generate().get_public_key(); - TransferOperation* op = new TransferOperation; - op->setSender(sender); - op->setReceiver(receiver); - op->setAmount(amount); - op->setAmountType(amountType); - op->setMemo(memo); - op->setFeeType(feeType); - auto feeParameters = model.global_properties().parameters.current_fees->get(); - op->operation().memo = graphene::chain::memo_data(); - op->operation().memo->set_message(dummyPrivate, dummyPublic, memo.toStdString()); - op->setFee(op->operation().calculate_fee(feeParameters).value); - return op; + try { + TransferOperation* op = new TransferOperation; + op->setSender(sender); + op->setReceiver(receiver); + op->setAmount(amount); + op->setAmountType(amountType); + op->setMemo(memo); + op->setFeeType(feeType); + auto feeParameters = model.global_properties().parameters.current_fees->get(); + op->setFee(op->operation().calculate_fee(feeParameters).value); + return op; + } catch (const fc::exception& e) { + qDebug() << e.to_detail_string().c_str(); + return nullptr; + } +} + +QString TransferOperation::memo() const { + if (!m_op.memo) + return QString::null; + QString memo = QString::fromStdString(m_op.memo->get_message({}, {})); + while (memo.endsWith('\0')) + memo.chop(1); + return memo; +} + +void TransferOperation::setMemo(QString memo) { + if (memo == this->memo()) + return; + if (!m_op.memo) + m_op.memo = graphene::chain::memo_data(); + while (memo.size() % 32) + memo.append('\0'); + m_op.memo->set_message({}, {}, memo.toStdString()); + Q_EMIT memoChanged(); } diff --git a/programs/light_client/Operations.hpp b/programs/light_client/Operations.hpp index 1a6eed03..a01e2d19 100644 --- a/programs/light_client/Operations.hpp +++ b/programs/light_client/Operations.hpp @@ -37,7 +37,6 @@ class TransferOperation : public OperationBase { Q_PROPERTY(QString memo READ memo WRITE setMemo NOTIFY memoChanged) graphene::chain::transfer_operation m_op; - QString m_memo; public: TransferOperation(){} @@ -57,9 +56,9 @@ public: ObjectId receiver() const { return m_op.to.instance.value; } 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, and does not get stored in the - /// underlying graphene operation. The encryption and storage steps must be handled elsewhere. - QString memo() const { return m_memo; } + /// This does not deal with encrypted memos. The memo stored here is unencrypted. The encryption step must be + /// performed elsewhere. + QString memo() const; const graphene::chain::transfer_operation& operation() const { return m_op; } graphene::chain::transfer_operation& operation() { return m_op; } @@ -101,14 +100,9 @@ public Q_SLOTS: m_op.amount.asset_id = arg; Q_EMIT amountTypeChanged(); } - /// This does not deal with encrypted memos. The memo stored here is unencrypted, and does not get stored in the - /// underlying graphene operation. The encryption and storage steps must be handled elsewhere. - void setMemo(QString memo) { - if (memo == m_memo) - return; - m_memo = memo; - Q_EMIT memoChanged(); - } + /// This does not deal with encrypted memos. The memo stored here is unencrypted. The encryption step must be + /// performed elsewhere. + void setMemo(QString memo); Q_SIGNALS: void feeChanged(); @@ -136,7 +130,7 @@ public: OperationBuilder(ChainDataModel& model, QObject* parent = nullptr) : QObject(parent), model(model){} - Q_INVOKABLE TransferOperation* transfer(ObjectId sender, ObjectId receiver, - qint64 amount, ObjectId amountType, QString memo, ObjectId feeType); + Q_INVOKABLE TransferOperation* transfer(ObjectId sender, ObjectId receiver, qint64 amount, + ObjectId amountType, QString memo, ObjectId feeType); }; diff --git a/programs/light_client/Transaction.cpp b/programs/light_client/Transaction.cpp index 64f54374..5f13814a 100644 --- a/programs/light_client/Transaction.cpp +++ b/programs/light_client/Transaction.cpp @@ -43,6 +43,11 @@ OperationBase* Transaction::operationAt(int index) const { void Transaction::appendOperation(OperationBase* op) { + if (op == nullptr) + { + qWarning("Unable to append null operation to transaction"); + return; + } op->setParent(this); m_transaction.operations.push_back(op->genericOperation()); Q_EMIT operationsChanged(); diff --git a/programs/light_client/Transaction.hpp b/programs/light_client/Transaction.hpp index 7081054b..69b2bc2a 100644 --- a/programs/light_client/Transaction.hpp +++ b/programs/light_client/Transaction.hpp @@ -50,6 +50,6 @@ private: Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged) Q_PROPERTY(QQmlListProperty operations READ operations NOTIFY operationsChanged) - Status m_status; + Status m_status = Unbroadcasted; graphene::chain::transaction m_transaction; }; diff --git a/programs/light_client/main.cpp b/programs/light_client/main.cpp index 0226fb63..5be59d5a 100644 --- a/programs/light_client/main.cpp +++ b/programs/light_client/main.cpp @@ -20,7 +20,10 @@ QML_DECLARE_TYPE(Crypto) int main(int argc, char *argv[]) { - fc::thread::current().set_name( "main" ); +#ifndef NDEBUG + QQmlDebuggingEnabler enabler; +#endif + fc::thread::current().set_name("main"); QApplication app(argc, argv); app.setApplicationName("Graphene Client"); app.setOrganizationDomain("cryptonomex.org"); @@ -28,6 +31,7 @@ int main(int argc, char *argv[]) qRegisterMetaType>(); qRegisterMetaType(); + qRegisterMetaType>(); qmlRegisterType("Graphene.Client", 0, 1, "Asset"); qmlRegisterType("Graphene.Client", 0, 1, "Balance"); @@ -49,7 +53,6 @@ int main(int argc, char *argv[]) #ifdef NDEBUG engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); #else - QQmlDebuggingEnabler enabler; engine.load(QUrl(QStringLiteral("qml/main.qml"))); #endif diff --git a/programs/light_client/qml/FormBase.qml b/programs/light_client/qml/FormBase.qml new file mode 100644 index 00000000..2a4936d2 --- /dev/null +++ b/programs/light_client/qml/FormBase.qml @@ -0,0 +1,37 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.2 + +import Graphene.Client 0.1 + +import "." + +/** + * Base for all forms + * + * This base contains all of the properties, slots, and signals which all forms are expected to expose. It also + * automatically lays its children out in a ColumnLayout + */ +Rectangle { + anchors.fill: parent + + /// Reference to the GrapheneApplication object + property GrapheneApplication app + /// Parent should trigger this signal to notify the form that it is about to be displayed + /// See specific form for the argument semantics + signal display(var arg) + /// Emitted when the form is canceled -- see specific form for the argument semantics + signal canceled(var arg) + /// Emitted when the form is completed -- see specific form for the argument semantics + signal completed(var arg) + + default property alias childItems: childLayout.data + + ColumnLayout { + id: childLayout + anchors.centerIn: parent + width: parent.width - Scaling.cm(2) + spacing: Scaling.mm(5) + } +} diff --git a/programs/light_client/qml/FormBox.qml b/programs/light_client/qml/FormBox.qml index dae5a1ec..8d6e41a5 100644 --- a/programs/light_client/qml/FormBox.qml +++ b/programs/light_client/qml/FormBox.qml @@ -33,6 +33,8 @@ Rectangle { form.completed.connect(function(){state = "HIDDEN"; internal.callbackArgs = arguments}) if (closedCallback instanceof Function) internal.callback = closedCallback + // Notify the form that it's about to go live + form.display({}) state = "SHOWN" } diff --git a/programs/light_client/qml/FormFlipper.qml b/programs/light_client/qml/FormFlipper.qml index 52c7d09d..e104382c 100644 --- a/programs/light_client/qml/FormFlipper.qml +++ b/programs/light_client/qml/FormFlipper.qml @@ -8,9 +8,10 @@ Flipable { property Component frontComponent property Component backComponent - property GrapheneApplication app - signal canceled - signal completed + + signal display(var arg) + signal canceled(var arg) + signal completed(var arg) property bool flipped: false @@ -19,13 +20,11 @@ Flipable { front = frontComponent.createObject(flipable, {app: app, enabled: Qt.binding(function(){return !flipped})}) front.canceled.connect(function() { canceled.apply(this, arguments) }) front.completed.connect(function() { - if (back.hasOwnProperty("arguments")) - back.arguments = arguments + back.display.apply(this, arguments) flipped = true }) back.canceled.connect(function() { - if (front.hasOwnProperty("arguments")) - front.arguments = arguments + front.display.apply(this, arguments) flipped = false }) back.completed.connect(function() { completed.apply(this, arguments) }) @@ -35,8 +34,10 @@ Flipable { id: rotation origin.x: flipable.width/2 origin.y: flipable.height/2 - axis.x: 0; axis.y: 1; axis.z: 0 // set axis.y to 1 to rotate around y-axis - angle: 0 // the default angle + // set axis.y to 1 to rotate around y-axis + axis.x: 0; axis.y: 1; axis.z: 0 + // the default angle + angle: 0 } states: State { diff --git a/programs/light_client/qml/TransactionConfirmationForm.qml b/programs/light_client/qml/TransactionConfirmationForm.qml new file mode 100644 index 00000000..e2ae5e4b --- /dev/null +++ b/programs/light_client/qml/TransactionConfirmationForm.qml @@ -0,0 +1,32 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 +import QtQuick.Layouts 1.2 + +import Graphene.Client 0.1 + +import "." + +/** + * This is the form for previewing and approving a transaction prior to broadcasting it + * + * The arguments property should be populated with an Array of operations. These operations will be used to create a + * Transaction, display it, and get confirmation to sign and broadcast it. This form will populate the transaction with + * the operations and expiration details, sign it, and pass the signed transaction through the completed signal. + */ +FormBase { + id: base + + property Transaction trx + + Component.onCompleted: console.log("Made a transaction confirmation form") + Component.onDestruction: console.log("Destroyed a transaction confirmation form") + + onDisplay: { + trx = app.createTransaction() + console.log(JSON.stringify(arg)) + for (var op in arg) + trx.appendOperation(arg[op]) + console.log(JSON.stringify(trx)) + } +} diff --git a/programs/light_client/qml/TransferForm.qml b/programs/light_client/qml/TransferForm.qml index 30044519..00780e69 100644 --- a/programs/light_client/qml/TransferForm.qml +++ b/programs/light_client/qml/TransferForm.qml @@ -10,13 +10,8 @@ import "." /** * This is the form for transferring some amount of asset from one account to another. */ -Rectangle { - id: root - anchors.fill: parent - - property GrapheneApplication app - signal canceled - signal completed(TransferOperation op) +FormBase { + id: base /// The Account object for the sender property alias senderAccount: senderPicker.account @@ -34,90 +29,84 @@ Rectangle { Component.onCompleted: console.log("Made a transfer form") Component.onDestruction: console.log("Destroyed a transfer form") - ColumnLayout { - anchors.centerIn: parent - width: parent.width - Scaling.cm(2) - spacing: Scaling.mm(5) + AccountPicker { + id: senderPicker + // The senderPicker is really the heart of the form. Everything else in the form adjusts based on the account + // selected here. The assetField below updates to contain all assets this account has a nonzero balance in. + // The amountField updates based on the asset selected in the assetField to have the appropriate precision and + // to have a maximum value equal to the account's balance in that asset. The transfer button enables only when + // both accounts are set, and a nonzero amount is selected to be transferred. - AccountPicker { - id: senderPicker - // The senderPicker is really the heart of the form. Everything else in the form adjusts based on the account - // selected here. The assetField below updates to contain all assets this account has a nonzero balance in. - // The amountField updates based on the asset selected in the assetField to have the appropriate precision and - // to have a maximum value equal to the account's balance in that asset. The transfer button enables only when - // both accounts are set, and a nonzero amount is selected to be transferred. + app: base.app + Layout.fillWidth: true + Layout.minimumWidth: Scaling.cm(5) + Component.onCompleted: setFocus() + placeholderText: qsTr("Sender") + showBalance: balances? balances.reduce(function(foundIndex, balance, index) { + if (foundIndex >= 0) return foundIndex + return balance.type.symbol === assetField.currentText? index : -1 + }, -1) : -1 + onBalanceClicked: amountField.value = balance + } + AccountPicker { + id: recipientPicker + app: base.app + Layout.fillWidth: true + Layout.minimumWidth: Scaling.cm(5) + placeholderText: qsTr("Recipient") + layoutDirection: Qt.RightToLeft + } + TextField { + id: memoField + Layout.fillWidth: true + placeholderText: qsTr("Memo") + } + RowLayout { + Layout.fillWidth: true + SpinBox { + id: amountField + Layout.preferredWidth: Scaling.cm(4) + Layout.minimumWidth: Scaling.cm(1.5) + enabled: maxBalance + minimumValue: 0 + maximumValue: maxBalance? maxBalance.amountReal() : 0 + decimals: maxBalance? maxBalance.type.precision : 0 - app: root.app - Layout.fillWidth: true - Layout.minimumWidth: Scaling.cm(5) - Component.onCompleted: setFocus() - placeholderText: qsTr("Sender") - showBalance: balances? balances.reduce(function(foundIndex, balance, index) { - if (foundIndex >= 0) return foundIndex - return balance.type.symbol === assetField.currentText? index : -1 - }, -1) : -1 - onBalanceClicked: amountField.value = balance + property Balance maxBalance: assetField.enabled && senderPicker.showBalance >= 0? + senderPicker.balances[senderPicker.showBalance] : null + property int precisionAdjustment: maxBalance? Math.pow(10, maxBalance.type.precision) : 1 + + // Workaround to preserve value in case form gets disabled then re-enabled + onEnabledChanged: if (!enabled) __valueBackup = value + onMaximumValueChanged: if (enabled && maximumValue > __valueBackup) value = __valueBackup + property real __valueBackup } - AccountPicker { - id: recipientPicker - app: root.app - Layout.fillWidth: true - Layout.minimumWidth: Scaling.cm(5) - placeholderText: qsTr("Recipient") - layoutDirection: Qt.RightToLeft + ComboBox { + id: assetField + Layout.minimumWidth: Scaling.cm(1) + enabled: senderPicker.balances instanceof Array && senderPicker.balances.length > 0 + model: enabled? senderPicker.balances.filter(function(balance) { return balance.amount > 0 }) + .map(function(balance) { return balance.type.symbol }) + : ["Asset Type"] } - TextField { - id: memoField - Layout.fillWidth: true - placeholderText: qsTr("Memo") + Text { + font.pixelSize: assetField.height / 2.5 + text: { + if (!senderPicker.account) + return "" + return qsTr("Fee:
") + operation().fee / amountField.precisionAdjustment + " CORE" + } } - RowLayout { - Layout.fillWidth: true - SpinBox { - id: amountField - Layout.preferredWidth: Scaling.cm(4) - Layout.minimumWidth: Scaling.cm(1.5) - enabled: maxBalance - minimumValue: 0 - maximumValue: maxBalance? maxBalance.amountReal() : 0 - decimals: maxBalance? maxBalance.type.precision : 0 - - property Balance maxBalance: assetField.enabled && senderPicker.showBalance >= 0? - senderPicker.balances[senderPicker.showBalance] : null - property int precisionAdjustment: maxBalance? Math.pow(10, maxBalance.type.precision) : 1 - - // Workaround to preserve value in case form gets disabled then re-enabled - onEnabledChanged: if (!enabled) __valueBackup = value - onMaximumValueChanged: if (enabled && maximumValue > __valueBackup) value = __valueBackup - property real __valueBackup - } - ComboBox { - id: assetField - Layout.minimumWidth: Scaling.cm(1) - enabled: senderPicker.balances instanceof Array && senderPicker.balances.length > 0 - model: enabled? senderPicker.balances.filter(function(balance) { return balance.amount > 0 }) - .map(function(balance) { return balance.type.symbol }) - : ["Asset Type"] - } - Text { - font.pixelSize: assetField.height / 2.5 - text: { - if (!senderPicker.account) - return "" - return qsTr("Fee:
") + operation().fee / amountField.precisionAdjustment + " CORE" - } - } - Item { Layout.fillWidth: true } - Button { - text: qsTr("Cancel") - onClicked: canceled() - } - Button { - id: transferButton - text: qsTr("Transfer") - enabled: senderPicker.account && recipientPicker.account && senderPicker.account !== recipientPicker.account && amountField.value - onClicked: completed(operation) - } + Item { Layout.fillWidth: true } + Button { + text: qsTr("Cancel") + onClicked: canceled() + } + Button { + id: transferButton + text: qsTr("Transfer") + 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 f117f342..74ffae61 100644 --- a/programs/light_client/qml/main.qml +++ b/programs/light_client/qml/main.qml @@ -63,7 +63,7 @@ ApplicationWindow { onClicked: { var front = Qt.createComponent("TransferForm.qml") // TODO: make back into a preview and confirm dialog - var back = Qt.createComponent("TransferForm.qml") + var back = Qt.createComponent("TransactionConfirmationForm.qml") formBox.showForm(Qt.createComponent("FormFlipper.qml"), {frontComponent: front, backComponent: back}, function() { console.log("Closed form") diff --git a/programs/light_client/qml/qml.qrc b/programs/light_client/qml/qml.qrc index cda31e3e..3e9ec224 100644 --- a/programs/light_client/qml/qml.qrc +++ b/programs/light_client/qml/qml.qrc @@ -1,7 +1,9 @@ main.qml + FormBase.qml TransferForm.qml + TransactionConfirmationForm.qml FormBox.qml FormFlipper.qml Scaling.qml