[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
This commit is contained in:
Nathan Hourt 2015-07-27 16:01:16 -04:00
parent 6d2b1a3648
commit 5d7ae4e6a8
16 changed files with 227 additions and 129 deletions

View file

@ -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, void memo_data::set_message(const fc::ecc::private_key& priv, const fc::ecc::public_key& pub,
const string& msg, uint64_t custom_nonce) 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 ) if( custom_nonce == 0 )
{ {
uint64_t entropy = fc::sha224::hash(fc::ecc::private_key::generate())._hash[0]; uint64_t entropy = fc::sha224::hash(fc::ecc::private_key::generate())._hash[0];

View file

@ -51,7 +51,6 @@ public:
void update(const account_balance_object& balance); void update(const account_balance_object& balance);
/** /**
* Anything greater than 1.0 means full authority. * Anything greater than 1.0 means full authority.
* Anything between (0 and 1.0) means partial authority * Anything between (0 and 1.0) means partial authority

View file

@ -2,6 +2,7 @@
#include "ChainDataModel.hpp" #include "ChainDataModel.hpp"
#include "Wallet.hpp" #include "Wallet.hpp"
#include "Operations.hpp" #include "Operations.hpp"
#include "Transaction.hpp"
#include <graphene/app/api.hpp> #include <graphene/app/api.hpp>
@ -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<void()>& func)const Q_SLOT void GrapheneApplication::execute(const std::function<void()>& func)const
{ {
func(); func();

View file

@ -14,6 +14,8 @@ class websocket_client;
class ChainDataModel; class ChainDataModel;
class OperationBuilder; class OperationBuilder;
class OperationBase;
class Transaction;
class Wallet; class Wallet;
class GrapheneApplication : public QObject { class GrapheneApplication : public QObject {
Q_OBJECT Q_OBJECT
@ -62,6 +64,9 @@ public:
return m_isConnected; 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: Q_SIGNALS:
void exceptionThrown(QString message); void exceptionThrown(QString message);
void loginFailed(); void loginFailed();

View file

@ -5,8 +5,7 @@
TransferOperation* OperationBuilder::transfer(ObjectId sender, ObjectId receiver, qint64 amount, TransferOperation* OperationBuilder::transfer(ObjectId sender, ObjectId receiver, qint64 amount,
ObjectId amountType, QString memo, ObjectId feeType) ObjectId amountType, QString memo, ObjectId feeType)
{ {
static fc::ecc::private_key dummyPrivate = fc::ecc::private_key::generate(); try {
static fc::ecc::public_key dummyPublic = fc::ecc::private_key::generate().get_public_key();
TransferOperation* op = new TransferOperation; TransferOperation* op = new TransferOperation;
op->setSender(sender); op->setSender(sender);
op->setReceiver(receiver); op->setReceiver(receiver);
@ -15,8 +14,30 @@ TransferOperation* OperationBuilder::transfer(ObjectId sender, ObjectId receiver
op->setMemo(memo); op->setMemo(memo);
op->setFeeType(feeType); op->setFeeType(feeType);
auto feeParameters = model.global_properties().parameters.current_fees->get<graphene::chain::transfer_operation>(); auto feeParameters = model.global_properties().parameters.current_fees->get<graphene::chain::transfer_operation>();
op->operation().memo = graphene::chain::memo_data();
op->operation().memo->set_message(dummyPrivate, dummyPublic, memo.toStdString());
op->setFee(op->operation().calculate_fee(feeParameters).value); op->setFee(op->operation().calculate_fee(feeParameters).value);
return op; 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();
} }

View file

@ -37,7 +37,6 @@ class TransferOperation : public OperationBase {
Q_PROPERTY(QString memo READ memo WRITE setMemo NOTIFY memoChanged) Q_PROPERTY(QString memo READ memo WRITE setMemo NOTIFY memoChanged)
graphene::chain::transfer_operation m_op; graphene::chain::transfer_operation m_op;
QString m_memo;
public: public:
TransferOperation(){} TransferOperation(){}
@ -57,9 +56,9 @@ public:
ObjectId receiver() const { return m_op.to.instance.value; } ObjectId receiver() const { return m_op.to.instance.value; }
qint64 amount() const { return m_op.amount.amount.value; } qint64 amount() const { return m_op.amount.amount.value; }
ObjectId amountType() const { return m_op.amount.asset_id.instance.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 /// This does not deal with encrypted memos. The memo stored here is unencrypted. The encryption step must be
/// underlying graphene operation. The encryption and storage steps must be handled elsewhere. /// performed elsewhere.
QString memo() const { return m_memo; } QString memo() const;
const graphene::chain::transfer_operation& operation() const { return m_op; } const graphene::chain::transfer_operation& operation() const { return m_op; }
graphene::chain::transfer_operation& operation() { return m_op; } graphene::chain::transfer_operation& operation() { return m_op; }
@ -101,14 +100,9 @@ public Q_SLOTS:
m_op.amount.asset_id = arg; m_op.amount.asset_id = arg;
Q_EMIT amountTypeChanged(); Q_EMIT amountTypeChanged();
} }
/// This does not deal with encrypted memos. The memo stored here is unencrypted, and does not get stored in the /// This does not deal with encrypted memos. The memo stored here is unencrypted. The encryption step must be
/// underlying graphene operation. The encryption and storage steps must be handled elsewhere. /// performed elsewhere.
void setMemo(QString memo) { void setMemo(QString memo);
if (memo == m_memo)
return;
m_memo = memo;
Q_EMIT memoChanged();
}
Q_SIGNALS: Q_SIGNALS:
void feeChanged(); void feeChanged();
@ -136,7 +130,7 @@ public:
OperationBuilder(ChainDataModel& model, QObject* parent = nullptr) OperationBuilder(ChainDataModel& model, QObject* parent = nullptr)
: QObject(parent), model(model){} : QObject(parent), model(model){}
Q_INVOKABLE TransferOperation* transfer(ObjectId sender, ObjectId receiver, Q_INVOKABLE TransferOperation* transfer(ObjectId sender, ObjectId receiver, qint64 amount,
qint64 amount, ObjectId amountType, QString memo, ObjectId feeType); ObjectId amountType, QString memo, ObjectId feeType);
}; };

View file

@ -43,6 +43,11 @@ OperationBase* Transaction::operationAt(int index) const {
void Transaction::appendOperation(OperationBase* op) void Transaction::appendOperation(OperationBase* op)
{ {
if (op == nullptr)
{
qWarning("Unable to append null operation to transaction");
return;
}
op->setParent(this); op->setParent(this);
m_transaction.operations.push_back(op->genericOperation()); m_transaction.operations.push_back(op->genericOperation());
Q_EMIT operationsChanged(); Q_EMIT operationsChanged();

View file

@ -50,6 +50,6 @@ private:
Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged) Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged)
Q_PROPERTY(QQmlListProperty<OperationBase> operations READ operations NOTIFY operationsChanged) Q_PROPERTY(QQmlListProperty<OperationBase> operations READ operations NOTIFY operationsChanged)
Status m_status; Status m_status = Unbroadcasted;
graphene::chain::transaction m_transaction; graphene::chain::transaction m_transaction;
}; };

View file

@ -20,6 +20,9 @@ QML_DECLARE_TYPE(Crypto)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
#ifndef NDEBUG
QQmlDebuggingEnabler enabler;
#endif
fc::thread::current().set_name("main"); fc::thread::current().set_name("main");
QApplication app(argc, argv); QApplication app(argc, argv);
app.setApplicationName("Graphene Client"); app.setApplicationName("Graphene Client");
@ -28,6 +31,7 @@ int main(int argc, char *argv[])
qRegisterMetaType<std::function<void()>>(); qRegisterMetaType<std::function<void()>>();
qRegisterMetaType<ObjectId>(); qRegisterMetaType<ObjectId>();
qRegisterMetaType<QList<OperationBase*>>();
qmlRegisterType<Asset>("Graphene.Client", 0, 1, "Asset"); qmlRegisterType<Asset>("Graphene.Client", 0, 1, "Asset");
qmlRegisterType<Balance>("Graphene.Client", 0, 1, "Balance"); qmlRegisterType<Balance>("Graphene.Client", 0, 1, "Balance");
@ -49,7 +53,6 @@ int main(int argc, char *argv[])
#ifdef NDEBUG #ifdef NDEBUG
engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
#else #else
QQmlDebuggingEnabler enabler;
engine.load(QUrl(QStringLiteral("qml/main.qml"))); engine.load(QUrl(QStringLiteral("qml/main.qml")));
#endif #endif

View file

@ -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)
}
}

View file

@ -33,6 +33,8 @@ Rectangle {
form.completed.connect(function(){state = "HIDDEN"; internal.callbackArgs = arguments}) form.completed.connect(function(){state = "HIDDEN"; internal.callbackArgs = arguments})
if (closedCallback instanceof Function) if (closedCallback instanceof Function)
internal.callback = closedCallback internal.callback = closedCallback
// Notify the form that it's about to go live
form.display({})
state = "SHOWN" state = "SHOWN"
} }

View file

@ -8,9 +8,10 @@ Flipable {
property Component frontComponent property Component frontComponent
property Component backComponent property Component backComponent
property GrapheneApplication app
signal canceled signal display(var arg)
signal completed signal canceled(var arg)
signal completed(var arg)
property bool flipped: false property bool flipped: false
@ -19,13 +20,11 @@ Flipable {
front = frontComponent.createObject(flipable, {app: app, enabled: Qt.binding(function(){return !flipped})}) front = frontComponent.createObject(flipable, {app: app, enabled: Qt.binding(function(){return !flipped})})
front.canceled.connect(function() { canceled.apply(this, arguments) }) front.canceled.connect(function() { canceled.apply(this, arguments) })
front.completed.connect(function() { front.completed.connect(function() {
if (back.hasOwnProperty("arguments")) back.display.apply(this, arguments)
back.arguments = arguments
flipped = true flipped = true
}) })
back.canceled.connect(function() { back.canceled.connect(function() {
if (front.hasOwnProperty("arguments")) front.display.apply(this, arguments)
front.arguments = arguments
flipped = false flipped = false
}) })
back.completed.connect(function() { completed.apply(this, arguments) }) back.completed.connect(function() { completed.apply(this, arguments) })
@ -35,8 +34,10 @@ Flipable {
id: rotation id: rotation
origin.x: flipable.width/2 origin.x: flipable.width/2
origin.y: flipable.height/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 // set axis.y to 1 to rotate around y-axis
angle: 0 // the default angle axis.x: 0; axis.y: 1; axis.z: 0
// the default angle
angle: 0
} }
states: State { states: State {

View file

@ -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))
}
}

View file

@ -10,13 +10,8 @@ import "."
/** /**
* This is the form for transferring some amount of asset from one account to another. * This is the form for transferring some amount of asset from one account to another.
*/ */
Rectangle { FormBase {
id: root id: base
anchors.fill: parent
property GrapheneApplication app
signal canceled
signal completed(TransferOperation op)
/// The Account object for the sender /// The Account object for the sender
property alias senderAccount: senderPicker.account property alias senderAccount: senderPicker.account
@ -34,11 +29,6 @@ Rectangle {
Component.onCompleted: console.log("Made a transfer form") Component.onCompleted: console.log("Made a transfer form")
Component.onDestruction: console.log("Destroyed 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 { AccountPicker {
id: senderPicker id: senderPicker
// The senderPicker is really the heart of the form. Everything else in the form adjusts based on the account // The senderPicker is really the heart of the form. Everything else in the form adjusts based on the account
@ -47,7 +37,7 @@ Rectangle {
// to have a maximum value equal to the account's balance in that asset. The transfer button enables only when // 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. // both accounts are set, and a nonzero amount is selected to be transferred.
app: root.app app: base.app
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: Scaling.cm(5) Layout.minimumWidth: Scaling.cm(5)
Component.onCompleted: setFocus() Component.onCompleted: setFocus()
@ -60,7 +50,7 @@ Rectangle {
} }
AccountPicker { AccountPicker {
id: recipientPicker id: recipientPicker
app: root.app app: base.app
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: Scaling.cm(5) Layout.minimumWidth: Scaling.cm(5)
placeholderText: qsTr("Recipient") placeholderText: qsTr("Recipient")
@ -116,8 +106,7 @@ Rectangle {
id: transferButton id: transferButton
text: qsTr("Transfer") text: qsTr("Transfer")
enabled: senderPicker.account && recipientPicker.account && senderPicker.account !== recipientPicker.account && amountField.value enabled: senderPicker.account && recipientPicker.account && senderPicker.account !== recipientPicker.account && amountField.value
onClicked: completed(operation) onClicked: completed([operation()])
}
} }
} }
} }

View file

@ -63,7 +63,7 @@ ApplicationWindow {
onClicked: { onClicked: {
var front = Qt.createComponent("TransferForm.qml") var front = Qt.createComponent("TransferForm.qml")
// TODO: make back into a preview and confirm dialog // 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}, formBox.showForm(Qt.createComponent("FormFlipper.qml"), {frontComponent: front, backComponent: back},
function() { function() {
console.log("Closed form") console.log("Closed form")

View file

@ -1,7 +1,9 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<file>main.qml</file> <file>main.qml</file>
<file>FormBase.qml</file>
<file>TransferForm.qml</file> <file>TransferForm.qml</file>
<file>TransactionConfirmationForm.qml</file>
<file>FormBox.qml</file> <file>FormBox.qml</file>
<file>FormFlipper.qml</file> <file>FormFlipper.qml</file>
<file>Scaling.qml</file> <file>Scaling.qml</file>