From f72f07b05bcacdd5b10e4e188471f7aad4b911b7 Mon Sep 17 00:00:00 2001 From: Nathan Hourt Date: Tue, 21 Jul 2015 17:09:44 -0400 Subject: [PATCH] [GUI] Early implementation of operations --- programs/light_client/CMakeLists.txt | 3 +- programs/light_client/ClientDataModel.cpp | 32 ++++++++++- programs/light_client/ClientDataModel.hpp | 39 ++++++++----- programs/light_client/Operations.cpp | 22 +++++++ programs/light_client/Operations.hpp | 64 +++++++++++++++++++++ programs/light_client/main.cpp | 4 ++ programs/light_client/qml/AccountPicker.qml | 6 +- programs/light_client/qml/TransferForm.qml | 17 +++++- 8 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 programs/light_client/Operations.cpp create mode 100644 programs/light_client/Operations.hpp diff --git a/programs/light_client/CMakeLists.txt b/programs/light_client/CMakeLists.txt index 5931383f..788bb617 100644 --- a/programs/light_client/CMakeLists.txt +++ b/programs/light_client/CMakeLists.txt @@ -18,7 +18,8 @@ if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") qt5_add_resources(QML_QRC qml/qml.qrc) endif() -add_executable(light_client ClientDataModel.cpp ClientDataModel.hpp main.cpp ${QML_QRC} ${QML}) +add_executable(light_client ClientDataModel.cpp ClientDataModel.hpp Operations.cpp main.cpp ${QML_QRC} ${QML}) + if (CMAKE_VERSION VERSION_LESS 3.0) add_dependencies(light_client gen_qrc) endif() diff --git a/programs/light_client/ClientDataModel.cpp b/programs/light_client/ClientDataModel.cpp index 309fcade..deda45c2 100644 --- a/programs/light_client/ClientDataModel.cpp +++ b/programs/light_client/ClientDataModel.cpp @@ -1,4 +1,5 @@ #include "ClientDataModel.hpp" +#include "Operations.hpp" #include #include @@ -18,7 +19,16 @@ QString idToString(graphene::db::object_id_type id) { } ChainDataModel::ChainDataModel(fc::thread& t, QObject* parent) -:QObject(parent),m_rpc_thread(&t){} + :QObject(parent),m_rpc_thread(&t){} + +void ChainDataModel::setDatabaseAPI(fc::api dbapi) { + m_db_api = dbapi; + m_rpc_thread->async([this] { + m_global_properties = m_db_api->get_global_properties(); + m_db_api->subscribe_to_objects([this](const variant& v) { m_global_properties = v.as(); }, + {m_global_properties.id}); + }); +} Asset* ChainDataModel::getAsset(ObjectId id) { @@ -275,6 +285,7 @@ GrapheneApplication::GrapheneApplication(QObject* parent) this, &GrapheneApplication::execute); m_model = new ChainDataModel(m_thread, this); + m_operationBuilder = new OperationBuilder(*m_model, this); connect(m_model, &ChainDataModel::queueExecute, this, &GrapheneApplication::execute); @@ -329,6 +340,7 @@ void GrapheneApplication::start(QString apiurl, QString user, QString pass) Q_EMIT exceptionThrown(QString::fromStdString(e.to_string())); } } + Q_SLOT void GrapheneApplication::execute(const std::function& func)const { func(); @@ -341,3 +353,21 @@ void Balance::update(const account_balance_object& update) emit amountChanged(); } } + + +void Asset::update(const asset_object& asset) +{ + if (asset.id.instance() != id()) + setProperty("id", QVariant::fromValue(asset.id.instance())); + if (asset.symbol != m_symbol.toStdString()) { + m_symbol = QString::fromStdString(asset.symbol); + Q_EMIT symbolChanged(); + } + if (asset.precision != m_precision) { + m_precision = asset.precision; + Q_EMIT precisionChanged(); + } + + if (asset.options.core_exchange_rate != coreExchangeRate) + coreExchangeRate = asset.options.core_exchange_rate; +} diff --git a/programs/light_client/ClientDataModel.hpp b/programs/light_client/ClientDataModel.hpp index 7c1db5e0..ade9fee1 100644 --- a/programs/light_client/ClientDataModel.hpp +++ b/programs/light_client/ClientDataModel.hpp @@ -64,6 +64,8 @@ class Asset : public GrapheneObject { QString m_symbol; quint32 m_precision; + graphene::chain::price coreExchangeRate; + public: Asset(ObjectId id = -1, QString symbol = QString(), quint32 precision = 0, QObject* parent = nullptr) : GrapheneObject(id, parent), m_symbol(symbol), m_precision(precision) @@ -80,6 +82,8 @@ public: return power; } + void update(const graphene::chain::asset_object& asset); + Q_SIGNALS: void symbolChanged(); void precisionChanged(); @@ -177,36 +181,43 @@ public: ChainDataModel(){} ChainDataModel( fc::thread& t, QObject* parent = nullptr ); - void setDatabaseAPI( fc::api dbapi ){ m_db_api = dbapi; } + void setDatabaseAPI(fc::api dbapi); + + const graphene::chain::global_property_object& global_properties() const { return m_global_properties; } Q_SIGNALS: void queueExecute( const std::function& ); void exceptionThrown( QString message ); private: - fc::thread* m_rpc_thread = nullptr; - std::string m_api_url; - fc::api m_db_api; + fc::thread* m_rpc_thread = nullptr; + std::string m_api_url; + fc::api m_db_api; - ObjectId m_account_query_num = -1; - account_multi_index_type m_accounts; - asset_multi_index_type m_assets; + graphene::chain::global_property_object m_global_properties; + + ObjectId m_account_query_num = -1; + account_multi_index_type m_accounts; + asset_multi_index_type m_assets; }; +class OperationBuilder; class GrapheneApplication : public QObject { Q_OBJECT Q_PROPERTY(ChainDataModel* model READ model CONSTANT) + Q_PROPERTY(OperationBuilder* operationBuilder READ operationBuilder CONSTANT) Q_PROPERTY(bool isConnected READ isConnected NOTIFY isConnectedChanged) - fc::thread m_thread; - ChainDataModel* m_model = nullptr; - bool m_isConnected = false; + fc::thread m_thread; + ChainDataModel* m_model = nullptr; + OperationBuilder* m_operationBuilder = nullptr; + bool m_isConnected = false; boost::signals2::scoped_connection m_connectionClosed; - std::shared_ptr m_client; + std::shared_ptr m_client; fc::future m_done; void setIsConnected(bool v); @@ -218,10 +229,12 @@ public: GrapheneApplication(QObject* parent = nullptr); ~GrapheneApplication(); - ChainDataModel* model() const - { + ChainDataModel* model() const { return m_model; } + OperationBuilder* operationBuilder() const { + return m_operationBuilder; + } Q_INVOKABLE void start(QString apiUrl, QString user, diff --git a/programs/light_client/Operations.cpp b/programs/light_client/Operations.cpp new file mode 100644 index 00000000..934d7cae --- /dev/null +++ b/programs/light_client/Operations.cpp @@ -0,0 +1,22 @@ +#include "Operations.hpp" + +#include + +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; + 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; +} diff --git a/programs/light_client/Operations.hpp b/programs/light_client/Operations.hpp new file mode 100644 index 00000000..1b49a3f4 --- /dev/null +++ b/programs/light_client/Operations.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "ClientDataModel.hpp" + +#include + +#include + +class TransferOperation { + Q_GADGET + Q_PROPERTY(qint64 fee READ fee) + Q_PROPERTY(ObjectId feeType READ feeType) + Q_PROPERTY(ObjectId sender READ sender WRITE setSender) + Q_PROPERTY(ObjectId receiver READ receiver WRITE setReceiver) + Q_PROPERTY(qint64 amount READ amount WRITE setAmount) + Q_PROPERTY(ObjectId amountType READ amountType WRITE setAmountType) + Q_PROPERTY(QString memo READ memo WRITE setMemo) + + graphene::chain::transfer_operation m_op; + QString m_memo; + +public: + Q_INVOKABLE qint64 fee() const { return m_op.fee.amount.value; } + Q_INVOKABLE void setFee(qint64 fee) { m_op.fee.amount = fee; } + + Q_INVOKABLE ObjectId feeType() const { return m_op.fee.asset_id.instance.value; } + Q_INVOKABLE void setFeeType(ObjectId feeType) { m_op.fee.asset_id = feeType; } + + Q_INVOKABLE ObjectId sender() const { return m_op.from.instance.value; } + Q_INVOKABLE void setSender(ObjectId sender) { m_op.from = sender; } + + Q_INVOKABLE ObjectId receiver() const { return m_op.to.instance.value; } + Q_INVOKABLE void setReceiver(ObjectId receiver) { m_op.to = receiver; } + + Q_INVOKABLE qint64 amount() const { return m_op.amount.amount.value; } + Q_INVOKABLE void setAmount(qint64 amount) { m_op.amount.amount = amount; } + + Q_INVOKABLE ObjectId amountType() const { return m_op.amount.asset_id.instance.value; } + Q_INVOKABLE void setAmountType(ObjectId assetType) { m_op.amount.asset_id = assetType; } + + /// 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. + Q_INVOKABLE QString memo() const { return m_memo; } + /// 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. + Q_INVOKABLE void setMemo(QString memo) { m_memo = memo; } + + const graphene::chain::transfer_operation& operation() const { return m_op; } + graphene::chain::transfer_operation& operation() { return m_op; } +}; +Q_DECLARE_METATYPE(TransferOperation) + +class OperationBuilder : public QObject { + Q_OBJECT + + ChainDataModel& model; + +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); +}; diff --git a/programs/light_client/main.cpp b/programs/light_client/main.cpp index e3aaa035..8da5be62 100644 --- a/programs/light_client/main.cpp +++ b/programs/light_client/main.cpp @@ -3,6 +3,7 @@ #include #include "ClientDataModel.hpp" +#include "Operations.hpp" int main(int argc, char *argv[]) { @@ -14,12 +15,15 @@ int main(int argc, char *argv[]) qRegisterMetaType>(); qRegisterMetaType(); + qRegisterMetaType(); qmlRegisterType("Graphene.Client", 0, 1, "Asset"); qmlRegisterType("Graphene.Client", 0, 1, "Balance"); qmlRegisterType("Graphene.Client", 0, 1, "Account"); qmlRegisterType("Graphene.Client", 0, 1, "DataModel"); qmlRegisterType("Graphene.Client", 0, 1, "GrapheneApplication"); + qmlRegisterUncreatableType("Graphene.Client", 0, 1, "OperationBuilder", + QStringLiteral("OperationBuilder cannot be created from QML")); QQmlApplicationEngine engine; QVariant crypto; diff --git a/programs/light_client/qml/AccountPicker.qml b/programs/light_client/qml/AccountPicker.qml index 01ecceec..f3dc4287 100644 --- a/programs/light_client/qml/AccountPicker.qml +++ b/programs/light_client/qml/AccountPicker.qml @@ -26,6 +26,8 @@ RowLayout { accountNameField.forceActiveFocus() } + signal balanceClicked(var balance) + Identicon { name: account && account.name == accountNameField.text? accountNameField.text : "" width: Scaling.cm(2) @@ -44,6 +46,7 @@ RowLayout { id: accountDetails width: parent.width height: text? implicitHeight : 0 + onLinkActivated: if (link === "balance") balanceClicked(balances[showBalance].amountReal()) Behavior on height { NumberAnimation{ easing.type: Easing.InOutQuad } } @@ -66,7 +69,8 @@ RowLayout { : account.id) if (showBalance >= 0) { var bal = balances[showBalance] - text += "\n" + qsTr("Balance: %1 %2").arg(String(bal.amountReal())).arg(bal.type.symbol) + text += "
" + qsTr("Balance: %1 %2").arg(String(bal.amountReal())) + .arg(bal.type.symbol) } return text }) diff --git a/programs/light_client/qml/TransferForm.qml b/programs/light_client/qml/TransferForm.qml index f72ff864..d3f9b416 100644 --- a/programs/light_client/qml/TransferForm.qml +++ b/programs/light_client/qml/TransferForm.qml @@ -21,6 +21,8 @@ Rectangle { property alias senderAccount: senderPicker.account /// The Account object for the receiver property alias receiverAccount: recipientPicker.account + /// The operation created in this form + property var operation Component.onCompleted: console.log("Made a transfer form") Component.onDestruction: console.log("Destroyed a transfer form") @@ -47,6 +49,7 @@ Rectangle { if (foundIndex >= 0) return foundIndex return balance.type.symbol === assetField.currentText? index : -1 }, -1) : -1 + onBalanceClicked: amountField.value = balance } AccountPicker { id: recipientPicker @@ -77,12 +80,24 @@ Rectangle { } ComboBox { id: assetField - Layout.minimumWidth: Scaling.cm(3) + 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: { + var balance = amountField.maxBalance + if (!balance || !balance.type) return "" + var precisionAdjustment = Math.pow(10, balance.type.precision) + + var op = app.operationBuilder.transfer(0, 0, amountField.value * precisionAdjustment, + balance.type.id, memoField.text, 0) + return qsTr("Fee:
") + op.fee / precisionAdjustment + " CORE" + } + } Item { Layout.fillWidth: true } Button { text: qsTr("Cancel")