[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,
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];

View file

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

View file

@ -2,6 +2,7 @@
#include "ChainDataModel.hpp"
#include "Wallet.hpp"
#include "Operations.hpp"
#include "Transaction.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
{
func();

View file

@ -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();

View file

@ -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<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);
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<graphene::chain::transfer_operation>();
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();
}

View file

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

View file

@ -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();

View file

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

View file

@ -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<std::function<void()>>();
qRegisterMetaType<ObjectId>();
qRegisterMetaType<QList<OperationBase*>>();
qmlRegisterType<Asset>("Graphene.Client", 0, 1, "Asset");
qmlRegisterType<Balance>("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

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})
if (closedCallback instanceof Function)
internal.callback = closedCallback
// Notify the form that it's about to go live
form.display({})
state = "SHOWN"
}

View file

@ -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 {

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.
*/
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:<br/>") + 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:<br/>") + 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()])
}
}
}

View file

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

View file

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