adding asset fetching

This commit is contained in:
Daniel Larimer 2015-07-14 17:36:18 -04:00
commit b9f6ee4f2c
12 changed files with 256 additions and 92 deletions

View file

@ -174,10 +174,10 @@ void witness_plugin::block_production_loop()
_production_enabled = true;
// is anyone scheduled to produce now or one second in the future?
uint32_t slot = db.get_slot_at_time( graphene::time::now() + fc::seconds(1) );
const fc::time_point_sec now = graphene::time::now();
uint32_t slot = db.get_slot_at_time( now );
graphene::chain::witness_id_type scheduled_witness = db.get_scheduled_witness( slot ).first;
fc::time_point_sec scheduled_time = db.get_slot_time( slot );
fc::time_point_sec now = graphene::time::now();
graphene::chain::public_key_type scheduled_key = scheduled_witness( db ).signing_key;
auto is_scheduled = [&]()
@ -194,7 +194,7 @@ void witness_plugin::block_production_loop()
uint32_t prate = db.witness_participation_rate();
if( prate < _required_witness_participation )
{
elog("Not producing block because node appers to be on a minority fork with only ${x}% witness participation",
elog("Not producing block because node appears to be on a minority fork with only ${x}% witness participation",
("x",uint32_t(100*uint64_t(prate) / GRAPHENE_1_PERCENT) ) );
return false;
}
@ -212,21 +212,19 @@ void witness_plugin::block_production_loop()
return false;
}
// the local clock must be at least 1 second ahead of
// head_block_time.
if( (now - db.head_block_time()).to_seconds() <= 1 ) {
// the local clock must be at least 1 second ahead of head_block_time.
if( (now - db.head_block_time()).to_seconds() < GRAPHENE_MIN_BLOCK_INTERVAL ) {
elog("Not producing block because head block is less than a second old.");
return false;
}
// the local clock must be within 500 milliseconds of
// the scheduled production time.
if( llabs((scheduled_time - now).count()) > fc::milliseconds(250).count() ) {
if( llabs((scheduled_time - now).count()) > fc::milliseconds( 500 ).count() ) {
elog("Not producing block because network time is not within 250ms of scheduled block time.");
return false;
}
// we must know the private key corresponding to the witness's
// published block production key.
if( _private_keys.find( scheduled_key ) == _private_keys.end() ) {

View file

@ -16,6 +16,8 @@ file(GLOB QML qml/*)
qt5_add_resources(QML_QRC qml/qml.qrc)
add_executable(light_client ClientDataModel.cpp ClientDataModel.hpp main.cpp ${QML_QRC} ${QML})
add_dependencies(light_client gen_qrc)
if (CMAKE_VERSION VERSION_LESS 3.0)
add_dependencies(light_client gen_qrc)
endif()
target_link_libraries(light_client PRIVATE Qt5::Core Qt5::Widgets Qt5::Quick graphene_chain graphene_utilities fc graphene_app )

View file

@ -134,13 +134,14 @@ Asset* ChainDataModel::getAsset(QString symbol)
Account* ChainDataModel::getAccount(qint64 id)
Account* ChainDataModel::getAccount(ObjectId id)
{
auto& by_id_idx = m_accounts.get<::by_id>();
auto itr = by_id_idx.find(id);
if( itr == by_id_idx.end() )
{
auto tmp = new Account;
QQmlEngine::setObjectOwnership(tmp, QQmlEngine::CppOwnership);
tmp->id = id; --m_account_query_num;
tmp->name = QString::number( --m_account_query_num);
auto result = m_accounts.insert( tmp );
@ -173,7 +174,7 @@ Account* ChainDataModel::getAccount(qint64 id)
);
}
});
}
}
catch ( const fc::exception& e )
{
Q_EMIT exceptionThrown( QString::fromStdString(e.to_string()) );
@ -191,6 +192,7 @@ Account* ChainDataModel::getAccount(QString name)
if( itr == by_name_idx.end() )
{
auto tmp = new Account;
QQmlEngine::setObjectOwnership(tmp, QQmlEngine::CppOwnership);
tmp->id = --m_account_query_num;
tmp->name = name;
auto result = m_accounts.insert( tmp );
@ -218,12 +220,12 @@ Account* ChainDataModel::getAccount(QString name)
{
by_symbol_idx.modify( itr,
[=]( Account* a ){
a->setProperty("id", result.front()->id.instance() );
a->setProperty("id", ObjectId(result.front()->id.instance()));
}
);
}
});
}
}
catch ( const fc::exception& e )
{
Q_EMIT exceptionThrown( QString::fromStdString(e.to_string()) );
@ -278,6 +280,7 @@ void GrapheneApplication::start( QString apiurl, QString user, QString pass )
m_client = std::make_shared<fc::http::websocket_client>();
ilog( "connecting...${s}", ("s",apiurl.toStdString()) );
auto con = m_client->connect( apiurl.toStdString() );
m_connectionClosed = con->closed.connect([this]{queueExecute([this]{setIsConnected(false);});});
auto apic = std::make_shared<fc::rpc::websocket_api_connection>(*con);
auto remote_api = apic->get_remote_api< login_api >(1);
auto db_api = apic->get_remote_api< database_api >(0);

View file

@ -11,25 +11,38 @@
#include <fc/thread/thread.hpp>
#include <graphene/app/api.hpp>
#include <QtQml>
#include <QObject>
#include <QQmlListProperty>
using boost::multi_index_container;
using namespace boost::multi_index;
using ObjectId = qint64;
Q_DECLARE_METATYPE(ObjectId)
Q_DECLARE_METATYPE(std::function<void()>)
class GrapheneObject : public QObject
{
Q_OBJECT
Q_PROPERTY(qint64 id MEMBER id NOTIFY idChanged)
Q_PROPERTY(ObjectId id MEMBER id NOTIFY idChanged)
public:
qint64 id;
ObjectId id;
Q_SIGNALS:
void idChanged();
};
class Crypto {
Q_GADGET
public:
Q_INVOKABLE QString sha256(QByteArray data) {
return QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex();
}
};
QML_DECLARE_TYPE(Crypto)
class Asset : public GrapheneObject {
@ -77,7 +90,6 @@ class Account : public GrapheneObject {
public:
const QString& getName()const { return name; }
QQmlListProperty<Balance> balances();
QString name;
@ -90,19 +102,16 @@ struct by_account_name;
typedef multi_index_container<
Account*,
indexed_by<
hashed_unique< tag<by_id>, member<GrapheneObject, qint64, &GrapheneObject::id > >,
hashed_unique< tag<by_id>, member<GrapheneObject, ObjectId, &GrapheneObject::id > >,
ordered_unique< tag<by_account_name>, member<Account, QString, &Account::name> >
>
> account_multi_index_type;
class ChainDataModel : public QObject {
Q_OBJECT
public:
Q_INVOKABLE Account* getAccount(qint64 id);
Q_INVOKABLE Account* getAccount(ObjectId id);
Q_INVOKABLE Account* getAccount(QString name);
Q_INVOKABLE Asset* getAsset(qint64 id);
Q_INVOKABLE Asset* getAsset(QString symbol);
@ -121,15 +130,11 @@ private:
std::string m_api_url;
fc::api<graphene::app::database_api> m_db_api;
qint64 m_account_query_num = -1;
ObjectId m_account_query_num = -1;
account_multi_index_type m_accounts;
asset_multi_index_type m_assets;
};
class GrapheneApplication : public QObject {
Q_OBJECT
@ -141,6 +146,8 @@ class GrapheneApplication : public QObject {
ChainDataModel* m_model = nullptr;
bool m_isConnected = false;
boost::signals2::scoped_connection m_connectionClosed;
std::shared_ptr<fc::http::websocket_client> m_client;
fc::future<void> m_done;

View file

@ -0,0 +1,11 @@
== Graphene Client GUI ==
This is a Qt-based native GUI client for Graphene blockchains.
To build this GUI, run cmake with -DBUILD_QT_GUI=ON
This GUI depends on Qt 5.5 or later. If you do not have Qt 5.5 installed
in the canonical location on your OS (or if your OS does not have a
canonical location for libraries), you can specify the Qt path by running
cmake with -DCMAKE_PREFIX_PATH=/path/to/Qt/5.5/gcc_64 as appropriate for
your environment.

View file

@ -13,6 +13,7 @@ int main(int argc, char *argv[])
app.setOrganizationName("Cryptonomex, Inc.");
qRegisterMetaType<std::function<void()>>();
qRegisterMetaType<ObjectId>();
qmlRegisterType<Asset>("Graphene.Client", 0, 1, "Asset");
qmlRegisterType<Balance>("Graphene.Client", 0, 1, "Balance");
@ -21,11 +22,9 @@ int main(int argc, char *argv[])
qmlRegisterType<GrapheneApplication>("Graphene.Client", 0, 1, "GrapheneApplication");
QQmlApplicationEngine engine;
/*
QVariant crypto;
crypto.setValue(Crypto());
engine.rootContext()->setContextProperty("Crypto", crypto);
*/
#ifdef NDEBUG
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
#else

View file

@ -0,0 +1,72 @@
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 "."
RowLayout {
property Account account
property alias placeholderText: accountNameField.placeholderText
function setFocus() {
accountNameField.forceActiveFocus()
}
Identicon {
name: accountNameField.text
width: Scaling.cm(2)
height: Scaling.cm(2)
}
Column {
Layout.fillWidth: true
TextField {
id: accountNameField
width: parent.width
onEditingFinished: accountDetails.update(text)
}
Label {
id: accountDetails
function update(name) {
if (!name)
{
text = ""
return
}
account = app.model.getAccount(name)
if (account == null)
text = qsTr("Error fetching account.")
else
text = Qt.binding(function() {
if (account == null)
return qsTr("Account does not exist.")
return qsTr("Account ID: %1").arg(account.id < 0? qsTr("Loading...")
: account.id)
})
}
Behavior on text {
SequentialAnimation {
PropertyAnimation {
target: accountDetails
property: "opacity"
from: 1; to: 0
duration: 100
}
PropertyAction { target: accountDetails; property: "text" }
PropertyAnimation {
target: accountDetails
property: "opacity"
from: 0; to: 1
duration: 100
}
}
}
}
}
}

View file

@ -22,32 +22,49 @@ Rectangle {
function showForm(formType, params, closedCallback) {
if (formType.status === Component.Error)
console.log(formType.errorString())
if (!params instanceof Object)
params = {app: app}
else
params.app = app
formContainer.data = [formType.createObject(formContainer, params)]
var form = formType.createObject(formContainer, params)
formContainer.data = [form]
form.finished.connect(function(){state = "HIDDEN"})
if (closedCallback instanceof Function)
internal.callback = closedCallback
state = "SHOWN"
}
MouseArea {
id: mouseTrap
FocusScope {
id: scope
anchors.fill: parent
onClicked: {
mouse.accepted = true
greySheet.state = "HIDDEN"
// Do not let focus leave this scope while form is open
onFocusChanged: if (enabled && !focus) forceActiveFocus()
Keys.onEscapePressed: greySheet.state = "HIDDEN"
MouseArea {
id: mouseTrap
anchors.fill: parent
onClicked: {
mouse.accepted = true
greySheet.state = "HIDDEN"
}
acceptedButtons: Qt.AllButtons
}
MouseArea {
// This mouse area blocks clicks inside the form from reaching the mouseTrap
anchors.fill: formContainer
acceptedButtons: Qt.AllButtons
onClicked: mouse.accepted = true
}
Item {
id: formContainer
anchors.centerIn: parent
width: parent.width / 2
height: parent.height / 2
}
acceptedButtons: Qt.AllButtons
}
MouseArea {
anchors.fill: formContainer
acceptedButtons: Qt.AllButtons
onClicked: mouse.accepted = true
}
Item {
id: formContainer
anchors.centerIn: parent
width: parent.width / 2
height: parent.height / 2
}
states: [

View file

@ -0,0 +1,39 @@
import QtQuick 2.5
import "jdenticon/jdenticon-1.0.1.min.js" as Jdenticon
Canvas {
id: identicon
contextType: "2d"
property var name
onNameChanged: requestPaint()
onPaint: {
if (name)
Jdenticon.draw(identicon, name)
else {
var context = identicon.context
context.reset()
var draw_circle = function(context, x, y, radius) {
context.beginPath()
context.arc(x, y, radius, 0, 2 * Math.PI, false)
context.fillStyle = "rgba(0, 0, 0, 0.1)"
context.fill()
}
var size = Math.min(identicon.height, identicon.width)
var centerX = size / 2
var centerY = size / 2
var radius = size/15
draw_circle(context, centerX, centerY, radius)
draw_circle(context, 2*radius, 2*radius, radius)
draw_circle(context, centerX, 2*radius, radius)
draw_circle(context, size - 2*radius, 2*radius, radius)
draw_circle(context, size - 2*radius, centerY, radius)
draw_circle(context, size - 2*radius, size - 2*radius, radius)
draw_circle(context, centerX, size - 2*radius, radius)
draw_circle(context, 2*radius, size - 2*radius, radius)
draw_circle(context, 2*radius, centerY, radius)
}
}
}

View file

@ -3,57 +3,61 @@ import QtQuick.Controls 1.4
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.2
import Graphene.Client 0.1
import "."
import "jdenticon/jdenticon-1.0.1.min.js" as Jdenticon
Rectangle {
anchors.fill: parent
property alias senderAccount: senderPicker.account
property alias receiverAccount: recipientPicker.account
property GrapheneApplication app
signal finished
Component.onCompleted: console.log("Made a transfer form")
Component.onDestruction: console.log("Destroyed a transfer form")
Column {
ColumnLayout {
anchors.centerIn: parent
width: parent.width - Scaling.cm(2)
spacing: Scaling.cm(1)
AccountPicker {
id: senderPicker
width: parent.width
Component.onCompleted: setFocus()
placeholderText: qsTr("Sender")
}
AccountPicker {
id: recipientPicker
width: parent.width
placeholderText: qsTr("Recipient")
layoutDirection: Qt.RightToLeft
}
RowLayout {
Canvas {
id: identicon
width: Scaling.cm(2)
height: Scaling.cm(2)
contextType: "2d"
onPaint: {
if (nameField.text)
Jdenticon.draw(identicon, nameField.text)
else {
var context = identicon.context
context.reset()
var draw_circle = function(context, x, y, radius) {
context.beginPath()
context.arc(x, y, radius, 0, 2 * Math.PI, false)
context.fillStyle = "rgba(0, 0, 0, 0.1)"
context.fill()
}
var size = Math.min(identicon.height, identicon.width)
var centerX = size / 2
var centerY = size / 2
var radius = size/15
draw_circle(context, centerX, centerY, radius)
draw_circle(context, 2*radius, 2*radius, radius)
draw_circle(context, centerX, 2*radius, radius)
draw_circle(context, size - 2*radius, 2*radius, radius)
draw_circle(context, size - 2*radius, centerY, radius)
draw_circle(context, size - 2*radius, size - 2*radius, radius)
draw_circle(context, centerX, size - 2*radius, radius)
draw_circle(context, 2*radius, size - 2*radius, radius)
draw_circle(context, 2*radius, centerY, radius)
}
}
width: parent.width
SpinBox {
Layout.preferredWidth: Scaling.cm(4)
Layout.minimumWidth: Scaling.cm(1.5)
enabled: senderPicker.account
minimumValue: 0
maximumValue: Number.POSITIVE_INFINITY
}
TextField {
id: nameField
Layout.fillWidth: true
onTextChanged: identicon.requestPaint()
ComboBox {
Layout.minimumWidth: Scaling.cm(3)
enabled: senderPicker.account
model: ["CORE", "USD", "GOLD"]
}
Item { Layout.fillWidth: true }
Button {
text: qsTr("Cancel")
onClicked: finished()
}
Button {
text: qsTr("Transfer")
enabled: senderPicker.account
}
}
}

View file

@ -14,10 +14,6 @@ ApplicationWindow {
height: 480
title: qsTr("Hello World")
Component.onCompleted: {
app.start("ws://localhost:8090", "user", "pass")
}
menuBar: MenuBar {
Menu {
title: qsTr("File")
@ -28,12 +24,26 @@ ApplicationWindow {
MenuItem {
text: qsTr("Exit")
onTriggered: Qt.quit();
shortcut: "Ctrl+Q"
}
}
}
statusBar: StatusBar {
Label {
anchors.right: parent.right
text: app.isConnected? qsTr("Connected") : qsTr("Disconnected")
}
}
GrapheneApplication {
id: app
}
Timer {
running: !app.isConnected
interval: 5000
repeat: true
onTriggered: app.start("ws://localhost:8090", "user", "pass")
triggeredOnStart: true
}
Settings {
id: appSettings
@ -46,6 +56,8 @@ ApplicationWindow {
Column {
anchors.centerIn: parent
enabled: app.isConnected
Button {
text: "Transfer"
onClicked: formBox.showForm(Qt.createComponent("TransferForm.qml"), {},
@ -142,8 +154,6 @@ ApplicationWindow {
}
}
}
}
FormBox {

View file

@ -3,8 +3,10 @@
<file>main.qml</file>
<file>TransferForm.qml</file>
<file>FormBox.qml</file>
<file>jdenticon/jdenticon-1.0.1.min.js</file>
<file>Scaling.qml</file>
<file>Identicon.qml</file>
<file>AccountPicker.qml</file>
<file>jdenticon/jdenticon-1.0.1.min.js</file>
</qresource>
</RCC>