diff --git a/.travis/lint_04_install.sh b/.travis/lint_04_install.sh
index 9a22773e57..b09f54336d 100755
--- a/.travis/lint_04_install.sh
+++ b/.travis/lint_04_install.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
-# Copyright (c) 2018 The Bitcoin Core developers
+# Copyright (c) 2018-2019 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
@@ -10,6 +10,6 @@ travis_retry pip install codespell==1.13.0
travis_retry pip install flake8==3.5.0
travis_retry pip install vulture==0.29
-SHELLCHECK_VERSION=v0.6.0
-curl -s "https://storage.googleapis.com/shellcheck/shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" | tar --xz -xf - --directory /tmp/
-export PATH="/tmp/shellcheck-${SHELLCHECK_VERSION}:${PATH}"
+SHELLCHECK_VERSION=v0.7.1
+curl -sL "https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" | tar --xz -xf - --directory /tmp/
+export PATH="/tmp/shellcheck-${SHELLCHECK_VERSION}:${PATH}"
\ No newline at end of file
diff --git a/configure.ac b/configure.ac
index a541b52c05..87a5938443 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N)
AC_PREREQ([2.60])
define(_CLIENT_VERSION_MAJOR, 4)
define(_CLIENT_VERSION_MINOR, 3)
-define(_CLIENT_VERSION_REVISION, 0)
+define(_CLIENT_VERSION_REVISION, 1)
define(_CLIENT_VERSION_BUILD, 0)
define(_CLIENT_VERSION_RC, 0)
define(_CLIENT_VERSION_IS_RELEASE, true)
diff --git a/doc/release-notes/release-notes-4.3.1.md b/doc/release-notes/release-notes-4.3.1.md
new file mode 100644
index 0000000000..813229b0ab
--- /dev/null
+++ b/doc/release-notes/release-notes-4.3.1.md
@@ -0,0 +1,48 @@
+# Blocknet Comet 4.3.1 Release Notes
+
+Notable changes
+================
+
+- Improved partial orders support
+- dxPartialOrderChainDetails rpc call
+- dxGetMyPartialOrderChain rpc call
+- Fixed order cancellation issues
+- Coin Control tree view
+- Coin Control keyboard navigation added
+- New Balances screen
+- Address book improvements
+- Dashboard transaction list fixes
+- Support lowercase rpc calls
+
+Changelog
+================
+
+- 6af51a592 4.3.1 release
+- b6d2e5ceb [xbridge] improved order amount checks
+- 9b5b49446 [xbridge] version bump 55
+- 974e9d638 [xbridge] partial orders drift check on amounts
+- 6804d82bf [xbridge] fix dxPartialOrderChainDetails
+- dad60112f [xbridge] use ints in partial order price checks
+- 21fd33a0b [xbridge] use correct conversion to xbridge float
+- f3169f0e5 Update partial order data help msgs
+- 96cf3b6a8 [xbridge] fix partial orders utxo selector
+- d7f31014f [xbridge] log partial prep tx
+- 4918db724 [core] support case insensitive rpc calls
+- 73d7977d6 [xbridge] partial order improvements
+- 6d6015978 [core] shellcheck linter update to v0.7.1
+- e7199f8cb [gui] fix dashboard table issues
+- e4de38e61 [gui] fix duplicate addresses on create
+- 651129099 [gui] addressbook create new address title
+- 753eb1b07 [gui] show proper address entry on double-click
+- d3c636a0a [gui] fix rescan after privkey import
+- a7a2e2509 [gui] add blocknet balances page; improve addressbook table
+- 38928f64d [gui] display warning on bad pubkey import; fix private key import
+- 726873367 [gui] improved coin control tree selection states; context menu fixes
+- 33e3d6f84 [xbridge] fix order cancel when order is not mine
+- 093003a47 [gui] set table focus on mode switch
+- feb31f047 [gui] refactor coin control state mgmt
+- fdec95670 [gui] add coin control keyboard navigation
+- 4d4b85186 [core] adjust timing of client loading progress
+- 768910060 [gui] fix disappearing address label
+- 43a417b8d [gui] added coin control tree view to redesign
+- 0e5953e7d 4.3.1
diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include
index f811207576..97aa066985 100644
--- a/src/Makefile.qt.include
+++ b/src/Makefile.qt.include
@@ -170,6 +170,7 @@ QT_MOC_CPP += \
if ENABLE_WALLET
QT_MOC_CPP += \
qt/moc_blocknetactionbtn.cpp \
+ qt/moc_blocknetaccounts.cpp \
qt/moc_blocknetaddressbook.cpp \
qt/moc_blocknetaddressbtn.cpp \
qt/moc_blocknetaddressedit.cpp \
@@ -313,6 +314,7 @@ BITCOIN_QT_H = \
# Blocknet Qt files
BITCOIN_QT_H += \
qt/blocknetactionbtn.h \
+ qt/blocknetaccounts.h \
qt/blocknetaddressbook.h \
qt/blocknetaddressbtn.h \
qt/blocknetaddressedit.h \
@@ -458,6 +460,8 @@ RES_ICONS += \
qt/res/redesign/icons/block-icon.png \
qt/res/redesign/icons/checkmark-off.png \
qt/res/redesign/icons/checkmark-on.png \
+ qt/res/redesign/icons/checkmark-partial.png \
+ qt/res/redesign/icons/lock_closed_white.png \
qt/res/redesign/icons/NavBarIcons/Active/AddressBookIcon.png \
qt/res/redesign/icons/NavBarIcons/Active/AnnouncementsIcon.png \
qt/res/redesign/icons/NavBarIcons/Active/DashboardIcon.png \
@@ -552,6 +556,7 @@ BITCOIN_QT_WALLET_CPP = \
# Blocknet wallet files
BITCOIN_QT_WALLET_CPP += \
qt/blocknetactionbtn.cpp \
+ qt/blocknetaccounts.cpp \
qt/blocknetaddressbook.cpp \
qt/blocknetaddressbtn.cpp \
qt/blocknetaddressedit.cpp \
diff --git a/src/Makefile.test.include b/src/Makefile.test.include
index c7f41b78d8..942eaa907f 100644
--- a/src/Makefile.test.include
+++ b/src/Makefile.test.include
@@ -136,6 +136,10 @@ BITCOIN_TESTS += \
BITCOIN_TEST_SUITE += \
test/xrouter_tests.h
+# Blocknet XBridge
+BITCOIN_TESTS += \
+ test/xbridge_tests.cpp
+
if ENABLE_PROPERTY_TESTS
BITCOIN_TESTS += \
test/key_properties.cpp
diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc
index 56de721013..c4a695eb95 100644
--- a/src/qt/bitcoin.qrc
+++ b/src/qt/bitcoin.qrc
@@ -128,6 +128,8 @@
res/redesign/icons/Table/HeaderViewArrowDown.png
res/redesign/icons/checkmark-on.png
res/redesign/icons/checkmark-off.png
+ res/redesign/icons/checkmark-partial.png
+ res/redesign/icons/lock_closed_white.png
res/redesign/icons/block-icon.png
res/redesign/fonts/Roboto/Roboto-Black.ttf
res/redesign/fonts/Roboto/Roboto-BlackItalic.ttf
diff --git a/src/qt/blocknetaccounts.cpp b/src/qt/blocknetaccounts.cpp
new file mode 100644
index 0000000000..5b30748f0d
--- /dev/null
+++ b/src/qt/blocknetaccounts.cpp
@@ -0,0 +1,350 @@
+// Copyright (c) 2020 The Blocknet developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+
+BlocknetAccounts::BlocknetAccounts(QWidget *parent) : QFrame(parent), layout(new QVBoxLayout) {
+ this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ layout->setContentsMargins(BGU::spi(46), BGU::spi(10), BGU::spi(50), 0);
+ this->setLayout(layout);
+
+ titleLbl = new QLabel(tr("Balances"));
+ titleLbl->setObjectName("h4");
+ titleLbl->setFixedHeight(BGU::spi(26));
+
+ auto *topBox = new QFrame;
+ topBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding);
+ auto *topBoxLayout = new QHBoxLayout;
+ topBoxLayout->setContentsMargins(QMargins());
+ topBoxLayout->setSizeConstraint(QLayout::SetMaximumSize);
+ topBox->setLayout(topBoxLayout);
+
+ auto *addAddressBtn = new BlocknetIconBtn(":/redesign/QuickActions/AddressButtonIcon.png");
+
+ addButtonLbl = new QLabel(tr("Create New Address"));
+ addButtonLbl->setObjectName("h4");
+
+ table = new QTableWidget;
+ table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ table->setContentsMargins(QMargins());
+ table->setColumnCount(COLUMN_EDIT + 1);
+ table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ table->setSelectionMode(QAbstractItemView::SingleSelection);
+ table->setFocusPolicy(Qt::NoFocus);
+ table->setAlternatingRowColors(true);
+ table->setColumnWidth(COLUMN_PADDING1, BGU::spi(5));
+ table->setColumnWidth(COLUMN_PADDING2, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING3, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING4, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING5, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING6, BGU::spi(1));
+ table->setColumnWidth(COLUMN_AVATAR, BGU::spi(50));
+ table->setColumnWidth(COLUMN_COPY, BGU::spi(65));
+ table->setColumnWidth(COLUMN_EDIT, BGU::spi(65));
+ table->setShowGrid(false);
+ table->setFocusPolicy(Qt::NoFocus);
+ table->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ table->setContextMenuPolicy(Qt::CustomContextMenu);
+ table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
+ table->verticalHeader()->setDefaultSectionSize(BGU::spi(58));
+ table->verticalHeader()->setVisible(false);
+ table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
+ table->horizontalHeader()->setSortIndicatorShown(true);
+ table->horizontalHeader()->setSectionsClickable(true);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING1, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING2, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING3, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING4, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING5, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING6, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_AVATAR, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_ALIAS, QHeaderView::ResizeToContents);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_ADDRESS, QHeaderView::Stretch);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_BALANCE, QHeaderView::ResizeToContents);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_INPUTS, QHeaderView::ResizeToContents);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_COPY, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_EDIT, QHeaderView::Fixed);
+ table->setHorizontalHeaderLabels({ "", "", "", tr("Alias"), "", tr("Address"), "", tr("Balance"), "", tr("Inputs"), "", "", "" });
+
+ topBoxLayout->addWidget(addAddressBtn, 0, Qt::AlignLeft);
+ topBoxLayout->addWidget(addButtonLbl, 0, Qt::AlignLeft | Qt::AlignVCenter);
+ topBoxLayout->addStretch(1);
+
+ layout->addWidget(titleLbl);
+ layout->addSpacing(BGU::spi(10));
+ layout->addWidget(topBox);
+ layout->addSpacing(BGU::spi(15));
+ layout->addWidget(table, 1);
+ layout->addSpacing(BGU::spi(20));
+
+ connect(addAddressBtn, &BlocknetIconBtn::clicked, this, &BlocknetAccounts::onAddAddress);
+ connect(table, &QTableWidget::cellDoubleClicked, this, &BlocknetAccounts::onDoubleClick);
+ connect(table->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this, [this](int column, Qt::SortOrder order) {
+ QSettings settings;
+ if (column <= COLUMN_PADDING2 || column == COLUMN_PADDING3 || column == COLUMN_PADDING4
+ || column == COLUMN_PADDING5 || column == COLUMN_PADDING6) {
+ table->horizontalHeader()->setSortIndicator(settings.value("blocknetAccountsSortColumn").toInt(),
+ static_cast(settings.value("blocknetAccountsSortOrder").toInt()));
+ return;
+ }
+ settings.setValue("blocknetAccountsSortOrder", static_cast(order));
+ settings.setValue("blocknetAccountsSortColumn", column);
+ });
+}
+
+void BlocknetAccounts::setWalletModel(WalletModel *w) {
+ if (walletModel == w)
+ return;
+
+ walletModel = w;
+ if (!walletModel || !walletModel->getOptionsModel())
+ return;
+
+ initialize();
+
+ connect(walletModel->getAddressTableModel(), &AddressTableModel::rowsInserted, this,
+ [this](const QModelIndex &, int, int) {
+ initialize();
+ });
+ connect(walletModel->getAddressTableModel(), &AddressTableModel::rowsRemoved, this,
+ [this](const QModelIndex &, int, int) {
+ initialize();
+ });
+ connect(walletModel->getAddressTableModel(), &AddressTableModel::dataChanged, this,
+ [this](const QModelIndex &, const QModelIndex &, const QVector &) {
+ initialize();
+ });
+}
+
+void BlocknetAccounts::initialize() {
+ if (!walletModel)
+ return;
+
+ dataModel.clear();
+
+ AddressTableModel *addressTableModel = walletModel->getAddressTableModel();
+ int rowCount = addressTableModel->rowCount(QModelIndex());
+
+ std::set addresses;
+ for (int row = 0; row < rowCount; ++row) {
+ auto index = addressTableModel->index(row, 0, QModelIndex());
+ auto *rec = static_cast(index.internalPointer());
+ if (!rec)
+ continue;
+ QString alias = rec->label;
+ QString address = rec->address;
+ int type = rec->type;
+ BlocknetAddressBook::Address a = {
+ alias,
+ address,
+ type,
+ };
+ addresses.insert(a);
+ }
+
+ // Tally up balances for each address
+ auto coins = walletModel->wallet().listCoins();
+ for (auto & item : coins) {
+ const auto addr = EncodeDestination(item.first);
+ auto temp = BlocknetAddressBook::Address{ "", QString::fromStdString(addr), AddressTableEntry::Type::Receiving };
+ auto it = addresses.find(temp);
+ if (it != addresses.end()) {
+ Account account;
+ account.alias = it->alias;
+ account.type = it->type;
+ account.address = QString::fromStdString(EncodeDestination(item.first));
+ account.inputs = item.second.size();
+ for (const auto & tup : item.second) {
+ auto tx = std::get<1>(tup);
+ account.balance += tx.txout.nValue;
+ }
+ dataModel << account;
+ }
+ }
+
+ this->setData(dataModel);
+}
+
+void BlocknetAccounts::unwatch() {
+ table->setEnabled(false);
+}
+
+void BlocknetAccounts::watch() {
+ table->setEnabled(true);
+}
+
+void BlocknetAccounts::setData(const QVector &data) {
+ this->filteredData = data;
+
+ unwatch();
+ table->clearContents();
+ table->setRowCount(this->filteredData.count());
+ table->setSortingEnabled(false);
+
+ for (int i = 0; i < this->filteredData.count(); ++i) {
+ auto &d = this->filteredData[i];
+
+ // avatar
+ auto *avatarItem = new QTableWidgetItem;
+ auto *avatarWidget = new QWidget();
+ avatarWidget->setContentsMargins(QMargins());
+ auto *avatarLayout = new QVBoxLayout;
+ avatarLayout->setContentsMargins(QMargins());
+ avatarLayout->setSpacing(0);
+ avatarWidget->setLayout(avatarLayout);
+
+ auto *avatar = d.type == AddressTableEntry::Sending ? new BlocknetAvatar(d.address)
+ : new BlocknetAvatarBlue(d.address);
+ avatarLayout->addWidget(avatar, 0, Qt::AlignCenter);
+ table->setCellWidget(i, COLUMN_AVATAR, avatarWidget);
+ table->setItem(i, COLUMN_AVATAR, avatarItem);
+
+ // alias
+ auto *aliasItem = new LabelItem;
+ aliasItem->label = d.alias.toStdString();
+ aliasItem->setData(Qt::DisplayRole, d.alias);
+ table->setItem(i, COLUMN_ALIAS, aliasItem);
+
+ // address
+ auto *addressItem = new QTableWidgetItem;
+ addressItem->setData(Qt::DisplayRole, d.address);
+ table->setItem(i, COLUMN_ADDRESS, addressItem);
+
+ // balance
+ auto *balanceItem = new NumberItem;
+ balanceItem->amount = d.balance;
+ balanceItem->setData(Qt::DisplayRole, BitcoinUnits::formatWithUnit(walletModel->getOptionsModel()->getDisplayUnit(), d.balance));
+ table->setItem(i, COLUMN_BALANCE, balanceItem);
+
+ // inputs
+ auto *inputsItem = new NumberItem;
+ inputsItem->amount = d.inputs;
+ inputsItem->setData(Qt::DisplayRole, d.inputs);
+ table->setItem(i, COLUMN_INPUTS, inputsItem);
+
+ // copy item
+ auto *copyItem = new QTableWidgetItem;
+ copyItem->setToolTip(tr("Copy address"));
+ auto *copyWidget = new QWidget();
+ copyWidget->setContentsMargins(QMargins());
+ auto *copyLayout = new QVBoxLayout;
+ copyLayout->setContentsMargins(QMargins());
+ copyLayout->setSpacing(0);
+ copyWidget->setLayout(copyLayout);
+
+ auto *copyButton = new BlocknetLabelBtn;
+ copyButton->setText(tr("Copy"));
+ copyButton->setID(d.address);
+ copyLayout->addWidget(copyButton, 0, Qt::AlignLeft);
+ copyLayout->addSpacing(BGU::spi(6));
+ connect(copyButton, &BlocknetLabelBtn::clicked, this, &BlocknetAccounts::onCopyAddress);
+
+ table->setCellWidget(i, COLUMN_COPY, copyWidget);
+ table->setItem(i, COLUMN_COPY, copyItem);
+
+ // edit item
+ auto *editItem = new QTableWidgetItem;
+ editItem->setToolTip(tr("Edit address alias"));
+ auto *editWidget = new QWidget();
+ editWidget->setContentsMargins(QMargins());
+ auto *editLayout = new QVBoxLayout;
+ editLayout->setContentsMargins(QMargins());
+ editLayout->setSpacing(0);
+ editWidget->setLayout(editLayout);
+
+ auto *editButton = new BlocknetLabelBtn;
+ editButton->setText(tr("Edit"));
+ editButton->setID(d.address);
+ editLayout->addWidget(editButton, 0, Qt::AlignLeft);
+ editLayout->addSpacing(BGU::spi(6));
+ connect(editButton, &BlocknetLabelBtn::clicked, this, &BlocknetAccounts::onEditAddress);
+
+ table->setCellWidget(i, COLUMN_EDIT, editWidget);
+ table->setItem(i, COLUMN_EDIT, editItem);
+ }
+
+ table->setSortingEnabled(true);
+ QSettings settings;
+ if (settings.contains("blocknetAccountsSortColumn") && settings.contains("blocknetAccountsSortOrder")) {
+ const auto col = settings.value("blocknetAccountsSortColumn").toInt();
+ const auto order = static_cast(settings.value("blocknetAccountsSortOrder").toInt());
+ table->horizontalHeader()->setSortIndicator(col, order);
+ } else {
+ table->horizontalHeader()->setSortIndicator(COLUMN_BALANCE, Qt::DescendingOrder);
+ settings.setValue("blocknetAccountsSortColumn", COLUMN_BALANCE);
+ settings.setValue("blocknetAccountsSortOrder", static_cast(Qt::DescendingOrder));
+ }
+ watch();
+}
+
+void BlocknetAccounts::onCopyAddress() {
+ auto *btn = qobject_cast(sender());
+ auto address = btn->getID();
+ GUIUtil::setClipboard(address);
+}
+
+void BlocknetAccounts::onAddAddress() {
+ BlocknetAddressAddDialog dlg(walletModel->getAddressTableModel(), walletModel, Qt::WindowSystemMenuHint | Qt::WindowTitleHint);
+ dlg.setStyleSheet(GUIUtil::loadStyleSheet());
+ connect(&dlg, &BlocknetAddressAddDialog::rescan, [this](std::string walletName) {
+ QTimer::singleShot(1000, [this,walletName]() {
+ Q_EMIT rescan(walletName);
+ });
+ });
+ dlg.exec();
+}
+
+void BlocknetAccounts::onEditAddress() {
+ auto *btn = qobject_cast(sender());
+ BlocknetAddressBook::Address data;
+ data.address = btn->getID();
+ // Remove address from data model
+ auto rows = walletModel->getAddressTableModel()->rowCount(QModelIndex());
+ for (int row = rows - 1; row >= 0; --row) {
+ auto index = walletModel->getAddressTableModel()->index(row, 0, QModelIndex());
+ auto *rec = static_cast(index.internalPointer());
+ if (rec && data.address == rec->address) {
+ data.alias = rec->label;
+ data.type = rec->type;
+ break;
+ }
+ }
+ BlocknetAddressEditDialog dlg(walletModel->getAddressTableModel(), walletModel, Qt::WindowSystemMenuHint | Qt::WindowTitleHint);
+ dlg.setData(data.address, data.alias, data.type, QString());
+ dlg.exec();
+}
+
+void BlocknetAccounts::onDoubleClick(int row, int col) {
+ if (row >= filteredData.size()) // check index
+ return;
+ auto *item = table->item(row, COLUMN_ADDRESS);
+ auto addr = item->data(Qt::DisplayRole).toString();
+ for (auto & account : filteredData) {
+ if (account.address.toStdString() == addr.toStdString()) {
+ BlocknetAddressEditDialog dlg(walletModel->getAddressTableModel(), walletModel, Qt::WindowSystemMenuHint | Qt::WindowTitleHint);
+ dlg.setData(account.address, account.alias, account.type, QString());
+ dlg.exec();
+ break;
+ }
+ }
+}
diff --git a/src/qt/blocknetaccounts.h b/src/qt/blocknetaccounts.h
new file mode 100644
index 0000000000..580f3a5da1
--- /dev/null
+++ b/src/qt/blocknetaccounts.h
@@ -0,0 +1,96 @@
+// Copyright (c) 2020 The Blocknet developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BLOCKNET_QT_BLOCKNETACCOUNTS_H
+#define BLOCKNET_QT_BLOCKNETACCOUNTS_H
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class BlocknetAccounts : public QFrame {
+ Q_OBJECT
+public:
+ explicit BlocknetAccounts(QWidget *parent = nullptr);
+ void setWalletModel(WalletModel *w);
+
+ struct Account : public BlocknetAddressBook::Address {
+ CAmount balance{0};
+ int inputs{0};
+ };
+
+private:
+ class NumberItem : public QTableWidgetItem {
+ public:
+ explicit NumberItem() = default;
+ bool operator < (const QTableWidgetItem &other) const override {
+ return amount < reinterpret_cast(&other)->amount;
+ };
+ CAmount amount{0};
+ };
+
+ class LabelItem : public QTableWidgetItem {
+ public:
+ explicit LabelItem() = default;
+ bool operator < (const QTableWidgetItem & other) const override {
+ if (label.empty())
+ return false;
+ auto *oitem = reinterpret_cast(&other);
+ if (oitem->label.empty())
+ return true;
+ return label < oitem->label;
+ };
+ std::string label;
+ };
+
+Q_SIGNALS:
+ void rescan(const std::string &);
+
+private:
+ WalletModel *walletModel;
+ QVBoxLayout *layout;
+ QLabel *titleLbl;
+ QLabel *addButtonLbl;
+ QTableWidget *table;
+ QVector dataModel;
+ QVector filteredData;
+
+ void initialize();
+ void setData(const QVector & data);
+ void unwatch();
+ void watch();
+
+ enum {
+ COLUMN_PADDING1,
+ COLUMN_AVATAR,
+ COLUMN_PADDING2,
+ COLUMN_ALIAS,
+ COLUMN_PADDING3,
+ COLUMN_ADDRESS,
+ COLUMN_PADDING4,
+ COLUMN_BALANCE,
+ COLUMN_PADDING5,
+ COLUMN_INPUTS,
+ COLUMN_PADDING6,
+ COLUMN_COPY,
+ COLUMN_EDIT,
+ };
+
+private Q_SLOTS:
+ void onCopyAddress();
+ void onAddAddress();
+ void onEditAddress();
+ void onDoubleClick(int row, int col);
+};
+
+#endif // BLOCKNET_QT_BLOCKNETACCOUNTS_H
diff --git a/src/qt/blocknetaddressbook.cpp b/src/qt/blocknetaddressbook.cpp
index 47eef1445f..7933553d86 100644
--- a/src/qt/blocknetaddressbook.cpp
+++ b/src/qt/blocknetaddressbook.cpp
@@ -80,7 +80,7 @@ BlocknetAddressBook::BlocknetAddressBook(bool slimMode, int filter, QWidget *par
auto *addAddressBtn = new BlocknetIconBtn(":/redesign/QuickActions/AddressButtonIcon.png");
- addButtonLbl = new QLabel(tr("New Address"));
+ addButtonLbl = new QLabel(tr("Create New Address"));
addButtonLbl->setObjectName("h4");
filterLbl = new QLabel(tr("Filter by:"));
@@ -99,7 +99,10 @@ BlocknetAddressBook::BlocknetAddressBook(bool slimMode, int filter, QWidget *par
table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setFocusPolicy(Qt::NoFocus);
table->setAlternatingRowColors(true);
- table->setColumnWidth(COLUMN_ACTION, BGU::spi(50));
+ table->setColumnWidth(COLUMN_PADDING1, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING2, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING3, BGU::spi(1));
+ table->setColumnWidth(COLUMN_ACTION, BGU::spi(40));
table->setColumnWidth(COLUMN_AVATAR, BGU::spi(50));
table->setColumnWidth(COLUMN_COPY, BGU::spi(65));
table->setColumnWidth(COLUMN_EDIT, BGU::spi(65));
@@ -110,20 +113,22 @@ BlocknetAddressBook::BlocknetAddressBook(bool slimMode, int filter, QWidget *par
table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
table->setContextMenuPolicy(Qt::CustomContextMenu);
table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
- table->verticalHeader()->setDefaultSectionSize(BGU::spi(78));
+ table->verticalHeader()->setDefaultSectionSize(BGU::spi(58));
table->verticalHeader()->setVisible(false);
table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
table->horizontalHeader()->setSortIndicatorShown(true);
table->horizontalHeader()->setSectionsClickable(true);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING1, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING2, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING3, QHeaderView::Fixed);
table->horizontalHeader()->setSectionResizeMode(COLUMN_ACTION, QHeaderView::Fixed);
table->horizontalHeader()->setSectionResizeMode(COLUMN_AVATAR, QHeaderView::Fixed);
table->horizontalHeader()->setSectionResizeMode(COLUMN_ALIAS, QHeaderView::ResizeToContents);
- table->horizontalHeader()->setSectionResizeMode(COLUMN_ADDRESS, QHeaderView::ResizeToContents);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_ADDRESS, QHeaderView::Stretch);
table->horizontalHeader()->setSectionResizeMode(COLUMN_COPY, QHeaderView::Fixed);
table->horizontalHeader()->setSectionResizeMode(COLUMN_EDIT, QHeaderView::Fixed);
table->horizontalHeader()->setSectionResizeMode(COLUMN_DELETE, QHeaderView::Fixed);
- table->horizontalHeader()->setStretchLastSection(true);
- table->setHorizontalHeaderLabels({ "", "", tr("Alias"), tr("Address"), "", "", "" });
+ table->setHorizontalHeaderLabels({ "", "", "", tr("Alias"), "", tr("Address"), "", "", "", "" });
// If in slim mode, hide all columns except add, alias, and address
if (slimMode) {
@@ -170,6 +175,16 @@ BlocknetAddressBook::BlocknetAddressBook(bool slimMode, int filter, QWidget *par
connect(addAddressBtn, &BlocknetIconBtn::clicked, this, &BlocknetAddressBook::onAddAddress);
connect(addressDropdown, &BlocknetDropdown::valueChanged, this, &BlocknetAddressBook::onFilter);
connect(table, &QTableWidget::cellDoubleClicked, this, &BlocknetAddressBook::onDoubleClick);
+ connect(table->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this, [this](int column, Qt::SortOrder order) {
+ QSettings settings;
+ if (column <= COLUMN_PADDING1 || column == COLUMN_PADDING2 || column == COLUMN_PADDING3) {
+ table->horizontalHeader()->setSortIndicator(settings.value("blocknetAddressBookSortColumn").toInt(),
+ static_cast(settings.value("blocknetAddressBookSortOrder").toInt()));
+ return;
+ }
+ settings.setValue("blocknetAddressBookSortOrder", static_cast(order));
+ settings.setValue("blocknetAddressBookSortColumn", column);
+ });
}
void BlocknetAddressBook::setWalletModel(WalletModel *w) {
@@ -221,11 +236,6 @@ void BlocknetAddressBook::initialize() {
dataModel << a;
}
- // Sort on alias descending
- std::sort(dataModel.begin(), dataModel.end(), [](const Address &a, const Address &b) {
- return a.alias > b.alias;
- });
-
this->setData(filtered(dataModel, filteredOption));
}
@@ -303,7 +313,8 @@ void BlocknetAddressBook::setData(const QVector &data) {
table->setItem(i, COLUMN_AVATAR, avatarItem);
// alias
- auto *aliasItem = new QTableWidgetItem;
+ auto *aliasItem = new LabelItem;
+ aliasItem->label = d.alias.toStdString();
aliasItem->setData(Qt::DisplayRole, d.alias);
table->setItem(i, COLUMN_ALIAS, aliasItem);
@@ -377,6 +388,16 @@ void BlocknetAddressBook::setData(const QVector &data) {
}
table->setSortingEnabled(true);
+ QSettings settings;
+ if (settings.contains("blocknetAddressBookSortColumn") && settings.contains("blocknetAddressBookSortOrder")) {
+ const auto col = settings.value("blocknetAddressBookSortColumn").toInt();
+ const auto order = static_cast(settings.value("blocknetAddressBookSortOrder").toInt());
+ table->horizontalHeader()->setSortIndicator(col, order);
+ } else {
+ table->horizontalHeader()->setSortIndicator(COLUMN_ALIAS, Qt::AscendingOrder);
+ settings.setValue("blocknetAddressBookSortColumn", COLUMN_ALIAS);
+ settings.setValue("blocknetAddressBookSortOrder", static_cast(Qt::AscendingOrder));
+ }
watch();
}
@@ -401,24 +422,10 @@ void BlocknetAddressBook::onCopyAddress() {
void BlocknetAddressBook::onAddAddress() {
BlocknetAddressAddDialog dlg(walletModel->getAddressTableModel(), walletModel, Qt::WindowSystemMenuHint | Qt::WindowTitleHint);
dlg.setStyleSheet(GUIUtil::loadStyleSheet());
- connect(&dlg, &QDialog::accepted, this, [this, &dlg]() {
- // If the user added a new private key, ask them if they want to perform a wallet rescan
- if (!dlg.form->getKey().isEmpty()) {
- QMessageBox::StandardButton retval = QMessageBox::question(this->parentWidget(), tr("Rescan the wallet"),
- tr("You imported a new wallet address. Would you like to rescan the blockchain to add coin associated with this address? If you don't rescan, you may not see all your coin.\n\nThis may take several minutes."),
- QMessageBox::Yes | QMessageBox::No,
- QMessageBox::No);
-
- if (retval != QMessageBox::Yes)
- return;
-
- walletModel->showProgress(tr("Rescanning..."), 0);
- // TODO Blocknet Qt wallet rescan after add private key
-// QTimer::singleShot(1000, [this]() {
-// walletModel->RescanFromTime();
-// });
- }
-
+ connect(&dlg, &BlocknetAddressAddDialog::rescan, [this](std::string walletName) {
+ QTimer::singleShot(1000, [this,walletName]() {
+ Q_EMIT rescan(walletName);
+ });
});
dlg.exec();
}
@@ -427,7 +434,6 @@ void BlocknetAddressBook::onEditAddress() {
auto *btn = qobject_cast(sender());
Address data;
data.address = btn->getID();
- // Remove address from data model
auto rows = walletModel->getAddressTableModel()->rowCount(QModelIndex());
for (int row = rows - 1; row >= 0; --row) {
auto index = walletModel->getAddressTableModel()->index(row, 0, QModelIndex());
@@ -446,13 +452,15 @@ void BlocknetAddressBook::onEditAddress() {
void BlocknetAddressBook::onDoubleClick(int row, int col) {
if (row >= filteredData.size()) // check index
return;
- auto data = filteredData[row];
- if (!slimMode) {
- BlocknetAddressEditDialog dlg(walletModel->getAddressTableModel(), walletModel, Qt::WindowSystemMenuHint | Qt::WindowTitleHint);
- dlg.setData(data.address, data.alias, data.type, QString());
- dlg.exec();
- } else {
- Q_EMIT send(data.address);
+ auto *item = table->item(row, COLUMN_ADDRESS);
+ auto addr = item->data(Qt::DisplayRole).toString();
+ for (auto & account : filteredData) {
+ if (account.address.toStdString() == addr.toStdString()) {
+ BlocknetAddressEditDialog dlg(walletModel->getAddressTableModel(), walletModel, Qt::WindowSystemMenuHint | Qt::WindowTitleHint);
+ dlg.setData(account.address, account.alias, account.type, QString());
+ dlg.exec();
+ break;
+ }
}
}
diff --git a/src/qt/blocknetaddressbook.h b/src/qt/blocknetaddressbook.h
index af1bf5d35c..6e0719c669 100644
--- a/src/qt/blocknetaddressbook.h
+++ b/src/qt/blocknetaddressbook.h
@@ -32,6 +32,9 @@ class BlocknetAddressBook : public QFrame
QString alias;
QString address;
int type;
+ bool operator<(const Address & other) const {
+ return address.toStdString() < other.address.toStdString();
+ }
};
enum {
@@ -64,18 +67,36 @@ class BlocknetAddressBook : public QFrame
enum {
COLUMN_ACTION,
COLUMN_AVATAR,
+ COLUMN_PADDING1,
COLUMN_ALIAS,
+ COLUMN_PADDING2,
COLUMN_ADDRESS,
+ COLUMN_PADDING3,
COLUMN_COPY,
COLUMN_EDIT,
COLUMN_DELETE,
};
+ class LabelItem : public QTableWidgetItem {
+ public:
+ explicit LabelItem() = default;
+ bool operator < (const QTableWidgetItem & other) const override {
+ if (label.empty())
+ return false;
+ auto *oitem = reinterpret_cast(&other);
+ if (oitem->label.empty())
+ return true;
+ return label < oitem->label;
+ };
+ std::string label;
+ };
+
public Q_SLOTS:
void onAddressAction();
Q_SIGNALS:
void send(const QString &);
+ void rescan(const std::string &);
private Q_SLOTS:
void onFilter();
diff --git a/src/qt/blocknetaddressedit.cpp b/src/qt/blocknetaddressedit.cpp
index 832a895b38..8b906a5c46 100644
--- a/src/qt/blocknetaddressedit.cpp
+++ b/src/qt/blocknetaddressedit.cpp
@@ -37,7 +37,7 @@ void BlocknetAddressEditDialog::accept() {
// Edit address
const auto dest = DecodeDestination(form->getAddress().toStdString());
if (walletModel->wallet().getAddress(dest, nullptr, nullptr, nullptr))
- walletModel->wallet().setAddressBook(dest, form->getAlias().toStdString(), "");
+ walletModel->wallet().setAddressBook(dest, form->getAlias().toStdString(), ""); // don't change purpose
QDialog::accept();
}
@@ -60,8 +60,10 @@ BlocknetAddressAddDialog::BlocknetAddressAddDialog(AddressTableModel *model, Wal
this->setWindowTitle(tr("Address Book"));
form = new BlocknetAddressEdit(false, tr("New Address"), tr("Add Address"), this);
CPubKey pubkey;
- if (walletModel->wallet().getKeyFromPool(false, pubkey))
- form->setNewAddress(CTxDestination(pubkey.GetID()));
+ if (walletModel->wallet().canGetAddresses() && walletModel->wallet().getKeyFromPool(false, pubkey))
+ form->setNewAddress(GetDestinationForKey(pubkey, OutputType::LEGACY), pubkey);
+ else
+ QMessageBox::warning(this, tr("Wallet was unable to generate a new address"), tr("Try using getnewaddress command in Tools -> Debug Console"));
form->show();
connect(form, &BlocknetAddressEdit::accept, this, &QDialog::accept);
connect(form, &BlocknetAddressEdit::cancel, this, &QDialog::reject);
@@ -75,7 +77,7 @@ void BlocknetAddressAddDialog::accept() {
QMessageBox::warning(this->parentWidget(), tr("Issue"), tr("Bad private key"));
return false;
}
- return importPrivateKey(secret, form->getAlias());
+ return importPrivateKey(secret, form->getAlias(), form->getAddress());
};
// Request to unlock the wallet
auto encStatus = walletModel->getEncryptionStatus();
@@ -93,24 +95,36 @@ void BlocknetAddressAddDialog::accept() {
QDialog::accept();
return;
}
- } else if (!form->getAddress().isEmpty() && IsValidDestinationString(form->getAddress().toStdString())) {
+ } else {
+ if (form->getAddress().isEmpty()) {
+ QMessageBox::warning(this->parentWidget(), tr("Issue"), tr("Address cannot be blank"));
+ return;
+ }
+ if (!IsValidDestinationString(form->getAddress().toStdString())) {
+ QMessageBox::warning(this->parentWidget(), tr("Issue"), tr("Bad or invalid address, please review"));
+ return;
+ }
+
// add send or receive address
- const auto dest = DecodeDestination(form->getAddress().toStdString());
if (form->getType() == AddressTableModel::Send) {
- walletModel->wallet().delAddressBook(dest);
+ const auto dest = DecodeDestination(form->getAddress().toStdString());
walletModel->wallet().setAddressBook(dest, form->getAlias().toStdString(), "send");
} else if (form->getType() == AddressTableModel::Receive) {
+ CTxDestination dest;
+ auto pubkey = form->getPubKey();
+ if (pubkey.IsFullyValid()) {
+ walletModel->wallet().learnRelatedScripts(pubkey, OutputType::LEGACY);
+ dest = GetDestinationForKey(pubkey, OutputType::LEGACY);
+ } else
+ dest = DecodeDestination(form->getAddress().toStdString());
walletModel->wallet().setAddressBook(dest, form->getAlias().toStdString(), "receive");
}
- } else
- model->addRow(form->getType(), form->getAlias(), form->getAddress(), model->GetDefaultAddressType());
+ }
QDialog::accept();
}
-bool BlocknetAddressAddDialog::importPrivateKey(CKey & key, const QString & alias) {
- bool fRescan = true;
-
+bool BlocknetAddressAddDialog::importPrivateKey(CKey & key, const QString & alias, const QString & addr) {
auto wallets = GetWallets();
CWallet *pwallet = nullptr;
for (auto & w : wallets) {
@@ -141,11 +155,6 @@ bool BlocknetAddressAddDialog::importPrivateKey(CKey & key, const QString & alia
return false;
}
- if (fRescan && !reserver.reserve()) {
- QMessageBox::warning(this->parentWidget(), tr("Issue"), tr("Wallet is currently rescanning. Abort existing rescan or wait."));
- return false;
- }
-
if (!key.IsValid()) {
QMessageBox::warning(this->parentWidget(), tr("Issue"), tr("Bad private key"));
return false;
@@ -160,12 +169,20 @@ bool BlocknetAddressAddDialog::importPrivateKey(CKey & key, const QString & alia
{
pwallet->MarkDirty();
- // We don't know which corresponding address will be used;
- // label all new addresses, and label existing addresses if a
- // label was passed.
+ // Check all pubkey addresses for the one specified in the form.
+ bool found{false};
for (const auto& dest : GetAllDestinationsForKey(pubkey)) {
- if (!alias.isEmpty() || pwallet->mapAddressBook.count(dest) == 0)
+ if (EncodeDestination(dest) == addr.toStdString()) {
+ found = true;
+ pwallet->DelAddressBook(dest);
pwallet->SetAddressBook(dest, alias.toStdString(), "receive");
+ break;
+ }
+ }
+
+ if (!found) {
+ QMessageBox::warning(this->parentWidget(), tr("Issue"), tr("Failed to add the key to the wallet"));
+ return false;
}
const auto vchAddress = key.GetPubKey().GetID();
@@ -182,18 +199,17 @@ bool BlocknetAddressAddDialog::importPrivateKey(CKey & key, const QString & alia
}
pwallet->LearnAllRelatedScripts(pubkey);
- }
- }
- if (fRescan) {
- int64_t time_begin = GetTime();
- int64_t scanned_time = pwallet->RescanFromTime(0, reserver, true);
- if (pwallet->IsAbortingRescan()) {
- QMessageBox::warning(this->parentWidget(), tr("Issue"), tr("Rescan aborted by user"));
- return false;
- } else if (scanned_time > time_begin) {
- QMessageBox::warning(this->parentWidget(), tr("Issue"), tr("Rescan was unable to fully rescan the blockchain. Some transactions may be missing."));
- return false;
+ // Prompt user about rescan
+ QMessageBox::StandardButton retval = QMessageBox::question(this->parentWidget(), tr("Rescan the wallet"),
+ tr("You imported a new wallet address. Would you like to rescan the blockchain to add "
+ "coin associated with this address? If you don't rescan, you may not see all your "
+ "coin.\n\nThis may take several minutes."),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No);
+
+ if (retval == QMessageBox::Yes)
+ Q_EMIT rescan(pwallet->GetName());
}
}
@@ -298,6 +314,12 @@ void BlocknetAddressEdit::setData(const QString &address, const QString &alias,
createAddressTi->lineEdit->setText(key);
createAddressTi->setEnabled(!otherUserBtn->isChecked());
}
+ if (!key.isEmpty()) {
+ auto secret = DecodeSecret(key.toStdString());
+ if (secret.IsValid())
+ pubkey = secret.GetPubKey();
+ } else
+ pubkey = CPubKey();
}
QString BlocknetAddressEdit::getAddress() {
@@ -320,6 +342,15 @@ QString BlocknetAddressEdit::getType() {
return AddressTableModel::Send;
}
+CPubKey BlocknetAddressEdit::getPubKey() {
+ CTxDestination dest1 = GetDestinationForKey(pubkey, OutputType::LEGACY);
+ CTxDestination dest2 = DecodeDestination(getAddress().toStdString());
+ if (dest1 == dest2)
+ return pubkey;
+ else
+ return CPubKey();
+}
+
void BlocknetAddressEdit::clear() {
addressTi->lineEdit->clear();
aliasTi->lineEdit->clear();
@@ -350,8 +381,9 @@ void BlocknetAddressEdit::onPrivateKey(const QString &) {
auto err = !secret.IsValid();
createAddressTi->setError(err);
if (!err) {
- CTxDestination address(secret.GetPubKey().GetID());
- addressTi->lineEdit->setText(QString::fromStdString(EncodeDestination(address)));
+ const auto dest = GetDestinationForKey(secret.GetPubKey(), OutputType::LEGACY);
+ pubkey = secret.GetPubKey();
+ addressTi->lineEdit->setText(QString::fromStdString(EncodeDestination(dest)));
myAddressBtn->setChecked(true);
otherUserBtn->setChecked(false);
otherUserBtn->setEnabled(false);
@@ -399,9 +431,10 @@ void BlocknetAddressEdit::onOtherUser(bool checked) {
createAddressTi->setEnabled(!otherUserBtn->isChecked());
}
-bool BlocknetAddressEdit::setNewAddress(const CTxDestination & dest) {
+bool BlocknetAddressEdit::setNewAddress(const CTxDestination & dest, const CPubKey & pkey) {
if (!IsValidDestination(dest))
return false;
+ pubkey = pkey;
myAddressBtn->setChecked(true);
addressTi->lineEdit->setText(QString::fromStdString(EncodeDestination(dest)));
return true;
diff --git a/src/qt/blocknetaddressedit.h b/src/qt/blocknetaddressedit.h
index 887aa3622a..a4bed01217 100644
--- a/src/qt/blocknetaddressedit.h
+++ b/src/qt/blocknetaddressedit.h
@@ -26,12 +26,13 @@ class BlocknetAddressEdit : public QFrame {
explicit BlocknetAddressEdit(bool editMode, const QString &title, const QString &buttonString, QWidget *parent = nullptr);
QSize sizeHint() const override;
bool validated();
- bool setNewAddress(const CTxDestination & dest);
+ bool setNewAddress(const CTxDestination & dest, const CPubKey & pkey);
void setData(const QString &address, const QString &alias, const int &type, const QString &key);
QString getAddress();
QString getAlias();
QString getKey();
QString getType();
+ CPubKey getPubKey();
Q_SIGNALS:
void cancel();
@@ -64,6 +65,7 @@ private Q_SLOTS:
QRadioButton *otherUserBtn;
BlocknetFormBtn *confirmBtn;
BlocknetFormBtn *cancelBtn;
+ CPubKey pubkey;
};
class BlocknetAddressEditDialog : public QDialog {
@@ -86,9 +88,11 @@ Q_OBJECT
explicit BlocknetAddressAddDialog(AddressTableModel *model, WalletModel *walletModel, Qt::WindowFlags f, QWidget *parent = nullptr);
void accept() override;
BlocknetAddressEdit *form;
+Q_SIGNALS:
+ void rescan(const std::string &);
protected:
void resizeEvent(QResizeEvent *evt) override;
- bool importPrivateKey(CKey & key, const QString & alias);
+ bool importPrivateKey(CKey & key, const QString & alias, const QString & addr);
private:
AddressTableModel *model;
WalletModel *walletModel;
diff --git a/src/qt/blocknetcoincontrol.cpp b/src/qt/blocknetcoincontrol.cpp
index b0f75f3753..c6dea79209 100644
--- a/src/qt/blocknetcoincontrol.cpp
+++ b/src/qt/blocknetcoincontrol.cpp
@@ -4,8 +4,6 @@
#include
-#include
-
#include
#include
#include
@@ -15,6 +13,7 @@
#include
#include
+#include
#include
#include
#include
@@ -60,7 +59,7 @@ BlocknetCoinControlDialog::BlocknetCoinControlDialog(WalletModel *w, QWidget *pa
btnBoxLayout->addStretch(1);
// Manages the coin list
- cc = new BlocknetCoinControl;
+ cc = new BlocknetCoinControl(nullptr, w);
// Manages the selected coin details
feePanel = new QFrame;
@@ -277,9 +276,11 @@ void BlocknetCoinControlDialog::updateUTXOState() {
/**
* @brief Manages and displays the coin control input list.
* @param parent
+ * @param w Wallet model
*/
-BlocknetCoinControl::BlocknetCoinControl(QWidget *parent) : QFrame(parent), layout(new QVBoxLayout),
- table(new QTableWidget), contextMenu(new QMenu) {
+BlocknetCoinControl::BlocknetCoinControl(QWidget *parent, WalletModel *w) : QFrame(parent), walletModel(w), layout(new QVBoxLayout),
+ table(new BlocknetTableWidget), tree(new QTreeWidget), contextMenu(new QMenu)
+{
// this->setStyleSheet("border: 1px solid red");
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout->setContentsMargins(QMargins());
@@ -290,40 +291,106 @@ BlocknetCoinControl::BlocknetCoinControl(QWidget *parent) : QFrame(parent), layo
table->setColumnCount(COLUMN_TXVOUT + 1);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ table->setSelectionMode(QAbstractItemView::ExtendedSelection);
table->setAlternatingRowColors(true);
- table->setColumnWidth(COLUMN_PADDING, BGU::spi(10));
- table->setColumnWidth(COLUMN_CHECKBOX, BGU::spi(30));
+ table->setColumnWidth(COLUMN_PADDING1, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING2, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING3, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING4, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING5, BGU::spi(1));
+ table->setColumnWidth(COLUMN_PADDING6, BGU::spi(1));
table->setShowGrid(false);
- table->setFocusPolicy(Qt::NoFocus);
+ table->setFocusPolicy(Qt::ClickFocus);
table->setContextMenuPolicy(Qt::CustomContextMenu);
table->setColumnHidden(COLUMN_TXHASH, true);
table->setColumnHidden(COLUMN_TXVOUT, true);
table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
- table->verticalHeader()->setDefaultSectionSize(BGU::spi(60));
+ table->verticalHeader()->setDefaultSectionSize(BGU::spi(25));
table->verticalHeader()->setVisible(false);
table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
table->horizontalHeader()->setSortIndicatorShown(true);
table->horizontalHeader()->setSectionsClickable(true);
table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
- table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING, QHeaderView::Fixed);
- table->horizontalHeader()->setSectionResizeMode(COLUMN_CHECKBOX, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING1, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING2, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING3, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING4, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING5, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING6, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_CHECKBOX, QHeaderView::ResizeToContents);
table->horizontalHeader()->setSectionResizeMode(COLUMN_AMOUNT, QHeaderView::ResizeToContents);
table->horizontalHeader()->setSectionResizeMode(COLUMN_ADDRESS, QHeaderView::ResizeToContents);
- table->setHorizontalHeaderLabels({ "", "", tr("Amount"), tr("Label"), tr("Address"), tr("Date"), tr("Confirmations"), tr("Priority"), "" });
+ table->setHorizontalHeaderLabels({ "", "", "", tr("Amount"), "", tr("Label"), "", tr("Address"), "", tr("Date"), "", tr("Confirmations"), "" });
+
+ // tree
+ tree->setContentsMargins(QMargins());
+ tree->setColumnCount(COLUMN_TXVOUT + 1);
+ tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ tree->setSelectionBehavior(QAbstractItemView::SelectRows);
+ tree->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ tree->setAlternatingRowColors(true);
+ tree->setColumnWidth(COLUMN_PADDING1, BGU::spi(1));
+ tree->setColumnWidth(COLUMN_PADDING2, BGU::spi(1));
+ tree->setColumnWidth(COLUMN_PADDING3, BGU::spi(1));
+ tree->setColumnWidth(COLUMN_PADDING4, BGU::spi(1));
+ tree->setColumnWidth(COLUMN_PADDING5, BGU::spi(1));
+ tree->setColumnWidth(COLUMN_PADDING6, BGU::spi(1));
+ tree->setFocusPolicy(Qt::ClickFocus);
+ tree->setContextMenuPolicy(Qt::CustomContextMenu);
+ tree->setColumnHidden(COLUMN_TXHASH, true);
+ tree->setColumnHidden(COLUMN_TXVOUT, true);
+ tree->header()->setDefaultAlignment(Qt::AlignLeft);
+ tree->header()->setSortIndicatorShown(true);
+ tree->header()->setSectionsClickable(true);
+ tree->header()->setSectionResizeMode(QHeaderView::Stretch);
+ tree->header()->setSectionResizeMode(COLUMN_PADDING1, QHeaderView::Fixed);
+ tree->header()->setSectionResizeMode(COLUMN_PADDING2, QHeaderView::Fixed);
+ tree->header()->setSectionResizeMode(COLUMN_PADDING3, QHeaderView::Fixed);
+ tree->header()->setSectionResizeMode(COLUMN_PADDING4, QHeaderView::Fixed);
+ tree->header()->setSectionResizeMode(COLUMN_PADDING5, QHeaderView::Fixed);
+ tree->header()->setSectionResizeMode(COLUMN_PADDING6, QHeaderView::Fixed);
+ tree->header()->setSectionResizeMode(COLUMN_CHECKBOX, QHeaderView::ResizeToContents);
+ tree->header()->setSectionResizeMode(COLUMN_AMOUNT, QHeaderView::ResizeToContents);
+ tree->header()->setSectionResizeMode(COLUMN_ADDRESS, QHeaderView::ResizeToContents);
+ tree->setUniformRowHeights(true);
+ tree->setItemDelegate(new TreeDelegate);
+ tree->setHeaderLabels({ "", "", "", tr("Amount"), "", tr("Label"), "", tr("Address"), "", tr("Date"), "", tr("Confirmations"), "" });
+
+ // Tree mode
+ auto *treeBox = new QFrame;
+ auto *treeBoxLayout = new QHBoxLayout;
+ treeBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ treeBox->setContentsMargins(QMargins());
+ treeBox->setLayout(treeBoxLayout);
+ treeBoxLayout->setSpacing(BGU::spi(20));
+ listRb = new QRadioButton(tr("List mode"));
+ treeRb = new QRadioButton(tr("Tree mode"));
+ treeBoxLayout->addStretch(1);
+ treeBoxLayout->addWidget(listRb);
+ treeBoxLayout->addWidget(treeRb);
+ treeBoxLayout->addStretch(1);
+ auto *group = new QButtonGroup;
+ group->addButton(listRb, 0);
+ group->addButton(treeRb, 1);
+ group->setExclusive(true);
// context menu actions
selectCoins = new QAction(tr("Select coins"), this);
deselectCoins = new QAction(tr("Deselect coins"), this);
selectCoins->setEnabled(false);
deselectCoins->setEnabled(false);
- auto *selectAllCoins = new QAction(tr("Select all coins"), this);
- auto *deselectAllCoins = new QAction(tr("Deselect all coins"), this);
- auto *copyAmountAction = new QAction(tr("Copy amount"), this);
- auto *copyLabelAction = new QAction(tr("Copy label"), this);
- auto *copyAddressAction = new QAction(tr("Copy address"), this);
- auto *copyTransactionAction = new QAction(tr("Copy transaction ID"), this);
- auto *lockAction = new QAction(tr("Lock unspent"), this);
- auto *unlockAction = new QAction(tr("Unlock unspent"), this);
+ selectAllCoins = new QAction(tr("Select all coins"), this);
+ deselectAllCoins = new QAction(tr("Deselect all coins"), this);
+ copyAmountAction = new QAction(tr("Copy amount"), this);
+ copyLabelAction = new QAction(tr("Copy label"), this);
+ copyAddressAction = new QAction(tr("Copy address"), this);
+ copyTransactionAction = new QAction(tr("Copy transaction ID"), this);
+ lockAction = new QAction(tr("Lock unspent"), this);
+ unlockAction = new QAction(tr("Unlock unspent"), this);
+ expandAll = new QAction(tr("Expand all"), this);
+ collapseAll = new QAction(tr("Collapse all"), this);
+ expandAll->setEnabled(false);
+ collapseAll->setEnabled(false);
// context menu
contextMenu->addAction(selectCoins);
@@ -339,60 +406,103 @@ BlocknetCoinControl::BlocknetCoinControl(QWidget *parent) : QFrame(parent), layo
contextMenu->addSeparator();
contextMenu->addAction(lockAction);
contextMenu->addAction(unlockAction);
+ contextMenu->addSeparator();
+ contextMenu->addAction(expandAll);
+ contextMenu->addAction(collapseAll);
+ layout->addWidget(tree);
layout->addWidget(table);
+ layout->addWidget(treeBox);
- // Restore sorting preferences
- QSettings s;
- if (s.contains("nCoinControlSortColumn") && s.contains("nCoinControlSortOrder"))
- table->horizontalHeader()->setSortIndicator(s.value("nCoinControlSortColumn").toInt(),
- static_cast(s.value("nCoinControlSortOrder").toInt()));
+ // By default hide the tree view
+ {
+ QSettings settings;
+ if (settings.contains("showTreeMode")) {
+ const auto b = settings.value("showTreeMode").toBool();
+ listRb->setChecked(!b);
+ treeRb->setChecked(b);
+ showTree(b);
+ } else {
+ listRb->setChecked(true);
+ treeRb->setChecked(false);
+ showTree(false);
+ settings.setValue("showTreeMode", false);
+ }
+ }
connect(table, &QTableWidget::customContextMenuRequested, this, &BlocknetCoinControl::showContextMenu);
+ connect(tree, &QTableWidget::customContextMenuRequested, this, &BlocknetCoinControl::showContextMenu);
connect(table->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this, [this](int column, Qt::SortOrder order) {
QSettings settings;
- settings.setValue("nCoinControlSortOrder", table->horizontalHeader()->sortIndicatorOrder());
- settings.setValue("nCoinControlSortColumn", table->horizontalHeader()->sortIndicatorSection());
+ // ignore sorting on columns less than or equal to pad2
+ if (column <= COLUMN_PADDING2 || column == COLUMN_PADDING3 || column == COLUMN_PADDING4
+ || column == COLUMN_PADDING5 || column == COLUMN_PADDING6) {
+ table->horizontalHeader()->setSortIndicator(settings.value("nCoinControlSortColumn").toInt(),
+ static_cast(settings.value("nCoinControlSortOrder").toInt()));
+ return;
+ }
+ settings.setValue("nCoinControlSortOrder", static_cast(order));
+ settings.setValue("nCoinControlSortColumn", column);
+ });
+ connect(tree->header(), &QHeaderView::sortIndicatorChanged, this, [this](int column, Qt::SortOrder order) {
+ QSettings settings;
+ // ignore sorting on columns less than or equal to pad2
+ if (column <= COLUMN_PADDING2 || column == COLUMN_PADDING3 || column == COLUMN_PADDING4
+ || column == COLUMN_PADDING5 || column == COLUMN_PADDING6) {
+ tree->header()->setSortIndicator(settings.value("nCoinControlTreeSortColumn").toInt(),
+ static_cast(settings.value("nCoinControlTreeSortOrder").toInt()));
+ return;
+ }
+ settings.setValue("nCoinControlTreeSortOrder", static_cast(order));
+ settings.setValue("nCoinControlTreeSortColumn", column);
+ });
+
+ // Tree mode
+ connect(group, static_cast(&QButtonGroup::buttonClicked), this, [this](int button) {
+ const auto b = treeMode();
+ QSettings settings;
+ settings.setValue("showTreeMode", b);
+ showTree(b);
});
connect(selectCoins, &QAction::triggered, this, [this]() {
+ if (treeMode()) {
+ auto items = tree->selectedItems();
+ if (!items.empty()) {
+ unwatch();
+ updateTableUtxos(updateTreeCheckStates(items, Qt::Checked));
+ watch();
+ Q_EMIT tableUpdated();
+ }
+ return;
+ }
+ // Update the list
auto *select = table->selectionModel();
if (select->hasSelection()) {
unwatch();
auto idxs = select->selectedRows(COLUMN_CHECKBOX);
- for (auto &idx : idxs) {
- auto *item = table->item(idx.row(), idx.column());
- if (item) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(item), getVOut(item), utxo) && utxo != nullptr && utxo->isValid()) {
- if (!utxo->locked) { // do not select locked coins
- utxo->checked = true;
- item->setCheckState(Qt::Checked);
- }
- }
- }
- }
+ updateTreeUtxos(updateTableCheckStates(idxs, Qt::Checked));
watch();
Q_EMIT tableUpdated();
}
});
connect(deselectCoins, &QAction::triggered, this, [this]() {
+ if (treeMode()) {
+ auto items = tree->selectedItems();
+ if (!items.empty()) {
+ unwatch();
+ updateTableUtxos(updateTreeCheckStates(items, Qt::Unchecked));
+ watch();
+ Q_EMIT tableUpdated();
+ }
+ return;
+ }
+ // Update the list
auto *select = table->selectionModel();
if (select->hasSelection()) {
unwatch();
auto idxs = select->selectedRows(COLUMN_CHECKBOX);
- for (auto &idx : idxs) {
- auto *item = table->item(idx.row(), idx.column());
- if (item) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(item), getVOut(item), utxo) && utxo != nullptr && utxo->isValid()) {
- if (!utxo->locked) { // do not modify locked coins
- utxo->checked = false;
- item->setCheckState(Qt::Unchecked);
- }
- }
- }
- }
+ updateTreeUtxos(updateTableCheckStates(idxs, Qt::Unchecked));
watch();
Q_EMIT tableUpdated();
}
@@ -400,113 +510,104 @@ BlocknetCoinControl::BlocknetCoinControl(QWidget *parent) : QFrame(parent), layo
connect(selectAllCoins, &QAction::triggered, this, [this]() {
unwatch();
- for (int row = 0; row < table->rowCount(); ++row) {
- auto *item = table->item(row, COLUMN_CHECKBOX);
- if (item) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(item), getVOut(item), utxo) && utxo != nullptr && utxo->isValid()) {
- if (!utxo->locked) { // do not select locked coins
- utxo->checked = true;
- item->setCheckState(Qt::Checked);
- }
- }
- }
- }
+ updateTableUtxos(updateTreeCheckStates(allTreeItems(), Qt::Checked));
watch();
Q_EMIT tableUpdated();
});
connect(deselectAllCoins, &QAction::triggered, this, [this]() {
unwatch();
- for (int row = 0; row < table->rowCount(); ++row) {
- auto *item = table->item(row, COLUMN_CHECKBOX);
- if (item) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(item), getVOut(item), utxo) && utxo != nullptr && utxo->isValid()) {
- if (!utxo->locked) { // do not select locked coins
- utxo->checked = false;
- item->setCheckState(Qt::Unchecked);
- }
- }
- }
- }
+ updateTableUtxos(updateTreeCheckStates(allTreeItems(), Qt::Unchecked));
watch();
Q_EMIT tableUpdated();
});
connect(copyAmountAction, &QAction::triggered, this, [this]() {
- if (contextItem) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(contextItem), getVOut(contextItem), utxo) && utxo != nullptr && utxo->isValid())
+ UTXO *utxo = nullptr;
+ if (treeMode()) {
+ if (contextItemTr && utxoForHash(getTransactionHash(contextItemTr), getVOut(contextItemTr), utxo) && utxo != nullptr && utxo->isValid())
setClipboard(utxo->amount);
- }
+ } else if (contextItem && utxoForHash(getTransactionHash(contextItem), getVOut(contextItem), utxo) && utxo != nullptr && utxo->isValid())
+ setClipboard(utxo->amount);
});
connect(copyLabelAction, &QAction::triggered, this, [this]() {
- if (contextItem) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(contextItem), getVOut(contextItem), utxo) && utxo != nullptr && utxo->isValid())
+ UTXO *utxo = nullptr;
+ if (treeMode()) {
+ if (contextItemTr && utxoForHash(getTransactionHash(contextItemTr), getVOut(contextItemTr), utxo) && utxo != nullptr && utxo->isValid())
setClipboard(utxo->label);
- }
+ } else if (contextItem && utxoForHash(getTransactionHash(contextItem), getVOut(contextItem), utxo) && utxo != nullptr && utxo->isValid())
+ setClipboard(utxo->label);
});
connect(copyAddressAction, &QAction::triggered, this, [this]() {
- if (contextItem) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(contextItem), getVOut(contextItem), utxo) && utxo != nullptr && utxo->isValid())
+ UTXO *utxo = nullptr;
+ if (treeMode()) {
+ if (contextItemTr && utxoForHash(getTransactionHash(contextItemTr), getVOut(contextItemTr), utxo) && utxo != nullptr && utxo->isValid())
setClipboard(utxo->address);
- }
+ } else if (contextItem && utxoForHash(getTransactionHash(contextItem), getVOut(contextItem), utxo) && utxo != nullptr && utxo->isValid())
+ setClipboard(utxo->address);
});
connect(copyTransactionAction, &QAction::triggered, this, [this]() {
- if (contextItem) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(contextItem), getVOut(contextItem), utxo) && utxo != nullptr && utxo->isValid())
+ UTXO *utxo = nullptr;
+ if (treeMode()) {
+ if (contextItemTr && utxoForHash(getTransactionHash(contextItemTr), getVOut(contextItemTr), utxo) && utxo != nullptr && utxo->isValid())
setClipboard(utxo->transaction);
- }
+ } else if (contextItem && utxoForHash(getTransactionHash(contextItem), getVOut(contextItem), utxo) && utxo != nullptr && utxo->isValid())
+ setClipboard(utxo->transaction);
});
+
connect(lockAction, &QAction::triggered, this, [this]() {
+ const bool locked{true};
+ if (treeMode()) {
+ auto items = tree->selectedItems();
+ if (!items.empty()) {
+ unwatch();
+ updateTableUtxos(updateTreeCheckStates(items, Qt::Unchecked, &locked));
+ watch();
+ Q_EMIT tableUpdated();
+ }
+ return;
+ }
+ // Update the list
auto *select = table->selectionModel();
if (select->hasSelection()) {
unwatch();
auto idxs = select->selectedRows(COLUMN_CHECKBOX);
- for (auto &idx : idxs) {
- auto *item = table->item(idx.row(), COLUMN_TXHASH);
- if (item) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(item), getVOut(item), utxo) && utxo != nullptr && utxo->isValid()) {
- utxo->locked = true;
- utxo->unlocked = !utxo->locked;
- utxo->checked = false;
- auto *cbItem = new QTableWidgetItem;
- cbItem->setIcon(QIcon(":/icons/lock_closed"));
- table->setItem(idx.row(), COLUMN_CHECKBOX, cbItem);
- }
- }
- }
+ updateTreeUtxos(updateTableCheckStates(idxs, Qt::Unchecked, &locked));
watch();
Q_EMIT tableUpdated();
}
});
connect(unlockAction, &QAction::triggered, this, [this]() {
+ const bool locked{false};
+ if (treeMode()) {
+ auto items = tree->selectedItems();
+ if (!items.empty()) {
+ unwatch();
+ updateTableUtxos(updateTreeCheckStates(items, Qt::Unchecked, &locked));
+ watch();
+ Q_EMIT tableUpdated();
+ }
+ return;
+ }
+ // Update the list
auto *select = table->selectionModel();
if (select->hasSelection()) {
unwatch();
auto idxs = select->selectedRows(COLUMN_CHECKBOX);
- for (auto &idx : idxs) {
- auto *item = table->item(idx.row(), COLUMN_TXHASH);
- if (item) {
- UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(item), getVOut(item), utxo) && utxo->isValid()) {
- utxo->locked = false;
- utxo->unlocked = !utxo->locked;
- utxo->checked = false;
- auto *cbItem = new QTableWidgetItem;
- cbItem->setCheckState(Qt::Unchecked);
- table->setItem(idx.row(), COLUMN_CHECKBOX, cbItem);
- }
- }
- }
+ updateTreeUtxos(updateTableCheckStates(idxs, Qt::Unchecked, &locked));
watch();
Q_EMIT tableUpdated();
}
});
+
+ connect(expandAll, &QAction::triggered, this, [this]() {
+ tree->expandAll();
+ });
+ connect(collapseAll, &QAction::triggered, this, [this]() {
+ tree->collapseAll();
+ });
+
+ table->installEventFilter(this);
+ tree->installEventFilter(this);
}
void BlocknetCoinControl::setData(ModelPtr dataModel) {
@@ -516,61 +617,111 @@ void BlocknetCoinControl::setData(ModelPtr dataModel) {
table->clearContents();
table->setRowCount(dataModel->data.count());
table->setSortingEnabled(false);
+ tree->clear();
+ tree->setSortingEnabled(false);
+
+ std::map topLevelItems;
for (int i = 0; i < dataModel->data.count(); ++i) {
auto *d = dataModel->data[i];
+ // Tree top level item
+ BlocknetCoinControl::TreeWidgetItem *topLevelItemTr = nullptr;
+ if (topLevelItems.count(d->address))
+ topLevelItemTr = topLevelItems[d->address];
+ else {
+ topLevelItemTr = new BlocknetCoinControl::TreeWidgetItem;
+ topLevelItemTr->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsAutoTristate);
+ topLevelItemTr->setText(COLUMN_LABEL, d->label);
+ topLevelItemTr->setText(COLUMN_ADDRESS, d->address);
+ topLevelItemTr->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
+ topLevelItems[d->address] = topLevelItemTr;
+ tree->addTopLevelItem(topLevelItemTr);
+ }
+
+ // Current tree item
+ auto *treeItem = new BlocknetCoinControl::TreeWidgetItem(topLevelItemTr);
+
// checkbox
auto *cbItem = new QTableWidgetItem;
- if (d->locked)
- cbItem->setIcon(QIcon(":/icons/lock_closed"));
- else cbItem->setCheckState(d->checked ? Qt::Checked : Qt::Unchecked);
+ if (d->locked) {
+ cbItem->setIcon(QIcon(":/redesign/lock_closed_white"));
+ treeItem->setIcon(COLUMN_CHECKBOX, QIcon(":/redesign/lock_closed_white"));
+ } else {
+ cbItem->setCheckState(d->checked ? Qt::Checked : Qt::Unchecked);
+ treeItem->setCheckState(COLUMN_CHECKBOX, d->checked ? Qt::Checked : Qt::Unchecked);
+ }
table->setItem(i, COLUMN_CHECKBOX, cbItem);
// amount
auto *amountItem = new BlocknetCoinControl::NumberItem;
amountItem->setData(Qt::DisplayRole, d->amount);
+ treeItem->setData(COLUMN_AMOUNT, Qt::DisplayRole, d->amount);
+ treeItem->setData(COLUMN_AMOUNT, Qt::UserRole, static_cast(d->camount));
table->setItem(i, COLUMN_AMOUNT, amountItem);
+ // Add the amount to the associated top level item's total
+ topLevelItemTr->camount += d->camount;
+ topLevelItemTr->setData(COLUMN_AMOUNT, Qt::DisplayRole, BitcoinUnits::format(walletModel->getOptionsModel()->getDisplayUnit(), topLevelItemTr->camount));
+ topLevelItemTr->setData(COLUMN_AMOUNT, Qt::UserRole, static_cast(topLevelItemTr->camount));
// label
- auto *labelItem = new QTableWidgetItem;
+ auto *labelItem = new LabelItem;
labelItem->setText(d->label);
table->setItem(i, COLUMN_LABEL, labelItem);
+ treeItem->setText(COLUMN_LABEL, d->label);
// address
auto *addressItem = new QTableWidgetItem;
addressItem->setText(d->address);
table->setItem(i, COLUMN_ADDRESS, addressItem);
+ treeItem->setText(COLUMN_ADDRESS, d->address);
// date
auto *dateItem = new QTableWidgetItem;
auto localDate = d->date.toLocalTime();
dateItem->setData(Qt::DisplayRole, d->date);
+ treeItem->setData(COLUMN_DATE, Qt::DisplayRole, d->date);
+ treeItem->setData(COLUMN_DATE, Qt::UserRole, d->date);
table->setItem(i, COLUMN_DATE, dateItem);
// confirmations
auto *confItem = new BlocknetCoinControl::NumberItem;
confItem->setData(Qt::DisplayRole, QString::number(d->confirmations));
+ treeItem->setData(COLUMN_CONFIRMATIONS, Qt::DisplayRole, static_cast(d->confirmations));
+ treeItem->setData(COLUMN_CONFIRMATIONS, Qt::UserRole, static_cast(d->confirmations));
table->setItem(i, COLUMN_CONFIRMATIONS, confItem);
- // priority
- auto *priorityItem = new BlocknetCoinControl::PriorityItem;
- priorityItem->setData(PriorityItem::PriorityRole, d->priority);
- priorityItem->setData(Qt::DisplayRole, getPriorityLabel(d->priority));
- table->setItem(i, COLUMN_PRIORITY, priorityItem);
-
// txhash
auto *txhashItem = new QTableWidgetItem;
txhashItem->setData(Qt::DisplayRole, d->transaction);
+ treeItem->setData(COLUMN_TXHASH, Qt::DisplayRole, d->transaction);
table->setItem(i, COLUMN_TXHASH, txhashItem);
// tx vout
auto *txvoutItem = new QTableWidgetItem;
txvoutItem->setData(Qt::DisplayRole, d->vout);
+ treeItem->setData(COLUMN_TXVOUT, Qt::DisplayRole, d->vout);
table->setItem(i, COLUMN_TXVOUT, txvoutItem);
}
table->setSortingEnabled(true);
+ tree->setSortingEnabled(true);
+
+ // Restore sorting preferences
+ QSettings s;
+ if (s.contains("nCoinControlSortColumn") && s.contains("nCoinControlSortOrder")) {
+ table->horizontalHeader()->setSortIndicator(s.value("nCoinControlSortColumn").toInt(),
+ static_cast(s.value("nCoinControlSortOrder").toInt()));
+ } else {
+ table->horizontalHeader()->setSortIndicator(COLUMN_LABEL, Qt::SortOrder::AscendingOrder);
+ }
+ if (s.contains("nCoinControlTreeSortOrder") && s.contains("nCoinControlTreeSortOrder")) {
+ tree->header()->setSortIndicator(s.value("nCoinControlTreeSortOrder").toInt(),
+ static_cast(s.value("nCoinControlTreeSortOrder").toInt()));
+ } else {
+ tree->header()->setSortIndicator(COLUMN_LABEL, Qt::SortOrder::AscendingOrder);
+ }
+
watch();
}
@@ -579,22 +730,56 @@ BlocknetCoinControl::ModelPtr BlocknetCoinControl::getData() {
}
void BlocknetCoinControl::sizeTo(const int minimumHeight, const int maximumHeight) {
- int h = dataModel ? dataModel->data.count() * 60 : minimumHeight;
+ int h = dataModel ? dataModel->data.count() * 25 : minimumHeight;
if (h > maximumHeight)
h = maximumHeight;
table->setFixedHeight(h);
}
void BlocknetCoinControl::showContextMenu(QPoint pt) {
- auto *select = table->selectionModel();
- selectCoins->setEnabled(select->hasSelection());
- deselectCoins->setEnabled(select->hasSelection());
- auto *item = table->itemAt(pt);
- if (!item) {
+ if (treeMode()) {
+ auto *select = tree->selectionModel();
+ selectCoins->setEnabled(select->hasSelection());
+ deselectCoins->setEnabled(select->hasSelection());
+ selectAllCoins->setEnabled(select->hasSelection());
+ deselectAllCoins->setEnabled(select->hasSelection());
+ lockAction->setEnabled(select->hasSelection());
+ unlockAction->setEnabled(select->hasSelection());
+ expandAll->setEnabled(true);
+ collapseAll->setEnabled(true);
+ auto *item = tree->itemAt(pt);
+ if (!item) {
+ contextItemTr = nullptr;
+ return;
+ }
+ copyAmountAction->setEnabled(item->childCount() <= 0);
+ copyLabelAction->setEnabled(item->childCount() <= 0);
+ copyAddressAction->setEnabled(item->childCount() <= 0);
+ copyTransactionAction->setEnabled(item->childCount() <= 0);
+ contextItemTr = item;
contextItem = nullptr;
- return;
+ } else {
+ auto *select = table->selectionModel();
+ selectCoins->setEnabled(select->hasSelection());
+ deselectCoins->setEnabled(select->hasSelection());
+ selectAllCoins->setEnabled(select->hasSelection());
+ deselectAllCoins->setEnabled(select->hasSelection());
+ lockAction->setEnabled(select->hasSelection());
+ unlockAction->setEnabled(select->hasSelection());
+ expandAll->setEnabled(false);
+ collapseAll->setEnabled(false);
+ auto *item = table->itemAt(pt);
+ if (!item) {
+ contextItem = nullptr;
+ return;
+ }
+ copyAmountAction->setEnabled(true);
+ copyLabelAction->setEnabled(true);
+ copyAddressAction->setEnabled(true);
+ copyTransactionAction->setEnabled(true);
+ contextItem = item;
+ contextItemTr = nullptr;
}
- contextItem = item;
contextMenu->exec(QCursor::pos());
}
@@ -634,13 +819,21 @@ QString BlocknetCoinControl::getPriorityLabel(double dPriority) {
}
void BlocknetCoinControl::unwatch() {
+ table->blockSignals(true);
+ tree->blockSignals(true);
table->setEnabled(false);
+ tree->setEnabled(false);
disconnect(table, &QTableWidget::itemChanged, this, &BlocknetCoinControl::onItemChanged);
+ disconnect(tree, &QTreeWidget::itemChanged, this, &BlocknetCoinControl::onTreeItemChanged);
}
void BlocknetCoinControl::watch() {
+ table->blockSignals(false);
+ tree->blockSignals(false);
table->setEnabled(true);
+ tree->setEnabled(true);
connect(table, &QTableWidget::itemChanged, this, &BlocknetCoinControl::onItemChanged);
+ connect(tree, &QTreeWidget::itemChanged, this, &BlocknetCoinControl::onTreeItemChanged);
}
bool BlocknetCoinControl::utxoForHash(const QString transaction, const uint vout, UTXO *&utxo) {
@@ -661,10 +854,302 @@ uint BlocknetCoinControl::getVOut(QTableWidgetItem *item) {
return table->item(item->row(), COLUMN_TXVOUT)->data(Qt::DisplayRole).toUInt();
}
-void BlocknetCoinControl::onItemChanged(QTableWidgetItem *item) {
+QString BlocknetCoinControl::getTransactionHash(QTreeWidgetItem *item) {
+ return item->data(COLUMN_TXHASH, Qt::DisplayRole).toString();
+}
+
+uint BlocknetCoinControl::getVOut(QTreeWidgetItem *item) {
+ return item->data(COLUMN_TXVOUT, Qt::DisplayRole).toUInt();
+}
+
+bool BlocknetCoinControl::treeMode() {
+ return treeRb->isChecked();
+}
+
+void BlocknetCoinControl::showTree(bool yes) {
+ if (yes) {
+ table->setDisabled(true);
+ table->hide();
+ tree->setDisabled(false);
+ tree->show();
+ tree->setFocus(Qt::FocusReason::MouseFocusReason);
+ } else {
+ tree->setDisabled(true);
+ tree->hide();
+ table->setDisabled(false);
+ table->show();
+ table->setFocus(Qt::FocusReason::MouseFocusReason);
+ }
+}
+
+BlocknetCoinControl::UTXO* BlocknetCoinControl::getTableUtxo(QTableWidgetItem *item, int row) {
UTXO *utxo = nullptr;
- if (utxoForHash(getTransactionHash(item), getVOut(item), utxo) && utxo != nullptr && utxo->isValid()) {
- utxo->checked = item->checkState() == Qt::Checked;
- Q_EMIT tableUpdated();
+ if (utxoForHash(getTransactionHash(item), getVOut(item), utxo)) {
+ if (!utxo->isValid())
+ utxo = nullptr;
+ }
+ return utxo;
+}
+
+BlocknetCoinControl::UTXO* BlocknetCoinControl::getTreeUtxo(QTreeWidgetItem *item) {
+ UTXO *utxo = nullptr;
+ if (utxoForHash(getTransactionHash(item), getVOut(item), utxo)) {
+ if (!utxo->isValid())
+ utxo = nullptr;
+ }
+ return utxo;
+}
+
+void BlocknetCoinControl::onItemChanged(QTableWidgetItem *item) {
+ unwatch();
+ auto idx = table->itemIndex(item);
+ UTXO *utxo = getTableUtxo(item, idx.row());
+ if (utxo && utxo->locked)
+ item->setCheckState(Qt::Unchecked);
+ else
+ updateTreeUtxos(updateTableCheckStates({idx}, item->checkState()));
+ watch();
+ Q_EMIT tableUpdated();
+}
+
+void BlocknetCoinControl::onTreeItemChanged(QTreeWidgetItem *item) {
+ if (item->checkState(COLUMN_CHECKBOX) == Qt::PartiallyChecked) // ignore indeterminate state changes
+ return;
+ unwatch();
+ UTXO *utxo = getTreeUtxo(item);
+ if (utxo && utxo->locked)
+ item->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
+ else
+ updateTableUtxos(updateTreeCheckStates({item}, item->checkState(COLUMN_CHECKBOX)));
+ watch();
+ Q_EMIT tableUpdated();
+}
+
+bool BlocknetCoinControl::eventFilter(QObject *obj, QEvent *event) {
+ if (event->type() == QEvent::KeyPress && (obj == table || obj == tree)) {
+ auto *keyEvent = dynamic_cast(event);
+ if (keyEvent->key() == Qt::Key_Space) {
+ if (obj == table && table->selectionModel()->hasSelection()) {
+ auto *select = table->selectionModel();
+ if (select->hasSelection()) {
+ unwatch();
+ QMap utxos;
+ auto idxs = select->selectedRows(COLUMN_CHECKBOX);
+ for (auto & idx : idxs) {
+ auto *item = table->item(idx.row(), idx.column());
+ if (item) {
+ UTXO *utxo = getTableUtxo(item, idx.row());
+ if (utxo && !utxo->locked)
+ utxos[utxo->toString()] = utxo;
+ }
+ }
+ // First check if they're all selected to determine
+ // whether to deselect. Only deselect if all utxos
+ // are selected.
+ bool shouldDeselect{true};
+ for (auto & item : utxos) {
+ if (!item->checked) {
+ shouldDeselect = false;
+ break;
+ }
+ }
+ for (auto & item : utxos)
+ item->checked = !shouldDeselect;
+ // Update list
+ for (auto & idx : idxs) {
+ auto *item = table->item(idx.row(), idx.column());
+ if (item)
+ item->setCheckState(!shouldDeselect ? Qt::Checked : Qt::Unchecked);
+ }
+ // Update tree
+ for (int i = 0; i < tree->topLevelItemCount(); ++i) {
+ auto *topLevelItem = tree->topLevelItem(i);
+ for (int j = 0; j < topLevelItem->childCount(); ++j) {
+ auto *item = topLevelItem->child(j);
+ auto *utxo = getTreeUtxo(item);
+ if (utxo && utxos.count(utxo->toString()))
+ item->setCheckState(COLUMN_CHECKBOX, utxos[utxo->toString()]->checked ? Qt::Checked : Qt::Unchecked);
+ }
+ }
+ watch();
+ table->setFocus(Qt::FocusReason::MouseFocusReason);
+ Q_EMIT tableUpdated();
+ }
+ } else if (obj == tree && !tree->selectedItems().isEmpty()) {
+ auto items = tree->selectedItems();
+ if (!items.empty()) {
+ unwatch();
+ QMap utxos;
+ QList qitems;
+ // Update tree
+ for (auto & item : items) {
+ if (item->childCount() > 0) {
+ for (int i = 0; i < item->childCount(); ++i)
+ qitems.push_back(item->child(i));
+ } else
+ qitems.push_back(item);
+ }
+ for (auto & qitem : qitems) {
+ UTXO *utxo = getTreeUtxo(qitem);
+ if (utxo && !utxo->locked)
+ utxos[utxo->toString()] = utxo;
+ }
+ // First check if they're all selected to determine
+ // whether to deselect. Only deselect if all utxos
+ // are selected.
+ bool shouldDeselect{true};
+ for (auto & item : utxos) {
+ if (!item->checked) {
+ shouldDeselect = false;
+ break;
+ }
+ }
+ for (auto & item : utxos)
+ item->checked = !shouldDeselect;
+ // Update tree
+ for (auto & qitem : qitems)
+ qitem->setCheckState(COLUMN_CHECKBOX, !shouldDeselect ? Qt::Checked : Qt::Unchecked);
+ // Update list
+ for (int row = 0; row < table->rowCount(); row++) {
+ auto *item = table->item(row, COLUMN_TXHASH);
+ auto *utxo = getTableUtxo(item, row);
+ if (utxo && utxos.count(utxo->toString())) {
+ item = table->item(row, COLUMN_CHECKBOX);
+ item->setCheckState(!shouldDeselect ? Qt::Checked : Qt::Unchecked);
+ }
+ }
+ watch();
+ tree->setFocus(Qt::FocusReason::MouseFocusReason);
+ Q_EMIT tableUpdated();
+ }
+ }
+ return true; // done
+ }
+ }
+ return QObject::eventFilter(obj, event);
+}
+
+void BlocknetCoinControl::updateTableUtxos(const QMap & utxos) {
+ for (int row = 0; row < table->rowCount(); row++) {
+ auto *item = table->item(row, COLUMN_TXHASH);
+ auto *utxo = getTableUtxo(item, row);
+ if (utxo && utxos.count(utxo->toString())) {
+ auto *putxo = utxos[utxo->toString()];
+ auto *cbItem = new QTableWidgetItem;
+ if (putxo->locked) {
+ cbItem->setIcon(QIcon(":/redesign/lock_closed_white"));
+ cbItem->setFlags(cbItem->flags() ^ Qt::ItemIsEditable);
+ } else {
+ cbItem->setCheckState(putxo->checked ? Qt::Checked : Qt::Unchecked);
+ cbItem->setFlags(cbItem->flags() | Qt::ItemIsEditable);
+ }
+ table->setItem(row, COLUMN_CHECKBOX, cbItem);
+ }
+ }
+}
+
+void BlocknetCoinControl::updateTreeUtxos(const QMap & utxos) {
+ for (int i = 0; i < tree->topLevelItemCount(); ++i) {
+ auto *topLevelItem = tree->topLevelItem(i);
+ for (int j = 0; j < topLevelItem->childCount(); ++j) {
+ auto *item = topLevelItem->child(j);
+ auto *utxo = getTreeUtxo(item);
+ if (utxo && utxos.count(utxo->toString())) {
+ auto *putxo = utxos[utxo->toString()];
+ if (putxo->locked) {
+ item->setIcon(COLUMN_CHECKBOX, QIcon(":/redesign/lock_closed_white"));
+ item->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
+ item->setFlags(item->flags() ^ Qt::ItemIsEditable);
+ } else {
+ item->setIcon(COLUMN_CHECKBOX, QIcon());
+ item->setCheckState(COLUMN_CHECKBOX, putxo->checked ? Qt::Checked : Qt::Unchecked);
+ item->setFlags(item->flags() | Qt::ItemIsEditable);
+ }
+ }
+ }
}
}
+
+QMap BlocknetCoinControl::updateTableCheckStates(const QList & idxs, Qt::CheckState checkState, const bool *lockState) {
+ QMap utxos;
+ for (auto & idx : idxs) {
+ auto *item = table->item(idx.row(), idx.column());
+ if (item) {
+ UTXO *utxo = getTableUtxo(item, idx.row());
+ if (lockState == nullptr && utxo && !utxo->locked && utxo->isValid()) {
+ utxo->checked = checkState == Qt::Checked;
+ item->setCheckState(checkState);
+ utxos[utxo->toString()] = utxo;
+ } else if (lockState != nullptr && utxo && utxo->isValid()) { // if lock state
+ utxo->locked = *lockState;
+ utxo->unlocked = !utxo->locked;
+ utxo->checked = false;
+ auto *cbItem = new QTableWidgetItem;
+ if (utxo->locked) {
+ cbItem->setIcon(QIcon(":/redesign/lock_closed_white"));
+ cbItem->setFlags(cbItem->flags() ^ Qt::ItemIsEditable);
+ walletModel->wallet().lockCoin({uint256S(utxo->transaction.toStdString()), utxo->vout});
+ } else {
+ cbItem->setCheckState(Qt::Unchecked);
+ cbItem->setFlags(cbItem->flags() | Qt::ItemIsEditable);
+ walletModel->wallet().unlockCoin({uint256S(utxo->transaction.toStdString()), utxo->vout});
+ }
+ table->setItem(idx.row(), COLUMN_CHECKBOX, cbItem);
+ utxos[utxo->toString()] = utxo;
+ }
+ }
+ }
+ return utxos;
+}
+
+QMap BlocknetCoinControl::updateTreeCheckStates(const QList & items, Qt::CheckState checkState, const bool *lockState) {
+ QMap utxos;
+ QList qitems;
+ for (auto & item : items) {
+ if (item->childCount() > 0) {
+ for (int i = 0; i < item->childCount(); ++i)
+ qitems.push_back(item->child(i));
+ } else
+ qitems.push_back(item);
+ }
+ for (auto & qitem : qitems) {
+ UTXO *utxo = getTreeUtxo(qitem);
+ if (lockState == nullptr && utxo && !utxo->locked && utxo->isValid()) { // non-lock state
+ utxo->checked = checkState == Qt::Checked;
+ qitem->setCheckState(COLUMN_CHECKBOX, checkState);
+ utxos[utxo->toString()] = utxo;
+ } else if (lockState != nullptr && utxo && utxo->isValid()) { // if lock state
+ utxo->locked = *lockState;
+ utxo->unlocked = !utxo->locked;
+ utxo->checked = false;
+ if (utxo->locked) {
+ qitem->setIcon(COLUMN_CHECKBOX, QIcon(":/redesign/lock_closed_white"));
+ qitem->setFlags(qitem->flags() ^ Qt::ItemIsEditable);
+ walletModel->wallet().lockCoin({uint256S(utxo->transaction.toStdString()), utxo->vout});
+ } else {
+ qitem->setIcon(COLUMN_CHECKBOX, QIcon());
+ qitem->setFlags(qitem->flags() | Qt::ItemIsEditable);
+ walletModel->wallet().unlockCoin({uint256S(utxo->transaction.toStdString()), utxo->vout});
+ }
+ qitem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
+ utxos[utxo->toString()] = utxo;
+ }
+ }
+ return utxos;
+}
+
+QList BlocknetCoinControl::allTreeItems() {
+ QList r;
+ for (int row = 0; row < tree->topLevelItemCount(); ++row) {
+ auto *topLevelItem = tree->topLevelItem(row);
+ if (!topLevelItem)
+ continue;
+ for (int childIdx = 0; childIdx < topLevelItem->childCount(); ++childIdx) {
+ auto *item = topLevelItem->child(childIdx);
+ if (!item)
+ continue;
+ r.push_back(item);
+ }
+ }
+ return r;
+}
\ No newline at end of file
diff --git a/src/qt/blocknetcoincontrol.h b/src/qt/blocknetcoincontrol.h
index 5b082f7123..866447b4f5 100644
--- a/src/qt/blocknetcoincontrol.h
+++ b/src/qt/blocknetcoincontrol.h
@@ -1,10 +1,11 @@
-// Copyright (c) 2018-2019 The Blocknet developers
+// Copyright (c) 2018-2020 The Blocknet developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BLOCKNET_QT_BLOCKNETCOINCONTROL_H
#define BLOCKNET_QT_BLOCKNETCOINCONTROL_H
+#include
#include
#include
@@ -18,8 +19,11 @@
#include
#include
#include
+#include
+#include
#include
#include
+#include
#include
#include
#include
@@ -27,7 +31,7 @@
class BlocknetCoinControl : public QFrame {
Q_OBJECT
public:
- explicit BlocknetCoinControl(QWidget *parent = nullptr);
+ explicit BlocknetCoinControl(QWidget *parent = nullptr, WalletModel *w = nullptr);
struct UTXO {
bool checked;
@@ -45,6 +49,12 @@ class BlocknetCoinControl : public QFrame {
bool isValid() {
return !transaction.isEmpty();
}
+ static std::string key(QString txhash, uint n) {
+ return QString("%1-%2").arg(txhash, QString::number(n)).toStdString();
+ }
+ std::string toString() {
+ return key(transaction, vout);
+ }
};
struct Model {
double freeThreshold;
@@ -69,6 +79,10 @@ class BlocknetCoinControl : public QFrame {
if (dataModel != nullptr)
table->clearContents();
table->blockSignals(false);
+ tree->blockSignals(true);
+ if (dataModel != nullptr)
+ tree->clear();
+ tree->blockSignals(false);
};
QTableWidget* getTable() {
@@ -86,14 +100,38 @@ public Q_SLOTS:
private Q_SLOTS:
void showContextMenu(QPoint);
void onItemChanged(QTableWidgetItem *item);
+ void onTreeItemChanged(QTreeWidgetItem *item);
private:
+ class BlocknetTableWidget : public QTableWidget {
+ public:
+ QModelIndex itemIndex(QTableWidgetItem *item) {
+ return indexFromItem(item);
+ }
+ };
+
+private:
+ WalletModel *walletModel = nullptr;
QVBoxLayout *layout;
- QTableWidget *table;
+ BlocknetTableWidget *table;
+ QTreeWidget *tree;
+ QRadioButton *listRb;
+ QRadioButton *treeRb;
QMenu *contextMenu;
QTableWidgetItem *contextItem = nullptr;
+ QTreeWidgetItem *contextItemTr = nullptr;
QAction *selectCoins;
QAction *deselectCoins;
+ QAction *selectAllCoins;
+ QAction *deselectAllCoins;
+ QAction *copyAmountAction;
+ QAction *copyLabelAction;
+ QAction *copyAddressAction;
+ QAction *copyTransactionAction;
+ QAction *lockAction;
+ QAction *unlockAction;
+ QAction *expandAll;
+ QAction *collapseAll;
ModelPtr dataModel = nullptr;
@@ -102,17 +140,36 @@ private Q_SLOTS:
void watch();
bool utxoForHash(QString transaction, uint vout, UTXO *&utxo);
QString getTransactionHash(QTableWidgetItem *item);
+ QString getTransactionHash(QTreeWidgetItem *item);
uint getVOut(QTableWidgetItem *item);
+ uint getVOut(QTreeWidgetItem *item);
+ bool treeMode();
+ void showTree(bool yes);
+
+ UTXO* getTableUtxo(QTableWidgetItem *item, int row);
+ UTXO* getTreeUtxo(QTreeWidgetItem *item);
+ void updateTableUtxos(const QMap & utxos);
+ void updateTreeUtxos(const QMap & utxos);
+ QMap updateTableCheckStates(const QList & idxs, Qt::CheckState checkState, const bool *lockState=nullptr);
+ QMap updateTreeCheckStates(const QList & items, Qt::CheckState checkState, const bool *lockState=nullptr);
+ QList allTreeItems();
+
+private:
+ bool eventFilter(QObject *obj, QEvent *event) override;
enum {
- COLUMN_PADDING,
+ COLUMN_PADDING1,
COLUMN_CHECKBOX,
+ COLUMN_PADDING2,
COLUMN_AMOUNT,
+ COLUMN_PADDING3,
COLUMN_LABEL,
+ COLUMN_PADDING4,
COLUMN_ADDRESS,
+ COLUMN_PADDING5,
COLUMN_DATE,
+ COLUMN_PADDING6,
COLUMN_CONFIRMATIONS,
- COLUMN_PRIORITY,
COLUMN_TXHASH,
COLUMN_TXVOUT,
};
@@ -136,6 +193,57 @@ private Q_SLOTS:
return data(PriorityRole).toDouble() < other.data(PriorityRole).toDouble();
};
};
+ class LabelItem : public QTableWidgetItem {
+ public:
+ explicit LabelItem() = default;
+ bool operator < (const QTableWidgetItem & other) const override {
+ auto label = data(Qt::DisplayRole).toString();
+ auto otherlabel = other.data(Qt::DisplayRole).toString();
+ if (label.contains("(") && otherlabel.contains("("))
+ return label.toStdString() < otherlabel.toStdString();
+ if (label.contains("("))
+ return false;
+ if (otherlabel.contains("("))
+ return true;
+ return label.toStdString() < otherlabel.toStdString();
+ };
+ };
+
+ class TreeWidgetItem : public QTreeWidgetItem {
+ public:
+ explicit TreeWidgetItem(QTreeWidgetItem *parent = nullptr) : QTreeWidgetItem(parent) { }
+ bool operator<(const QTreeWidgetItem & other) const {
+ int column = treeWidget()->sortColumn();
+
+ if (column == COLUMN_LABEL) {
+ auto label = data(column, Qt::DisplayRole).toString();
+ auto otherlabel = other.data(column, Qt::DisplayRole).toString();
+ if (label.contains("(") && otherlabel.contains("("))
+ return label.toStdString() < otherlabel.toStdString();
+ if (label.contains("("))
+ return false;
+ if (otherlabel.contains("("))
+ return true;
+ return label.toStdString() < otherlabel.toStdString();
+ }
+
+ if (column == BlocknetCoinControl::COLUMN_AMOUNT || column == BlocknetCoinControl::COLUMN_CONFIRMATIONS)
+ return data(column, Qt::UserRole).toLongLong() < other.data(column, Qt::UserRole).toLongLong();
+ else if (column == BlocknetCoinControl::COLUMN_DATE)
+ return data(column, Qt::UserRole).toDateTime().toMSecsSinceEpoch() < other.data(column, Qt::UserRole).toDateTime().toMSecsSinceEpoch();
+
+ return QTreeWidgetItem::operator<(other);
+ }
+ int64_t camount{0};
+ };
+
+ class TreeDelegate : public QStyledItemDelegate {
+ public:
+ QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const override {
+ auto s = QStyledItemDelegate::sizeHint(option, index);
+ return { s.width(), BGU::spi(25) };
+ }
+ };
};
class BlocknetCoinControlDialog : public QDialog {
@@ -149,8 +257,8 @@ class BlocknetCoinControlDialog : public QDialog {
updateLabels();
}
BlocknetCoinControl* getCC() { return cc; }
- void setPayAmount(CAmount payAmount) {
- this->payAmount = payAmount;
+ void setPayAmount(CAmount amount) {
+ this->payAmount = amount;
}
void populateUnspentTransactions(const QVector & txSelectedUtxos);
diff --git a/src/qt/blocknetdashboard.cpp b/src/qt/blocknetdashboard.cpp
index 422c332ce4..5c6fe2d0a0 100644
--- a/src/qt/blocknetdashboard.cpp
+++ b/src/qt/blocknetdashboard.cpp
@@ -114,26 +114,47 @@ BlocknetDashboard::BlocknetDashboard(QFrame *parent) : QFrame(parent), layout(ne
auto *recentTransactionsGridLayout = new QVBoxLayout;
recentTransactionsGridLayout->setContentsMargins(QMargins());
recentTransactions->setLayout(recentTransactionsGridLayout);
- transactionsTbl = new BlocknetDashboardTable;
- transactionsTbl->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
- transactionsTbl->setEditTriggers(QAbstractItemView::NoEditTriggers);
- transactionsTbl->setSelectionBehavior(QAbstractItemView::SelectRows);
- transactionsTbl->setSelectionMode(QAbstractItemView::NoSelection);
- transactionsTbl->setAlternatingRowColors(true);
- transactionsTbl->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
- transactionsTbl->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
- transactionsTbl->setShowGrid(false);
- transactionsTbl->setFocusPolicy(Qt::NoFocus);
- transactionsTbl->setContextMenuPolicy(Qt::CustomContextMenu);
- transactionsTbl->setSortingEnabled(true);
- transactionsTbl->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
- transactionsTbl->verticalHeader()->setDefaultSectionSize(BGU::spi(60));
- transactionsTbl->verticalHeader()->setVisible(false);
- transactionsTbl->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
- transactionsTbl->horizontalHeader()->setSortIndicatorShown(false);
- transactionsTbl->horizontalHeader()->setSectionsClickable(false);
- transactionsTbl->horizontalHeader()->setVisible(false);
- recentTransactionsGridLayout->addWidget(transactionsTbl, 1);
+ table = new QTableWidget;
+ table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ table->setContentsMargins(QMargins());
+ table->setColumnCount(COLUMN_PADDING4 + 1);
+ table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ table->setSelectionMode(QAbstractItemView::NoSelection);
+ table->setFocusPolicy(Qt::NoFocus);
+ table->setAlternatingRowColors(true);
+ table->setColumnWidth(COLUMN_PADDING1, 1);
+ table->setColumnWidth(COLUMN_PADDING2, 1);
+ table->setColumnWidth(COLUMN_PADDING3, 1);
+ table->setColumnWidth(COLUMN_PADDING4, 1);
+ table->setColumnWidth(COLUMN_STATUS, BGU::spi(3));
+ table->setColumnWidth(COLUMN_DATE, BGU::spi(60));
+ table->setColumnWidth(COLUMN_TIME, BGU::spi(72));
+ table->setShowGrid(false);
+ table->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ table->setContextMenuPolicy(Qt::CustomContextMenu);
+ table->setSortingEnabled(false);
+ table->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
+ table->verticalHeader()->setDefaultSectionSize(BGU::spi(58));
+ table->verticalHeader()->setVisible(false);
+ table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
+ table->horizontalHeader()->setSortIndicatorShown(false);
+ table->horizontalHeader()->setSectionsClickable(false);
+ table->horizontalHeader()->setVisible(false);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING1, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING2, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING3, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_PADDING4, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_STATUS, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_DATE, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_TIME, QHeaderView::Fixed);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_TYPE, QHeaderView::ResizeToContents);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_AMOUNT, QHeaderView::ResizeToContents);
+ table->horizontalHeader()->setSectionResizeMode(COLUMN_TOADDRESS, QHeaderView::Stretch);
+ table->setItemDelegateForColumn(COLUMN_STATUS, new BlocknetDashboardCellItem);
+ table->setItemDelegateForColumn(COLUMN_DATE, new BlocknetDashboardCellItem);
+ recentTransactionsGridLayout->addWidget(table, 1);
layout->addWidget(titleLbl, 0, Qt::AlignTop | Qt::AlignLeft);
layout->addWidget(balanceLbl);
@@ -165,31 +186,105 @@ void BlocknetDashboard::setWalletModel(WalletModel *w) {
displayUnit = walletModel->getOptionsModel()->getDisplayUnit();
balanceChanged(walletModel->wallet().getBalances());
- transactionsTbl->setWalletModel(walletModel);
- transactionsTbl->horizontalHeader()->setSectionResizeMode(BlocknetDashboardFilterProxy::DashboardStatus, QHeaderView::Fixed);
- transactionsTbl->horizontalHeader()->setSectionResizeMode(BlocknetDashboardFilterProxy::DashboardDate, QHeaderView::Fixed);
- transactionsTbl->horizontalHeader()->setSectionResizeMode(BlocknetDashboardFilterProxy::DashboardTime, QHeaderView::Fixed);
- transactionsTbl->horizontalHeader()->setSectionResizeMode(BlocknetDashboardFilterProxy::DashboardType, QHeaderView::ResizeToContents);
- transactionsTbl->horizontalHeader()->setSectionResizeMode(BlocknetDashboardFilterProxy::DashboardAmount, QHeaderView::ResizeToContents);
- transactionsTbl->horizontalHeader()->setSectionResizeMode(BlocknetDashboardFilterProxy::DashboardToAddress, QHeaderView::Stretch);
- transactionsTbl->setColumnWidth(BlocknetDashboardFilterProxy::DashboardStatus, BGU::spi(3));
- transactionsTbl->setColumnWidth(BlocknetDashboardFilterProxy::DashboardDate, BGU::spi(60));
- transactionsTbl->setColumnWidth(BlocknetDashboardFilterProxy::DashboardTime, BGU::spi(72));
+ auto *tableModel = walletModel->getTransactionTableModel();
+ filter = new BlocknetDashboardFilterProxy(walletModel->getOptionsModel(), this);
+ filter->setSourceModel(tableModel);
+ filter->setLimit(30);
+ filter->setDynamicSortFilter(true);
+ filter->setSortRole(Qt::EditRole);
+ filter->setFilterRole(Qt::EditRole);
+ filter->sort(BlocknetDashboardFilterProxy::DashboardDate, Qt::DescendingOrder);
+
+ initialize();
// Watch for wallet changes
walletEvents(true);
+
+ connect(filter, &BlocknetDashboardFilterProxy::dataChanged, this, [this]() {
+ refreshTableData();
+ });
+}
+
+void BlocknetDashboard::initialize() {
+ if (!walletModel)
+ return;
+
+ dataModel.clear();
+
+ // Set up transaction list
+ auto *tableModel = walletModel->getTransactionTableModel();
+ for (int row = 0; row < filter->rowCount(QModelIndex()); ++row) {
+ BlocknetDashboard::DashboardTx tx;
+ updateData(row, tableModel, tx);
+ dataModel << tx;
+ }
+
+ this->setData(dataModel);
+}
+
+void BlocknetDashboard::setData(const QVector & data) {
+ walletEvents(false);
+ table->clearContents();
+ table->setRowCount(data.count());
+
+ for (int i = 0; i < data.count(); ++i) {
+ auto & d = data[i];
+ addData(i, d);
+ }
+
+ walletEvents(true);
+}
+
+void BlocknetDashboard::refreshTableData() {
+ walletEvents(false);
+ // Add new rows
+ const int filterRows = filter->rowCount(QModelIndex());
+ const int tableRows = table->rowCount();
+ if (filterRows > tableRows) {
+ table->setRowCount(filterRows);
+ for (int row = tableRows; row < filterRows; ++row) {
+ BlocknetDashboard::DashboardTx d;
+ addData(row, d);
+ dataModel << d;
+ }
+ }
+ // Update existing rows
+ for (int row = tableRows - 1; row >= 0; --row) {
+ if (filterRows < row) {
+ table->removeRow(row);
+ dataModel.removeAt(row);
+ table->setRowCount(row);
+ continue;
+ }
+ auto & d = dataModel[row];
+ updateData(row, walletModel->getTransactionTableModel(), d);
+ table->item(row, COLUMN_STATUS)->setData(Qt::EditRole, d.status);
+ table->item(row, COLUMN_DATE)->setData(Qt::DisplayRole, d.date);
+ table->item(row, COLUMN_DATE)->setData(Qt::EditRole, d.datetime);
+ table->item(row, COLUMN_TIME)->setData(Qt::DisplayRole, d.time);
+ table->item(row, COLUMN_TOADDRESS)->setData(Qt::DisplayRole, d.address);
+ table->item(row, COLUMN_TYPE)->setData(Qt::DisplayRole, d.type);
+ table->item(row, COLUMN_AMOUNT)->setData(Qt::DisplayRole, d.amount);
+ table->item(row, COLUMN_AMOUNT)->setData(Qt::ForegroundRole, d.amountColor);
+ // tooltip
+ table->item(row, COLUMN_STATUS)->setData(Qt::ToolTipRole, d.tooltip);
+ table->item(row, COLUMN_DATE)->setData(Qt::ToolTipRole, d.tooltip);
+ table->item(row, COLUMN_TIME)->setData(Qt::ToolTipRole, d.tooltip);
+ table->item(row, COLUMN_TOADDRESS)->setData(Qt::ToolTipRole, d.tooltip);
+ table->item(row, COLUMN_TYPE)->setData(Qt::ToolTipRole, d.tooltip);
+ table->item(row, COLUMN_AMOUNT)->setData(Qt::ToolTipRole, d.tooltip);
+ }
+ walletEvents(true);
}
void BlocknetDashboard::showEvent(QShowEvent *event) {
QWidget::showEvent(event);
- walletEvents(true);
- transactionsTbl->enter();
+ refreshTableData(); // turns on wallet events
}
void BlocknetDashboard::hideEvent(QHideEvent *event) {
QWidget::hideEvent(event);
walletEvents(false);
- transactionsTbl->leave();
}
void BlocknetDashboard::balanceChanged(const interfaces::WalletBalances & balances) {
@@ -212,50 +307,90 @@ void BlocknetDashboard::updateBalance() {
}
void BlocknetDashboard::walletEvents(const bool on) {
+ table->blockSignals(true);
if (walletModel && on) {
connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &BlocknetDashboard::displayUnitChanged);
displayUnitChanged(walletModel->getOptionsModel()->getDisplayUnit());
} else if (walletModel) {
disconnect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &BlocknetDashboard::displayUnitChanged);
}
+ table->blockSignals(false);
}
-BlocknetDashboardTable::BlocknetDashboardTable(QWidget *parent) : QTableView(parent),
- walletModel(nullptr) {
-}
-
-void BlocknetDashboardTable::setWalletModel(WalletModel *w) {
- if (walletModel == w)
- return;
- walletModel = w;
-
- if (walletModel == nullptr) {
- setModel(nullptr);
+void BlocknetDashboard::updateData(int row, TransactionTableModel *tableModel, DashboardTx & d) {
+ auto index = filter->index(row, 0);
+ auto sourceIndex = filter->mapToSource(index);
+ auto *rec = static_cast(tableModel->index(sourceIndex.row(), 0).internalPointer());
+ if (!rec)
return;
- }
-
- this->setItemDelegateForColumn(BlocknetDashboardFilterProxy::DashboardStatus, new BlocknetDashboardCellItem(this));
- this->setItemDelegateForColumn(BlocknetDashboardFilterProxy::DashboardDate, new BlocknetDashboardCellItem(this));
- // Set up transaction list
- auto *filter = new BlocknetDashboardFilterProxy(walletModel->getOptionsModel(), this);
- filter->setSourceModel(walletModel->getTransactionTableModel());
- filter->setLimit(30);
- filter->setDynamicSortFilter(true);
- filter->setSortRole(Qt::EditRole);
- filter->setFilterRole(Qt::EditRole);
- filter->sort(BlocknetDashboardFilterProxy::DashboardDate, Qt::DescendingOrder);
- setModel(filter);
+ auto datetime = rec->time;
+ auto date = tableModel->formatTxDate(rec);
+ auto time = QDateTime::fromTime_t(static_cast(rec->time)).toString("h:mmap");
+ auto addr = tableModel->formatTxToAddress(rec, false);
+ if (addr.isEmpty())
+ addr = tr("n/a");
+ auto type = tableModel->formatTxType(rec);
+ auto amt = static_cast(rec->credit + rec->debit);
+ auto amount = BitcoinUnits::floorWithUnit(displayUnit, amt, 6, true, BitcoinUnits::separatorNever);
+ if (!rec->status.countsForBalance)
+ amount = QString("[%1]").arg(amount);
+ QColor amountColor("white");
+ if ((rec->credit + rec->debit) < 0)
+ amountColor = QColor(0xFB, 0x7F, 0x70);
+ else if ((rec->credit + rec->debit) > 0)
+ amountColor = QColor(0x4B, 0xF5, 0xC6);
+ auto tooltip = tableModel->formatTooltip(rec);
+
+ d.outpoint = COutPoint(uint256S(rec->getTxHash().toStdString()), rec->getOutputIndex());
+ d.status = static_cast(rec->status.status);
+ d.datetime = datetime;
+ d.date = date;
+ d.time = time;
+ d.address = addr;
+ d.type = type;
+ d.amount = amount;
+ d.amountColor = amountColor;
+ d.tooltip = tooltip;
}
-void BlocknetDashboardTable::leave() {
- this->blockSignals(true);
- model()->blockSignals(true);
-}
-void BlocknetDashboardTable::enter() {
- this->blockSignals(false);
- model()->blockSignals(false);
- setModel(model());
+void BlocknetDashboard::addData(int row, const DashboardTx & d) {
+ QColor white("white");
+
+ // color indicator
+ auto *colorItem = new QTableWidgetItem;
+ colorItem->setData(Qt::EditRole, d.status);
+ table->setItem(row, COLUMN_STATUS, colorItem);
+
+ // date
+ auto *dateItem = new QTableWidgetItem;
+ dateItem->setData(Qt::DisplayRole, d.date);
+ dateItem->setData(Qt::EditRole, d.datetime);
+ table->setItem(row, COLUMN_DATE, dateItem);
+
+ // time
+ auto *timeItem = new QTableWidgetItem;
+ timeItem->setData(Qt::DisplayRole, d.time);
+ timeItem->setData(Qt::ForegroundRole, white);
+ table->setItem(row, COLUMN_TIME, timeItem);
+
+ // address
+ auto *addressItem = new QTableWidgetItem;
+ addressItem->setData(Qt::DisplayRole, d.address);
+ addressItem->setData(Qt::ForegroundRole, white);
+ table->setItem(row, COLUMN_TOADDRESS, addressItem);
+
+ // type
+ auto *typeItem = new QTableWidgetItem;
+ typeItem->setData(Qt::DisplayRole, d.type);
+ typeItem->setData(Qt::ForegroundRole, white);
+ table->setItem(row, COLUMN_TYPE, typeItem);
+
+ // amount
+ auto *amountItem = new QTableWidgetItem;
+ amountItem->setData(Qt::DisplayRole, d.amount);
+ amountItem->setData(Qt::ForegroundRole, d.amountColor);
+ table->setItem(row, COLUMN_AMOUNT, amountItem);
}
BlocknetDashboardFilterProxy::BlocknetDashboardFilterProxy(OptionsModel *o, QObject *parent) : QSortFilterProxyModel(parent),
@@ -335,6 +470,8 @@ QVariant BlocknetDashboardFilterProxy::data(const QModelIndex &index, int role)
str = QString("[%1]").arg(str);
return str;
}
+ default:
+ return "";
}
break;
case Qt::EditRole: // Edit role is used for sorting, so return the unformatted values
@@ -353,6 +490,8 @@ QVariant BlocknetDashboardFilterProxy::data(const QModelIndex &index, int role)
return model->formatTxToAddress(rec, true);
case DashboardAmount:
return static_cast(rec->credit + rec->debit);
+ default:
+ return "";
}
break;
case Qt::DecorationRole:
@@ -397,9 +536,9 @@ void BlocknetDashboardCellItem::paint(QPainter *painter, const QStyleOptionViewI
painter->save();
switch (index.column()) {
- case BlocknetDashboardFilterProxy::DashboardStatus: {
+ case BlocknetDashboard::COLUMN_STATUS: {
QColor color;
- auto status = static_cast(index.data(Qt::DisplayRole).toInt());
+ auto status = static_cast(index.data(Qt::EditRole).toInt());
switch (status) {
case TransactionStatus::Status::Confirmed:
case TransactionStatus::Status::Confirming:
@@ -423,7 +562,7 @@ void BlocknetDashboardCellItem::paint(QPainter *painter, const QStyleOptionViewI
painter->fillRect(r, color);
break;
}
- case BlocknetDashboardFilterProxy::DashboardDate: {
+ case BlocknetDashboard::COLUMN_DATE: {
auto date = QDateTime::fromTime_t(index.data(Qt::EditRole).toULongLong());
auto month = date.toString("MMM").toUpper();
auto dt = date.toString("dd");
diff --git a/src/qt/blocknetdashboard.h b/src/qt/blocknetdashboard.h
index 9f2f600a23..4036166d82 100644
--- a/src/qt/blocknetdashboard.h
+++ b/src/qt/blocknetdashboard.h
@@ -21,11 +21,12 @@
#include
#include
#include
+#include
#include
#include
#include
-class BlocknetDashboardTable;
+class BlocknetDashboardFilterProxy;
class BlocknetDashboard : public QFrame
{
@@ -35,6 +36,32 @@ class BlocknetDashboard : public QFrame
explicit BlocknetDashboard(QFrame *parent = nullptr);
void setWalletModel(WalletModel *w);
+ enum {
+ COLUMN_STATUS,
+ COLUMN_DATE,
+ COLUMN_TIME,
+ COLUMN_PADDING1,
+ COLUMN_TOADDRESS,
+ COLUMN_PADDING2,
+ COLUMN_TYPE,
+ COLUMN_PADDING3,
+ COLUMN_AMOUNT,
+ COLUMN_PADDING4,
+ };
+
+ struct DashboardTx {
+ COutPoint outpoint;
+ int status;
+ qint64 datetime;
+ QString date;
+ QString time;
+ QString address;
+ QString type;
+ QString amount;
+ QColor amountColor;
+ QString tooltip;
+ };
+
Q_SIGNALS:
void quicksend();
void history();
@@ -51,6 +78,13 @@ public Q_SLOTS:
private Q_SLOTS:
void onViewAll() { Q_EMIT history(); };
+private:
+ void initialize();
+ void setData(const QVector & data);
+ void updateData(int row, TransactionTableModel *tableModel, DashboardTx & d);
+ void addData(int row, const DashboardTx & d);
+ void refreshTableData();
+
private:
QVBoxLayout *layout;
WalletModel *walletModel;
@@ -71,29 +105,14 @@ private Q_SLOTS:
QPushButton *viewAll;
QLabel *recentTxsLbl;
QFrame *recentTransactions;
- BlocknetDashboardTable *transactionsTbl;
+ QTableWidget *table;
+ QVector dataModel;
+ BlocknetDashboardFilterProxy *filter;
void updateBalance();
void walletEvents(bool on);
};
-class BlocknetDashboardTable : public QTableView {
- Q_OBJECT
-
-public:
- explicit BlocknetDashboardTable(QWidget *parent = nullptr);
- void setWalletModel(WalletModel *w);
- void leave();
- void enter();
-
-Q_SIGNALS:
-
-public Q_SLOTS:
-
-private:
- WalletModel *walletModel;
-};
-
class BlocknetDashboardFilterProxy : public QSortFilterProxyModel {
Q_OBJECT
diff --git a/src/qt/blocknetleftmenu.cpp b/src/qt/blocknetleftmenu.cpp
index be4c718edd..5e2b67d747 100644
--- a/src/qt/blocknetleftmenu.cpp
+++ b/src/qt/blocknetleftmenu.cpp
@@ -46,6 +46,10 @@ BlocknetLeftMenu::BlocknetLeftMenu(QFrame *parent) : QFrame(parent), layout(new
addressBook->setIcon(":/redesign/Active/AddressBookIcon.png", ":/redesign/Inactive/AddressBookIcon.png");
addressBook->setLabel(tr("Address Book"));
+ accounts = new BlocknetIconLabel;
+ accounts->setIcon(":/redesign/Active/RequestFundsIcon.png", ":/redesign/Inactive/RequestFundsIcon.png");
+ accounts->setLabel(tr("Balances"));
+
sendFunds = new BlocknetIconLabel;
sendFunds->setIcon(":/redesign/Active/SendFundsIcon.png", ":/redesign/Inactive/SendFundsIcon.png");
sendFunds->setLabel(tr("Send Funds"));
@@ -82,6 +86,7 @@ BlocknetLeftMenu::BlocknetLeftMenu(QFrame *parent) : QFrame(parent), layout(new
group->setExclusive(false);
group->addButton(dashboard, DASHBOARD);
group->addButton(addressBook, ADDRESSBOOK);
+ group->addButton(accounts, ACCOUNTS);
group->addButton(sendFunds, SEND);
// group->addButton(requestFunds, REQUEST);
group->addButton(transactionHistory, HISTORY);
@@ -120,6 +125,7 @@ BlocknetLeftMenu::BlocknetLeftMenu(QFrame *parent) : QFrame(parent), layout(new
box2->layout()->setSpacing(BGU::spi(8));
box2->layout()->addWidget(dashboard);
box2->layout()->addWidget(addressBook);
+ box2->layout()->addWidget(accounts);
auto *box3 = new QFrame;
box3->setContentsMargins(padding);
diff --git a/src/qt/blocknetleftmenu.h b/src/qt/blocknetleftmenu.h
index b5f8dbc51e..a541706e22 100644
--- a/src/qt/blocknetleftmenu.h
+++ b/src/qt/blocknetleftmenu.h
@@ -41,6 +41,7 @@ private Q_SLOTS:
QButtonGroup *group;
BlocknetIconLabel *dashboard;
BlocknetIconLabel *addressBook;
+ BlocknetIconLabel *accounts;
BlocknetIconLabel *sendFunds;
BlocknetIconLabel *requestFunds;
BlocknetIconLabel *transactionHistory;
diff --git a/src/qt/blocknetquicksend.cpp b/src/qt/blocknetquicksend.cpp
index 3cc84534c7..ebfc46063a 100644
--- a/src/qt/blocknetquicksend.cpp
+++ b/src/qt/blocknetquicksend.cpp
@@ -10,6 +10,7 @@
#include
#include
+#include
#include
#include
@@ -221,7 +222,8 @@ void BlocknetQuickSend::openAddressBook() {
void BlocknetQuickSend::onAmountChanged() {
BlocknetSendFundsModel model;
- model.addRecipient(addressTi->text(), BlocknetSendFundsModel::stringToInt(amountTi->text(), displayUnit));
+ const auto addrLabel = walletModel->getAddressTableModel()->labelForAddress(addressTi->text());
+ model.addRecipient(addressTi->text(), BlocknetSendFundsModel::stringToInt(amountTi->text(), displayUnit), addrLabel);
CCoinControl cc = model.getCoinControl(walletModel);
if (!walletModel->wallet().isLocked()) { // if the wallet is unlocked, calculate exact fees
@@ -279,7 +281,8 @@ void BlocknetQuickSend::onSubmit() {
WalletModel::SendCoinsReturn BlocknetQuickSend::submitFunds() {
QList recipients;
- recipients.push_back(SendCoinsRecipient{ addressTi->text(), QString(),
+ const auto addrLabel = walletModel->getAddressTableModel()->labelForAddress(addressTi->text());
+ recipients.push_back(SendCoinsRecipient{ addressTi->text(), addrLabel,
BlocknetSendFundsModel::stringToInt(amountTi->text(), displayUnit),
QString() });
WalletModelTransaction currentTransaction(recipients);
diff --git a/src/qt/blocknetsendfunds1.cpp b/src/qt/blocknetsendfunds1.cpp
index 6f18fe1bac..8f5ac5b1a8 100644
--- a/src/qt/blocknetsendfunds1.cpp
+++ b/src/qt/blocknetsendfunds1.cpp
@@ -8,6 +8,8 @@
#include
#include
+#include
+
#include
#include
@@ -137,7 +139,7 @@ void BlocknetSendFunds1::onAddressesChanged() {
for (const QString &addr : addresses) {
setAddresses.insert(addr);
if (!model->hasRecipient(addr))
- model->addRecipient(addr, 0);
+ model->addRecipient(addr, 0, walletModel->getAddressTableModel()->labelForAddress(addr));
}
// Remove any unspecified addresses
for (const auto & r : model->txRecipients()) {
diff --git a/src/qt/blocknetsendfunds2.cpp b/src/qt/blocknetsendfunds2.cpp
index 70c8ffc538..a67a86b1da 100644
--- a/src/qt/blocknetsendfunds2.cpp
+++ b/src/qt/blocknetsendfunds2.cpp
@@ -621,6 +621,7 @@ void BlocknetSendFunds2::onAmount(QString addr, QString amount) {
SendCoinsRecipient recipient;
recipient.address = addr;
recipient.amount = newAmount;
+ recipient.label = walletModel->getAddressTableModel()->labelForAddress(addr);
model->addRecipient(recipient);
}
diff --git a/src/qt/blocknetsendfundsutil.h b/src/qt/blocknetsendfundsutil.h
index b93793ae00..4f02db2bbc 100644
--- a/src/qt/blocknetsendfundsutil.h
+++ b/src/qt/blocknetsendfundsutil.h
@@ -80,7 +80,7 @@ class BlocknetSendFundsModel {
void addRecipient(const SendCoinsRecipient & recipient) {
txRecipients_.push_back(recipient);
}
- void addRecipient(const QString & address, const CAmount amount, const QString & alias = QString()) {
+ void addRecipient(const QString & address, const CAmount amount, const QString & alias) {
txRecipients_.push_back(SendCoinsRecipient{ address, alias, amount, QString() });
}
bool hasRecipient(const QString & address) {
diff --git a/src/qt/blocknetvars.h b/src/qt/blocknetvars.h
index b4d0b9a31c..1058023d52 100644
--- a/src/qt/blocknetvars.h
+++ b/src/qt/blocknetvars.h
@@ -17,6 +17,7 @@
enum BlocknetPage {
DASHBOARD = 1,
ADDRESSBOOK,
+ ACCOUNTS,
SEND,
QUICKSEND,
REQUEST,
diff --git a/src/qt/blocknetwallet.cpp b/src/qt/blocknetwallet.cpp
index 7d31252c5f..fd8e6b0ab9 100644
--- a/src/qt/blocknetwallet.cpp
+++ b/src/qt/blocknetwallet.cpp
@@ -4,6 +4,7 @@
#include
+#include
#include
#include
#include
@@ -158,9 +159,17 @@ void BlocknetWallet::setPage(BlocknetPage page) {
auto *addressBook = new BlocknetAddressBook;
addressBook->setWalletModel(walletModel);
connect(addressBook, &BlocknetAddressBook::send, this, &BlocknetWallet::onSendToAddress);
+ connect(addressBook, &BlocknetAddressBook::rescan, this, &BlocknetWallet::onRescanRequest);
screen = addressBook;
break;
}
+ case BlocknetPage::ACCOUNTS: {
+ auto *accounts = new BlocknetAccounts;
+ accounts->setWalletModel(walletModel);
+ connect(accounts, &BlocknetAccounts::rescan, this, &BlocknetWallet::onRescanRequest);
+ screen = accounts;
+ break;
+ }
case BlocknetPage::SEND: {
sendFunds->show();
screen = sendFunds;
@@ -247,6 +256,28 @@ void BlocknetWallet::onSendToAddress(const QString &address) {
sendFunds->addAddress(address);
}
+void BlocknetWallet::onRescanRequest(const std::string & walletName) {
+ tg.create_thread([walletName]() {
+ RenameThread("blocknet-rescan");
+ auto ws = GetWallets();
+ CWallet *pwallet = nullptr;
+ for (auto & w : ws) {
+ if (walletName == w->GetName()) {
+ pwallet = w.get();
+ break;
+ }
+ }
+ if (!pwallet)
+ return;
+ WalletRescanReserver reserver(pwallet);
+ if (!reserver.reserve())
+ return;
+ auto locked_chain = pwallet->chain().lock();
+ LOCK(pwallet->cs_wallet);
+ pwallet->RescanFromTime(0, reserver, true);
+ });
+}
+
void BlocknetWallet::goToDashboard() {
setPage(BlocknetPage::DASHBOARD);
}
@@ -419,4 +450,9 @@ void BlocknetWallet::processNewTransaction(const QModelIndex& parent, int start,
Q_EMIT incomingTransaction(date, walletModel->getOptionsModel()->getDisplayUnit(), amount, type, address,
label, walletModel->getWalletName());
+}
+
+BlocknetWallet::~BlocknetWallet() {
+ tg.interrupt_all();
+ tg.join_all();
}
\ No newline at end of file
diff --git a/src/qt/blocknetwallet.h b/src/qt/blocknetwallet.h
index 978e9faa17..a9324b20c8 100644
--- a/src/qt/blocknetwallet.h
+++ b/src/qt/blocknetwallet.h
@@ -23,11 +23,14 @@
#include
#include
+#include
+
class BlocknetWallet : public QFrame {
Q_OBJECT
public:
explicit BlocknetWallet(interfaces::Node & node, const PlatformStyle *platformStyle, QFrame *parent = nullptr);
+ ~BlocknetWallet() override;
void setClientModel(ClientModel *c) { this->clientModel = c; }
@@ -85,6 +88,7 @@ public Q_SLOTS:
protected Q_SLOTS:
void onSendFunds();
void onSendToAddress(const QString &);
+ void onRescanRequest(const std::string &);
void goToDashboard();
void goToQuickSend();
void goToHistory();
@@ -102,6 +106,7 @@ protected Q_SLOTS:
ClientModel *clientModel;
WalletModel *walletModel;
BlocknetPage page;
+ boost::thread_group tg;
BlocknetLeftMenu *leftMenu;
QFrame *contentBox;
diff --git a/src/qt/res/redesign/css/stylesheet.qss b/src/qt/res/redesign/css/stylesheet.qss
index 8602e7111a..6c8083a4d2 100644
--- a/src/qt/res/redesign/css/stylesheet.qss
+++ b/src/qt/res/redesign/css/stylesheet.qss
@@ -37,17 +37,19 @@ QCalendarWidget QAbstractItemView:disabled { color: #666; }
QCalendarWidget QMenu { color: white; background-color: #6F8097; font-size: 10pt; font-family: "Roboto"; }
QCalendarWidget QSpinBox { color: white; background-color: #051937; font-size: 10pt; font-family: "Roboto"; }
QCalendarWidget QToolButton { color: white; background-color: #051937; font-family: "Roboto"; }
-QTableView, QTableWidget { background-color: #172F49; alternate-background-color: #0F2743; border-style: none; border: 0; }
-QTableView::item, QTableWidget::item { font-size: 10pt; font-family: "Roboto"; background-color: #172F49; }
-QTableView::item:alternate, QTableWidget::item:alternate { background-color: #0F2743; }
-QTableView::item::selected, QTableWidget::item::selected { color: white; background-color: #016AFF; }
-QTableView QHeaderView, QTableWidget QHeaderView, QTableWidget QHeaderView::section { background-color: #000D20; border-style: none; border: 0; }
-QTableView QHeaderView::section, QTableWidget QHeaderView::section { font-size: 9pt; color: #6F8097; font-family: "Roboto"; padding: 6px; }
-QTableView QHeaderView::section::selected, QTableWidget QHeaderView::section::selected { color: white; }
-QTableView::indicator:checked, QTableWidget::indicator:checked { image: url(:/redesign/checkmark-on); }
-QTableView::indicator:unchecked, QTableWidget::indicator:unchecked { image: url(:/redesign/checkmark-off); }
-QTableView QHeaderView::down-arrow, QTableView QHeaderView::down-arrow { image: url(:/redesign/HeaderViewArrowDown); }
-QTableView QHeaderView::up-arrow, QTableView QHeaderView::up-arrow { image: url(:/redesign/HeaderViewArrowUp); }
+QTableView, QTableWidget, QTreeWidget { background-color: #172F49; alternate-background-color: #0F2743; border-style: none; border: 0; }
+QTableView::item, QTableWidget::item, QTreeWidget::item { font-size: 10pt; font-family: "Roboto"; background-color: #172F49; }
+QTableView::item:alternate, QTableWidget::item:alternate, QTreeWidget::item:alternate { background-color: #0F2743; }
+QTableView::item::selected, QTableWidget::item::selected, QTreeWidget::item::selected { color: white; background-color: #016AFF; }
+QTableView QHeaderView, QTableWidget QHeaderView, QTableWidget QHeaderView::section, QTreeWidget QHeaderView::section { background-color: #000D20; border-style: none; border: 0; }
+QTableView QHeaderView::section, QTableWidget QHeaderView::section, QTreeWidget QHeaderView::section { font-size: 9pt; color: #6F8097; font-family: "Roboto"; padding: 6px; }
+QTableView QHeaderView::section::selected, QTableWidget QHeaderView::section::selected, QTreeWidget QHeaderView::section::selected { color: white; }
+QTableView::indicator:checked, QTableWidget::indicator:checked, QTreeWidget::indicator:checked { image: url(:/redesign/checkmark-on); }
+QTableView::indicator:unchecked, QTableWidget::indicator:unchecked, QTreeWidget::indicator:unchecked { image: url(:/redesign/checkmark-off); }
+QTableView::indicator:indeterminate, QTableWidget::indicator:indeterminate, QTreeWidget::indicator:indeterminate { image: url(:/redesign/checkmark-partial); }
+QTableView QHeaderView::down-arrow, QTableView QHeaderView::down-arrow, QTreeWidget QHeaderView::down-arrow { image: url(:/redesign/HeaderViewArrowDown); }
+QTableView QHeaderView::up-arrow, QTableView QHeaderView::up-arrow, QTreeWidget QHeaderView::up-arrow { image: url(:/redesign/HeaderViewArrowUp); }
+QTreeWidget::branch { color: #CCC; }
BlocknetLeftMenu { background-color: #051937; }
BlocknetLeftMenu BlocknetIconLabel { border: none; text-align: left; }
@@ -197,6 +199,12 @@ BlocknetAddressBook QTableWidget::item { padding: 3px; }
BlocknetAddressBook QTableWidget::item::selected BlocknetLabelBtn { color: white; }
BlocknetAddressBookDialog { background-color: #172F49; }
+BlocknetAccounts #h4 { padding-left: 1px; }
+BlocknetAccounts #title { font-size: 11pt; color: white; font-family: "Roboto"; }
+BlocknetAccounts QTableWidget { color: white; background-color: transparent; }
+BlocknetAccounts QTableWidget QHeaderView { color: #37475C; }
+BlocknetAccounts QTableWidget::item::selected BlocknetLabelBtn { color: white; }
+
BlocknetAddressEditDialog, BlocknetAddressAddDialog { background-color: #172F49; }
BlocknetAddressEditDialog #h2, BlocknetAddressAddDialog #h2 { background-color: #172F49; border: 0; }
BlocknetAddressEditDialog #readOnly:disabled, BlocknetAddressAddDialog #readOnly:disabled { color: #CCCCCC; border: 1px solid #CCCCCC; }
@@ -215,7 +223,7 @@ BlocknetPeersList QTableView::item { padding: 3px; }
BlocknetPeerDetails { border: 1px solid #37475C; background-color: #051937; }
BlocknetPeerDetails #detailLbl { color: #6F8097; font-size: 12pt; }
-BlocknetCoinControl QTableWidget { color: white; }
+BlocknetCoinControl QTableWidget, BlocknetCoinControl QTreeWidget { color: white; }
#coinControlDialog { background-color: #172F49; }
#coinControlDialog #feePanel { background-color: #172F49; }
#coinControlDialog #feePanel QLabel { font-size: 13pt; color: white; font-family: "Roboto"; }
diff --git a/src/qt/res/redesign/icons/checkmark-partial.png b/src/qt/res/redesign/icons/checkmark-partial.png
new file mode 100644
index 0000000000..9fa0f8c88e
Binary files /dev/null and b/src/qt/res/redesign/icons/checkmark-partial.png differ
diff --git a/src/qt/res/redesign/icons/lock_closed_white.png b/src/qt/res/redesign/icons/lock_closed_white.png
new file mode 100644
index 0000000000..63d219a4ed
Binary files /dev/null and b/src/qt/res/redesign/icons/lock_closed_white.png differ
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
index 6ed999e20a..dfe35fa47d 100644
--- a/src/rpc/client.cpp
+++ b/src/rpc/client.cpp
@@ -226,10 +226,10 @@ class CRPCConvertTable
CRPCConvertTable();
bool convert(const std::string& method, int idx) {
- return (members.count(std::make_pair(method, idx)) > 0);
+ return (members.count(std::make_pair(lowercase(method), idx)) > 0);
}
bool convert(const std::string& method, const std::string& name) {
- return (membersByName.count(std::make_pair(method, name)) > 0);
+ return (membersByName.count(std::make_pair(lowercase(method), name)) > 0);
}
};
@@ -239,9 +239,9 @@ CRPCConvertTable::CRPCConvertTable()
(sizeof(vRPCConvertParams) / sizeof(vRPCConvertParams[0]));
for (unsigned int i = 0; i < n_elem; i++) {
- members.insert(std::make_pair(vRPCConvertParams[i].methodName,
+ members.insert(std::make_pair(lowercase(vRPCConvertParams[i].methodName),
vRPCConvertParams[i].paramIdx));
- membersByName.insert(std::make_pair(vRPCConvertParams[i].methodName,
+ membersByName.insert(std::make_pair(lowercase(vRPCConvertParams[i].methodName),
vRPCConvertParams[i].paramName));
}
}
diff --git a/src/rpc/protocol.cpp b/src/rpc/protocol.cpp
index 23999b305a..7e1647abb4 100644
--- a/src/rpc/protocol.cpp
+++ b/src/rpc/protocol.cpp
@@ -12,6 +12,13 @@
#include
#include
+#include
+
+std::string lowercase(std::string s) {
+ boost::to_lower(s, std::locale::classic());
+ return s;
+}
+
/**
* JSON-RPC protocol. Bitcoin speaks version 1.0 for maximum compatibility,
* but uses JSON-RPC 1.1/2.0 standards for parts of the 1.0 standard that were
diff --git a/src/rpc/protocol.h b/src/rpc/protocol.h
index 6bcbccbd4f..87811720dc 100644
--- a/src/rpc/protocol.h
+++ b/src/rpc/protocol.h
@@ -15,6 +15,8 @@
#include
+std::string lowercase(std::string s);
+
//! HTTP status codes
enum HTTPStatusCode
{
diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
index 17d023b1b5..2e68873569 100644
--- a/src/rpc/server.cpp
+++ b/src/rpc/server.cpp
@@ -362,7 +362,7 @@ CRPCTable::CRPCTable()
const CRPCCommand *CRPCTable::operator[](const std::string &name) const
{
- std::map::const_iterator it = mapCommands.find(name);
+ std::map::const_iterator it = mapCommands.find(lowercase(name));
if (it == mapCommands.end())
return nullptr;
return (*it).second;
@@ -374,11 +374,11 @@ bool CRPCTable::appendCommand(const std::string& name, const CRPCCommand* pcmd)
return false;
// don't allow overwriting for now
- std::map::const_iterator it = mapCommands.find(name);
+ std::map::const_iterator it = mapCommands.find(lowercase(name));
if (it != mapCommands.end())
return false;
- mapCommands[name] = pcmd;
+ mapCommands[lowercase(name)] = pcmd;
return true;
}
diff --git a/src/test/xbridge_tests.cpp b/src/test/xbridge_tests.cpp
new file mode 100644
index 0000000000..5888e7ef28
--- /dev/null
+++ b/src/test/xbridge_tests.cpp
@@ -0,0 +1,133 @@
+// Copyright (c) 2011-2018 The Bitcoin Core developers
+// Copyright (c) 2020 The Blocknet developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#include
+#include
+#include
+
+BOOST_FIXTURE_TEST_SUITE(xbridge_tests, BasicTestingSetup)
+
+BOOST_AUTO_TEST_CASE(xbridge_partialorderdriftcheck) {
+ { // full order should pass drift check
+ CAmount makerSource{11220000}, makerDest{5000000}, takerSource{5000000}, takerDest{11220000};
+ BOOST_CHECK_MESSAGE(xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "exact order should pass drift check");
+ makerSource = 5000000, makerDest = 11220000, takerSource = 11220000, takerDest = 5000000;
+ BOOST_CHECK_MESSAGE(xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "exact order should pass drift check (inverse)");
+ }
+ { // maker source > maker dest drift should pass
+ CAmount makerSource{11220000}, makerDest{100}, takerSource{62}, takerDest{7000000};
+ BOOST_CHECK_MESSAGE(xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "maker source > maker dest should pass");
+ }
+ { // maker source < maker dest drift should pass
+ CAmount makerSource{100}, makerDest{11220000}, takerSource{7000000}, takerDest{62};
+ BOOST_CHECK_MESSAGE(xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "maker source < maker dest should pass");
+ }
+ { // bad taker price (source) should fail upper limit
+ CAmount makerSource{11220000}, makerDest{100}, takerSource{63}, takerDest{7000000};
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "bad taker price (source) should fail upper limit");
+ makerSource = 100, makerDest = 11220000, takerSource = 7000000, takerDest = 63;
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "bad taker price (source) should fail upper limit (inverse)");
+ }
+ { // bad taker price (source) should fail lower limit
+ CAmount makerSource{11220000}, makerDest{100}, takerSource{61}, takerDest{7000000};
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "bad taker price (source) should fail lower limit");
+ makerSource = 100, makerDest = 11220000, takerSource = 7000000, takerDest = 61;
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "bad taker price (source) should fail lower limit (inverse)");
+ }
+ { // bad taker price (dest) should fail upper limit
+ CAmount makerSource{11220000}, makerDest{100}, takerSource{62}, takerDest{7068600+1};
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "bad taker price (dest) should fail upper limit");
+ makerSource = 100, makerDest = 11220000, takerSource = 7068600+1, takerDest = 62;
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "bad taker price (dest) should fail upper limit (inverse)");
+ }
+ { // bad taker price (dest) should fail lower limit
+ CAmount makerSource{11220000}, makerDest{100}, takerSource{62}, takerDest{6844200};
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "bad taker price (dest) should fail lower limit");
+ makerSource = 100, makerDest = 11220000, takerSource = 6844200, takerDest = 62;
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "bad taker price (dest) should fail lower limit (inverse)");
+ }
+ { // taker price should pass upper limit
+ CAmount makerSource{11220000}, makerDest{100}, takerSource{62}, takerDest{7068600};
+ BOOST_CHECK_MESSAGE(xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should pass upper limit");
+ makerSource = 100, makerDest = 11220000, takerSource = 7068600, takerDest = 62;
+ BOOST_CHECK_MESSAGE(xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should pass upper limit (inverse)");
+ }
+ { // taker price should fail lower limit
+ CAmount makerSource{11220000}, makerDest{100}, takerSource{62}, takerDest{6844200};
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should fail lower limit");
+ makerSource = 100, makerDest = 11220000, takerSource = 6844200, takerDest = 62;
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should fail lower limit (inverse)");
+ }
+ { // taker price should pass divisible limits
+ CAmount makerSource{100000}, makerDest{1000}, takerSource{1}, takerDest{100};
+ BOOST_CHECK_MESSAGE(xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should pass divisible limits");
+ makerSource = 1000, makerDest = 100000, takerSource = 100, takerDest = 1;
+ BOOST_CHECK_MESSAGE(xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should pass divisible limits (inverse)");
+ }
+ { // taker price should fail divisible limits
+ CAmount makerSource{100000}, makerDest{1000}, takerSource{10}, takerDest{10000};
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should fail divisible limits");
+ makerSource = 1000, makerDest = 100000, takerSource = 10000, takerDest = 10;
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should fail divisible limits (inverse)");
+ }
+ { // taker price should fail on bad taker size
+ CAmount makerSource{100000}, makerDest{1000}, takerSource{100}, takerDest{990};
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should fail on bad taker size");
+ makerSource = 1000, makerDest = 100000, takerSource = 990, takerDest = 100;
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should fail on bad taker size (inverse)");
+ }
+ { // taker price should fail on bad taker size
+ CAmount makerSource{100000}, makerDest{1000}, takerSource{10}, takerDest{990};
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should fail on bad taker size");
+ makerSource = 1000, makerDest = 100000, takerSource = 990, takerDest = 10;
+ BOOST_CHECK_MESSAGE(!xbridge::xBridgePartialOrderDriftCheck(makerSource, makerDest, takerSource, takerDest), "taker price should fail on bad taker size (inverse)");
+ }
+}
+
+BOOST_AUTO_TEST_CASE(xbridge_pricecheck) {
+ { // exact price conversions should pass
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (10000, 10000, 100000), 100000);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (100000, 10000, 100000), 10000);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (1, 1, 10), 10);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (10, 1, 10), 1);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (62, 62, 100), 100);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (100, 62, 100), 62);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (11220000, 11220000, 62), 62);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (62, 11220000, 62), 11220000);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (99, 99, 199), 199);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (199, 99, 199), 99);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (10999, 10999, 20999), 20999);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (20999, 10999, 20999), 10999);
+ }
+ { // partial price conversions should pass
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (892, 10000, 100000), 8920);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (8920, 10000, 100000), 892);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (1, 5, 10), 2);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (2, 5, 10), 1);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (99, 1, 10), 990);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (990, 1, 10), 99);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (34567, 12345678, 9999), 27);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (27, 12345678, 9999), 33336);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (99999999999, 99, 9), 9090909090);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (9090909090, 99, 9), 99999999990);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeDestAmountFromPrice (999, 999, 999), 999);
+ BOOST_CHECK_EQUAL(xbridge::xBridgeSourceAmountFromPrice (1111, 1111, 1111), 1111);
+ }
+ { // bad price conversions should fail (changes only 2nd column price, all other values same as previous test set)
+ BOOST_CHECK(xbridge::xBridgeDestAmountFromPrice (892, 100000, 100000) != 8920);
+ BOOST_CHECK(xbridge::xBridgeSourceAmountFromPrice (8920, 100000, 100000) != 892);
+ BOOST_CHECK(xbridge::xBridgeDestAmountFromPrice (1, 20, 10) != 2);
+ BOOST_CHECK(xbridge::xBridgeSourceAmountFromPrice (2, 20, 10) != 1);
+ BOOST_CHECK(xbridge::xBridgeDestAmountFromPrice (99, 2, 10) != 990);
+ BOOST_CHECK(xbridge::xBridgeSourceAmountFromPrice (990, 2, 10) != 99);
+ BOOST_CHECK(xbridge::xBridgeDestAmountFromPrice (34567, 123456789, 9999) != 27);
+ BOOST_CHECK(xbridge::xBridgeSourceAmountFromPrice (27, 123456789, 9999) != 33336);
+ BOOST_CHECK(xbridge::xBridgeDestAmountFromPrice (99999999999, 990, 9) != 9090909090);
+ BOOST_CHECK(xbridge::xBridgeSourceAmountFromPrice (9090909090, 990, 9) != 99999999990);
+ BOOST_CHECK(xbridge::xBridgeDestAmountFromPrice (999, 9992, 999) != 999);
+ BOOST_CHECK(xbridge::xBridgeSourceAmountFromPrice (1111, 11112, 1111) != 1111);
+ }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/txdb.cpp b/src/txdb.cpp
index f87ea027e9..7dca209173 100644
--- a/src/txdb.cpp
+++ b/src/txdb.cpp
@@ -340,7 +340,7 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams,
continue;
}
- progress(1, estTotalBlocks, 15);
+ progress(1, estTotalBlocks, 10);
}
});
}
@@ -385,7 +385,7 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams,
pindexNew->hashStakeBlock = diskindex.hashStakeBlock;
pindexNew->hashProofOfStake = diskindex.hashProofOfStake;
- progress(1, estTotalBlocks, 10);
+ progress(1, estTotalBlocks, 40);
}
return true;
};
@@ -411,7 +411,7 @@ bool CBlockTreeDB::LoadBlockIndexGuts(const Consensus::Params& consensusParams,
blocks.reserve(std::min(group, consensusParams.lastCheckpointHeight));
}
}
- progress(1, estTotalBlocks, 45);
+ progress(1, estTotalBlocks, 20);
pcursor->Next();
} else {
return error("%s: failed to read value", __func__);
diff --git a/src/xbridge/rpcxbridge.cpp b/src/xbridge/rpcxbridge.cpp
index f773cb5b27..a6fd636a12 100644
--- a/src/xbridge/rpcxbridge.cpp
+++ b/src/xbridge/rpcxbridge.cpp
@@ -52,6 +52,12 @@ UniValue uret(const json_spirit::Value & o) {
return uv;
}
+std::string parseParentId(const uint256 & parentId) {
+ if (parentId.IsNull())
+ return "";
+ return parentId.GetHex();
+}
+
/**
* @brief TxOutToCurrencyPair inspects a CTxOut and returns currency pair transaction info
* @param tx.vout - transaction outpoints with possible multisig/op_return
@@ -348,7 +354,10 @@ UniValue dxGetOrders(const JSONRPCRequest& request)
"created_at": "2018-01-15T18:15:30.12345Z",
"order_type": "partial",
"partial_minimum": "10.000000",
+ "partial_orig_maker_size": "100.000000",
+ "partial_orig_taker_size": "10.500000",
"partial_repost": false,
+ "partial_parent_id": "",
"status": "open"
},
{
@@ -361,36 +370,47 @@ UniValue dxGetOrders(const JSONRPCRequest& request)
"created_at": "2018-01-15T18:15:30.12345Z",
"order_type": "exact",
"partial_minimum": "0.000000",
+ "partial_orig_maker_size": "0.000000",
+ "partial_orig_taker_size": "0.000000",
"partial_repost": false,
+ "partial_parent_id": "",
"status": "open"
}
]
- Key | Type | Description
- ----------------|------|-----------------------------------------------------
- Array | arr | An array of all orders with each order having the
- | | following parameters.
- id | str | The order ID.
- maker | str | Maker trading asset; the ticker of the asset being
- | | sold by the maker.
- maker_size | str | Maker trading size. String is used to preserve
- | | precision.
- taker | str | Taker trading asset; the ticker of the asset being
- | | sold by the taker.
- taker_size | str | Taker trading size. String is used to preserve
- | | precision.
- updated_at | str | ISO 8601 datetime, with microseconds, of the last
- | | time the order was updated.
- created_at | str | ISO 8601 datetime, with microseconds, of when the
- | | order was created.
- order_type | str | The order type.
- partial_minimum | str | The minimum amount that can be taken. This applies
- | | to `partial` order types and will show `0` on `
- | | exact` order types.
- partial_repost | str | Whether the order will be reposted or not. This
- | | applies to `partial` order types and will show
- | | `false` if you are not the maker of this order.
- status | str | The order status.
+ Key | Type | Description
+ ------------------------|------|---------------------------------------------
+ Array | arr | An array of all orders with each order
+ | | having the following parameters.
+ id | str | The order ID.
+ maker | str | Maker trading asset; the ticker of the asset
+ | | being sold by the maker.
+ maker_size | str | Maker trading size. String is used to
+ | | preserve precision.
+ maker_address | str | Address for sending the outgoing asset.
+ taker | str | Taker trading asset; the ticker of the asset
+ | | being sold by the taker.
+ taker_size | str | Taker trading size. String is used to
+ | | preserve precision.
+ taker_address | str | Address for receiving the incoming asset.
+ updated_at | str | ISO 8601 datetime, with microseconds, of the
+ | | last time the order was updated.
+ created_at | str | ISO 8601 datetime, with microseconds, of
+ | | when the order was created.
+ order_type | str | The order type.
+ partial_minimum* | str | The minimum amount that can be taken.
+ partial_orig_maker_size*| str | The partial order original maker_size.
+ partial_orig_taker_size*| str | The partial order original taker_size.
+ partial_repost | str | Whether the order will be reposted or not.
+ | | This applies to `partial` order types and
+ | | will show `false` for `exact` order types.
+ partial_parent_id | str | The previous order id of a reposted partial
+ | | order. This will return an empty string if
+ | | there is no parent order.
+ status | str | The order status.
+
+ * This only applies to `partial` order types and will show `0` on `exact`
+ order types.
)"
},
RPCExamples{
@@ -438,7 +458,10 @@ UniValue dxGetOrders(const JSONRPCRequest& request)
jtr.emplace_back(Pair("created_at", xbridge::iso8601(tr->created)));
jtr.emplace_back(Pair("order_type", tr->orderType()));
jtr.emplace_back(Pair("partial_minimum", xbridge::xBridgeStringValueFromAmount(tr->minFromAmount)));
+ jtr.emplace_back(Pair("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(tr->origFromAmount)));
+ jtr.emplace_back(Pair("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(tr->origToAmount)));
jtr.emplace_back(Pair("partial_repost", tr->repostOrder));
+ jtr.emplace_back(Pair("partial_parent_id", parseParentId(tr->getParentOrder())));
jtr.emplace_back(Pair("status", tr->strState()));
result.emplace_back(jtr);
@@ -551,7 +574,10 @@ UniValue dxGetOrderFills(const JSONRPCRequest& request)
tmp.emplace_back(Pair("taker_size", xbridge::xBridgeStringValueFromAmount(transaction->toAmount)));
tmp.emplace_back(Pair("order_type", transaction->orderType()));
tmp.emplace_back(Pair("partial_minimum", xbridge::xBridgeStringValueFromAmount(transaction->minFromAmount)));
+ tmp.emplace_back(Pair("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(transaction->origFromAmount)));
+ tmp.emplace_back(Pair("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(transaction->origToAmount)));
tmp.emplace_back(Pair("partial_repost", transaction->repostOrder));
+ tmp.emplace_back(Pair("partial_parent_id", parseParentId(transaction->getParentOrder())));
arr.emplace_back(tmp);
}
@@ -695,33 +721,46 @@ UniValue dxGetOrder(const JSONRPCRequest& request)
"created_at": "2018-01-15T18:15:30.12345Z",
"order_type": "exact",
"partial_minimum": "0.000000",
+ "partial_orig_maker_size": "0.000000",
+ "partial_orig_taker_size": "0.000000",
"partial_repost": false,
+ "partial_parent_id": "",
"status": "open"
}
- Key | Type | Description
- ----------------|------|-----------------------------------------------------
- id | str | The order ID.
- maker | str | Maker trading asset; the ticker of the asset being
- | | sold by the maker.
- maker_size | str | Maker trading size. String is used to preserve
- | | precision.
- taker | str | Taker trading asset; the ticker of the asset being
- | | sold by the taker.
- taker_size | str | Taker trading size. String is used to preserve
- | | precision.
- updated_at | str | ISO 8601 datetime, with microseconds, of the last
- | | time the order was updated.
- created_at | str | ISO 8601 datetime, with microseconds, of when the
- | | order was created.
- order_type | str | The order type.
- partial_minimum | str | The minimum amount that can be taken. This applies
- | | to `partial` order types and will show `0` on
- | | `exact` order types.
- partial_repost | str | Whether the order will be reposted or not. This
- | | applies to `partial` order types and will show
- | | `false` if you are not the maker of this order.
- status | str | The order status.
+ Key | Type | Description
+ ------------------------|------|---------------------------------------------
+ Array | arr | An array of all orders with each order
+ | | having the following parameters.
+ id | str | The order ID.
+ maker | str | Maker trading asset; the ticker of the asset
+ | | being sold by the maker.
+ maker_size | str | Maker trading size. String is used to
+ | | preserve precision.
+ maker_address | str | Address for sending the outgoing asset.
+ taker | str | Taker trading asset; the ticker of the asset
+ | | being sold by the taker.
+ taker_size | str | Taker trading size. String is used to
+ | | preserve precision.
+ taker_address | str | Address for receiving the incoming asset.
+ updated_at | str | ISO 8601 datetime, with microseconds, of the
+ | | last time the order was updated.
+ created_at | str | ISO 8601 datetime, with microseconds, of
+ | | when the order was created.
+ order_type | str | The order type.
+ partial_minimum* | str | The minimum amount that can be taken.
+ partial_orig_maker_size*| str | The partial order original maker_size.
+ partial_orig_taker_size*| str | The partial order original taker_size.
+ partial_repost | str | Whether the order will be reposted or not.
+ | | This applies to `partial` order types and
+ | | will show `false` for `exact` order types.
+ partial_parent_id | str | The previous order id of a reposted partial
+ | | order. This will return an empty string if
+ | | there is no parent order.
+ status | str | The order status.
+
+ * This only applies to `partial` order types and will show `0` on `exact`
+ order types.
)"
},
RPCExamples{
@@ -764,7 +803,10 @@ UniValue dxGetOrder(const JSONRPCRequest& request)
result.emplace_back(Pair("created_at", xbridge::iso8601(order->created)));
result.emplace_back(Pair("order_type", order->orderType()));
result.emplace_back(Pair("partial_minimum", xbridge::xBridgeStringValueFromAmount(order->minFromAmount)));
+ result.emplace_back(Pair("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(order->origFromAmount)));
+ result.emplace_back(Pair("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(order->origToAmount)));
result.emplace_back(Pair("partial_repost", order->repostOrder));
+ result.emplace_back(Pair("partial_parent_id", parseParentId(order->getParentOrder())));
result.emplace_back(Pair("status", order->strState()));
return uret(result);
}
@@ -808,38 +850,46 @@ UniValue dxMakeOrder(const JSONRPCRequest& request)
"block_id": "38729344720548447445023782734923740427863289632489723984723",
"order_type": "exact",
"partial_minimum": "0.000000",
+ "partial_orig_maker_size": "0.000000",
+ "partial_orig_taker_size": "0.000000",
"partial_repost": false,
+ "partial_parent_id": "",
"status": "created"
}
- Key | Type | Description
- ----------------|------|-----------------------------------------------------
- id | str | The order ID. Reposted partial orders are given a
- | | new order ID.
- maker | str | Maker trading asset; the ticker of the asset being
- | | sold by the maker.
- maker_size | str | Maker trading size. String is used to preserve
- | | precision.
- maker_address | str | Maker address for sending the outgoing asset.
- taker | str | Taker trading asset; the ticker of the asset being
- | | sold by the taker.
- taker_size | str | Taker trading size. String is used to preserve
- | | precision.
- taker_address | str | Maker address for receiving the incoming asset.
- updated_at | str | ISO 8601 datetime, with microseconds, of the last
- | | time the order was updated.
- created_at | str | ISO 8601 datetime, with microseconds, of when the
- | | order was created.
- block_id | str | The hash of the current block on the Blocknet
- | | blockchain at the time the order was created.
- order_type | str | The order type.
- partial_minimum | str | The minimum amount that can be taken. This applies
- | | to `partial` order types and will show `0` on
- | | `exact` order types.
- partial_repost | str | Whether the order will be reposted or not. This only
- | | applies to `partial` order types and will always
- | | show `false` if you are not the maker.
- status | str | The order status.
+ Key | Type | Description
+ ------------------------|------|---------------------------------------------
+ Array | arr | An array of all orders with each order
+ | | having the following parameters.
+ id | str | The order ID.
+ maker | str | Maker trading asset; the ticker of the asset
+ | | being sold by the maker.
+ maker_size | str | Maker trading size. String is used to
+ | | preserve precision.
+ maker_address | str | Address for sending the outgoing asset.
+ taker | str | Taker trading asset; the ticker of the asset
+ | | being sold by the taker.
+ taker_size | str | Taker trading size. String is used to
+ | | preserve precision.
+ taker_address | str | Address for receiving the incoming asset.
+ updated_at | str | ISO 8601 datetime, with microseconds, of the
+ | | last time the order was updated.
+ created_at | str | ISO 8601 datetime, with microseconds, of
+ | | when the order was created.
+ order_type | str | The order type.
+ partial_minimum* | str | The minimum amount that can be taken.
+ partial_orig_maker_size*| str | The partial order original maker_size.
+ partial_orig_taker_size*| str | The partial order original taker_size.
+ partial_repost | str | Whether the order will be reposted or not.
+ | | This applies to `partial` order types and
+ | | will show `false` for `exact` order types.
+ partial_parent_id | str | The previous order id of a reposted partial
+ | | order. This will return an empty string if
+ | | there is no parent order.
+ status | str | The order status.
+
+ * This only applies to `partial` order types and will show `0` on `exact`
+ order types.
)"
},
RPCExamples{
@@ -964,7 +1014,10 @@ UniValue dxMakeOrder(const JSONRPCRequest& request)
result.emplace_back(Pair("taker_address", toAddress));
result.emplace_back(Pair("order_type", "exact"));
result.emplace_back(Pair("partial_minimum","0"));
+ result.emplace_back(Pair("partial_orig_maker_size", "0"));
+ result.emplace_back(Pair("partial_orig_taker_size", "0"));
result.emplace_back(Pair("partial_repost", false));
+ result.emplace_back(Pair("partial_parent_id", parseParentId(uint256())));
result.emplace_back(Pair("status", "created"));
return uret(result);
}
@@ -1007,7 +1060,10 @@ UniValue dxMakeOrder(const JSONRPCRequest& request)
obj.emplace_back(Pair("block_id", blockHash.GetHex()));
obj.emplace_back(Pair("order_type", "exact"));
obj.emplace_back(Pair("partial_minimum","0"));
+ obj.emplace_back(Pair("partial_orig_maker_size", "0"));
+ obj.emplace_back(Pair("partial_orig_taker_size", "0"));
obj.emplace_back(Pair("partial_repost", false));
+ obj.emplace_back(Pair("partial_parent_id", parseParentId(uint256())));
obj.emplace_back(Pair("status", "created"));
return uret(obj);
@@ -1121,8 +1177,8 @@ UniValue dxTakeOrder(const JSONRPCRequest& request) {
return uret(xbridge::makeError(xbridge::TRANSACTION_NOT_FOUND, __FUNCTION__));
}
- uint64_t fromSize = txDescr->toAmount;
- uint64_t toSize = txDescr->fromAmount;
+ CAmount fromSize = txDescr->toAmount;
+ CAmount toSize = txDescr->fromAmount;
// If no amount is specified on a partial order by default use the full
// order sizes (will result in the entire partial order being taken).
@@ -1135,8 +1191,8 @@ UniValue dxTakeOrder(const JSONRPCRequest& request) {
xbridge::xBridgeStringValueFromAmount(txDescr->fromAmount)));
}
if (xbridge::xBridgeAmountFromReal(amount) < toSize) {
- fromSize = xbridge::xBridgeAmountFromReal(amount) * xbridge::price(txDescr);
toSize = xbridge::xBridgeAmountFromReal(amount);
+ fromSize = xbridge::xBridgeSourceAmountFromPrice(toSize, txDescr->toAmount, txDescr->fromAmount);
}
} else if (amount > 0) {
WARN() << "partial orders are not allowed for this order " << __FUNCTION__;
@@ -1177,7 +1233,10 @@ UniValue dxTakeOrder(const JSONRPCRequest& request) {
result.emplace_back(Pair("created_at", xbridge::iso8601(txDescr->created)));
result.emplace_back(Pair("order_type", txDescr->orderType()));
result.emplace_back(Pair("partial_minimum", xbridge::xBridgeStringValueFromAmount(txDescr->minFromAmount)));
+ result.emplace_back(Pair("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(txDescr->origFromAmount)));
+ result.emplace_back(Pair("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(txDescr->origToAmount)));
result.emplace_back(Pair("partial_repost", txDescr->repostOrder));
+ result.emplace_back(Pair("partial_parent_id", parseParentId(txDescr->getParentOrder())));
result.emplace_back(Pair("status", "filled"));
return uret(result);
}
@@ -1218,7 +1277,10 @@ UniValue dxTakeOrder(const JSONRPCRequest& request) {
result.emplace_back(Pair("created_at", xbridge::iso8601(txDescr->created)));
result.emplace_back(Pair("order_type", txDescr->orderType()));
result.emplace_back(Pair("partial_minimum", xbridge::xBridgeStringValueFromAmount(txDescr->minFromAmount)));
+ result.emplace_back(Pair("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(txDescr->origFromAmount)));
+ result.emplace_back(Pair("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(txDescr->origToAmount)));
result.emplace_back(Pair("partial_repost", txDescr->repostOrder));
+ result.emplace_back(Pair("partial_parent_id", parseParentId(txDescr->getParentOrder())));
result.emplace_back(Pair("status", txDescr->strState()));
return uret(result);
} else {
@@ -1939,57 +2001,73 @@ UniValue dxGetMyOrders(const JSONRPCRequest& request)
"id": "91d0ea83edc79b9a2041c51d08037cff87c181efb311a095dfdd4edbcc7993a9",
"maker": "SYS",
"maker_size": "100.000000",
+ "maker_address": "SVTbaYZ8olpVn3uNyImst3GKyrvfzXQgdK",
"taker": "LTC",
"taker_size": "10.500000",
+ "taker_address": "LVvFhZroMRGTtg1hHp7jVew3YoZRX8y35Z",
"updated_at": "2018-01-15T18:25:05.12345Z",
"created_at": "2018-01-15T18:15:30.12345Z",
"order_type": "partial",
"partial_minimum": "10.000000",
+ "partial_orig_maker_size": "100.000000",
+ "partial_orig_taker_size": "10.500000",
"partial_repost": true,
+ "partial_parent_id": "",
"status": "open"
},
{
"id": "6be548bc46a3dcc69b6d56529948f7e679dd96657f85f5870a017e005caa050a",
"maker": "SYS",
"maker_size": "4.000000",
+ "maker_address": "SVTbaYZ8olpVn3uNyImst3GKyrvfzXQgdK",
"taker": "LTC",
"taker_size": "0.400000",
+ "taker_address": "LVvFhZroMRGTtg1hHp7jVew3YoZRX8y35Z",
"updated_at": "2018-01-15T18:25:05.12345Z",
"created_at": "2018-01-15T18:15:30.12345Z",
"order_type": "partial",
"partial_minimum": "0.400000",
- "partial_repost": false,
+ "partial_orig_maker_size": "4.000000",
+ "partial_orig_taker_size": "0.400000",
+ "partial_repost": true,
+ "partial_parent_id": "91d0ea83edc79b9a2041c51d08037cff87c181efb311a095dfdd4edbcc7993a9",
"status": "open"
}
]
- Key | Type | Description
- ----------------|------|-----------------------------------------------------
- Array | arr | An array of all orders with each order having the
- | | following parameters.
- id | str | The order ID.
- maker | str | Maker trading asset; the ticker of the asset being
- | | sold by the maker.
- maker_size | str | Maker trading size. String is used to preserve
- | | precision.
- maker_address | str | Address for sending the outgoing asset.
- taker | str | Taker trading asset; the ticker of the asset being
- | | sold by the taker.
- taker_size | str | Taker trading size. String is used to preserve
- | | precision.
- taker_address | str | Address for receiving the incoming asset.
- updated_at | str | ISO 8601 datetime, with microseconds, of the last
- | | time the order was updated.
- created_at | str | ISO 8601 datetime, with microseconds, of when the
- | | order was created.
- order_type | str | The order type.
- partial_minimum | str | The minimum amount that can be taken. This applies
- | | to `partial` order types and will show `0` on
- | | `exact` order types.
- partial_repost | str | Whether the order will be reposted or not. This
- | | applies to `partial` order types and will show
- | | `false` for `exact` order types.
- status | str | The order status.
+ Key | Type | Description
+ ------------------------|------|---------------------------------------------
+ Array | arr | An array of all orders with each order
+ | | having the following parameters.
+ id | str | The order ID.
+ maker | str | Maker trading asset; the ticker of the asset
+ | | being sold by the maker.
+ maker_size | str | Maker trading size. String is used to
+ | | preserve precision.
+ maker_address | str | Address for sending the outgoing asset.
+ taker | str | Taker trading asset; the ticker of the asset
+ | | being sold by the taker.
+ taker_size | str | Taker trading size. String is used to
+ | | preserve precision.
+ taker_address | str | Address for receiving the incoming asset.
+ updated_at | str | ISO 8601 datetime, with microseconds, of the
+ | | last time the order was updated.
+ created_at | str | ISO 8601 datetime, with microseconds, of
+ | | when the order was created.
+ order_type | str | The order type.
+ partial_minimum* | str | The minimum amount that can be taken.
+ partial_orig_maker_size*| str | The partial order original maker_size.
+ partial_orig_taker_size*| str | The partial order original taker_size.
+ partial_repost | str | Whether the order will be reposted or not.
+ | | This applies to `partial` order types and
+ | | will show `false` for `exact` order types.
+ partial_parent_id | str | The previous order id of a reposted partial
+ | | order. This will return an empty string if
+ | | there is no parent order.
+ status | str | The order status.
+
+ * This only applies to `partial` order types and will show `0` on `exact`
+ order types.
)"
},
RPCExamples{
@@ -2083,7 +2161,10 @@ UniValue dxGetMyOrders(const JSONRPCRequest& request)
// partial order details
o.emplace_back(Pair("order_type", t->orderType()));
o.emplace_back(Pair("partial_minimum", xbridge::xBridgeStringValueFromAmount(t->minFromAmount)));
+ o.emplace_back(Pair("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(t->origFromAmount)));
+ o.emplace_back(Pair("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(t->origToAmount)));
o.emplace_back(Pair("partial_repost", t->repostOrder));
+ o.emplace_back(Pair("partial_parent_id", parseParentId(t->getParentOrder())));
o.emplace_back(Pair("status", t->strState()));
r.emplace_back(o);
@@ -2092,6 +2173,311 @@ UniValue dxGetMyOrders(const JSONRPCRequest& request)
return uret(r);
}
+UniValue dxGetMyPartialOrderChain(const JSONRPCRequest& request) {
+ if (request.fHelp || request.params.empty() || request.params.size() > 1)
+ throw std::runtime_error(
+ RPCHelpMan{"dxGetMyPartialOrderChain",
+ "\nReturns a list of all orders related to the specified "
+ "order id. This includes partial orders that were repost "
+ "from a parent order.\n",
+ {
+ {"order_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Order id"},
+ },
+ RPCResult{
+ R"(
+ [
+ {
+ "id": "91d0ea83edc79b9a2041c51d08037cff87c181efb311a095dfdd4edbcc7993a9",
+ "maker": "SYS",
+ "maker_size": "100.000000",
+ "maker_address": "SVTbaYZ8olpVn3uNyImst3GKyrvfzXQgdK",
+ "taker": "LTC",
+ "taker_size": "10.500000",
+ "taker_address": "LVvFhZroMRGTtg1hHp7jVew3YoZRX8y35Z",
+ "updated_at": "2018-01-15T18:25:05.12345Z",
+ "created_at": "2018-01-15T18:15:30.12345Z",
+ "order_type": "partial",
+ "partial_minimum": "10.000000",
+ "partial_orig_maker_size": "100.000000",
+ "partial_orig_taker_size": "10.500000",
+ "partial_repost": true,
+ "partial_parent_id": "",
+ "status": "open"
+ },
+ {
+ "id": "6be548bc46a3dcc69b6d56529948f7e679dd96657f85f5870a017e005caa050a",
+ "maker": "SYS",
+ "maker_size": "4.000000",
+ "maker_address": "SVTbaYZ8olpVn3uNyImst3GKyrvfzXQgdK",
+ "taker": "LTC",
+ "taker_size": "0.400000",
+ "taker_address": "LVvFhZroMRGTtg1hHp7jVew3YoZRX8y35Z",
+ "updated_at": "2018-01-15T18:25:05.12345Z",
+ "created_at": "2018-01-15T18:15:30.12345Z",
+ "order_type": "partial",
+ "partial_minimum": "0.400000",
+ "partial_orig_maker_size": "4.000000",
+ "partial_orig_taker_size": "0.400000",
+ "partial_repost": true,
+ "partial_parent_id": "91d0ea83edc79b9a2041c51d08037cff87c181efb311a095dfdd4edbcc7993a9",
+ "status": "open"
+ }
+ ]
+
+ Key | Type | Description
+ ------------------------|------|---------------------------------------------
+ Array | arr | An array of all orders with each order
+ | | having the following parameters.
+ id | str | The order ID.
+ maker | str | Maker trading asset; the ticker of the asset
+ | | being sold by the maker.
+ maker_size | str | Maker trading size. String is used to
+ | | preserve precision.
+ maker_address | str | Address for sending the outgoing asset.
+ taker | str | Taker trading asset; the ticker of the asset
+ | | being sold by the taker.
+ taker_size | str | Taker trading size. String is used to
+ | | preserve precision.
+ taker_address | str | Address for receiving the incoming asset.
+ updated_at | str | ISO 8601 datetime, with microseconds, of the
+ | | last time the order was updated.
+ created_at | str | ISO 8601 datetime, with microseconds, of
+ | | when the order was created.
+ order_type | str | The order type.
+ partial_minimum* | str | The minimum amount that can be taken.
+ partial_orig_maker_size*| str | The partial order original maker_size.
+ partial_orig_taker_size*| str | The partial order original taker_size.
+ partial_repost | str | Whether the order will be reposted or not.
+ | | This applies to `partial` order types and
+ | | will show `false` for `exact` order types.
+ partial_parent_id | str | The previous order id of a reposted partial
+ | | order. This will return an empty string if
+ | | there is no parent order.
+ status | str | The order status.
+
+ * This only applies to `partial` order types and will show `0` on `exact`
+ order types.
+ )"
+ },
+ RPCExamples{
+ HelpExampleCli("dxGetMyPartialOrderChain", "\"6be548bc46a3dcc69b6d56529948f7e679dd96657f85f5870a017e005caa050a\"")
+ + HelpExampleRpc("dxGetMyPartialOrderChain", "6be548bc46a3dcc69b6d56529948f7e679dd96657f85f5870a017e005caa050a")
+ },
+ }.ToString());
+
+ RPCTypeCheck(request.params, {UniValue::VSTR});
+ const auto orderid = uint256S(request.params[0].get_str());
+ if (orderid.IsNull())
+ return uret(xbridge::makeError(xbridge::INVALID_PARAMETERS, __FUNCTION__, "bad order id"));
+
+ UniValue r(UniValue::VARR);
+
+ xbridge::App & xapp = xbridge::App::instance();
+ auto orderChain = xapp.getPartialOrderChain(orderid);
+
+ std::map seen;
+ for (const auto & t : orderChain) {
+ // do not process already seen orders
+ if (seen.count(t->id.GetHex()))
+ continue;
+ seen[t->id.GetHex()] = true;
+
+ xbridge::WalletConnectorPtr connFrom = xapp.connectorByCurrency(t->fromCurrency);
+ xbridge::WalletConnectorPtr connTo = xapp.connectorByCurrency(t->toCurrency);
+
+ std::string makerAddress;
+ std::string takerAddress;
+ if (connFrom)
+ makerAddress = connFrom->fromXAddr(t->from);
+ if (connTo)
+ takerAddress = connTo->fromXAddr(t->to);
+
+ UniValue o(UniValue::VOBJ);
+ o.pushKV("id", t->id.GetHex());
+
+ // maker data
+ o.pushKV("maker", t->fromCurrency);
+ o.pushKV("maker_size", xbridge::xBridgeStringValueFromAmount(t->fromAmount));
+ o.pushKV("maker_address", makerAddress);
+ // taker data
+ o.pushKV("taker", t->toCurrency);
+ o.pushKV("taker_size", xbridge::xBridgeStringValueFromAmount(t->toAmount));
+ o.pushKV("taker_address", takerAddress);
+ // dates
+ o.pushKV("updated_at", xbridge::iso8601(t->txtime));
+ o.pushKV("created_at", xbridge::iso8601(t->created));
+ // partial order details
+ o.pushKV("order_type", t->orderType());
+ o.pushKV("partial_minimum", xbridge::xBridgeStringValueFromAmount(t->minFromAmount));
+ o.pushKV("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(t->origFromAmount));
+ o.pushKV("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(t->origToAmount));
+ o.pushKV("partial_repost", t->repostOrder);
+ o.pushKV("partial_parent_id", parseParentId(t->getParentOrder()));
+ o.pushKV("status", t->strState());
+
+ r.push_back(o);
+ }
+
+ return r;
+}
+
+UniValue dxPartialOrderChainDetails(const JSONRPCRequest& request) {
+ if (request.fHelp || request.params.empty() || request.params.size() > 1)
+ throw std::runtime_error(
+ RPCHelpMan{"dxPartialOrderChainDetails",
+ "\nReturns detailed information about a partial order "
+ "chain. This includes original amounts, total amount, "
+ "reported amounts sent and received and other information.\n",
+ {
+ {"order_id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Order id"},
+ },
+ RPCResult{
+ R"(
+ {
+ "first_order_id": "0b28e7c7de9a048dd2cb28b7d91062a052d16adf6d1a2154aa99ab2321c29770",
+ "maker": "BLOCK",
+ "maker_address": "y4Fn5z58KFA4qLcktBFCrKc8UHrWnNaVym",
+ "taker": "LTC",
+ "taker_address": "LWvt2ygq8QDkVEcCkMWHR4qXCqL2gC9D2B",
+ "partial_minimum": "0.100000",
+ "partial_orig_maker_size": "0.100000",
+ "partial_orig_taker_size": "0.000100",
+ "first_order_time": "2020-07-23T23:52:05.999Z",
+ "last_order_time": "2020-07-23T23:58:34.604Z",
+ "total_reported_sent": "0.200000",
+ "total_reported_received": "0.000200",
+ "total_reported_notsent": "0.800000",
+ "total_reported_notreceived": "0.000800",
+ "total_orders_open": 0,
+ "total_orders_finished": 2,
+ "total_orders_canceled": 1,
+ "orders": [
+ "0b28e7c7de9a048dd2cb28b7d91062a052d16adf6d1a2154aa99ab2321c29770",
+ "d3afd3b5faf604245a6962214bd0460bec88ff275236480d24b9e5cd45d44c41",
+ "5d4bde2de3d6982ce40da82da3b55803f82e11672b2292c611aec9b54cc4c4c9"
+ ],
+ "p2sh_deposits": [
+ "a3bd9b849696946a06ad90b5e03337dba326400192d5b9b96ce0faf2cb513377",
+ "a29c4d06941877b501d0b5fe6dc054ca177f723e30a987f67a5871df8b14bfa5",
+ ""
+ ],
+ "p2sh_deposits_counterparty": [
+ "41e106c3668d097166cc4a5cce283a9079e769859c4a5467826506fc2547725e",
+ "c2f86465d26b3f90f559e3fe56a4a0aa44ee01e07f1e27b2236f16be92991f25",
+ ""
+ ]
+ }
+
+ Key | Type | Description
+ ---------------------------|------|-----------------------------------------------------
+ first_order_id | str | The order ID.
+ maker | str | Maker trading asset; the ticker of the asset being
+ | | sold by the maker.
+ maker_address | str | Address for sending the outgoing asset.
+ taker | str | Taker trading asset; the ticker of the asset being
+ | | sold by the taker.
+ taker_address | str | Address for receiving the incoming asset.
+ partial_minimum | str | The minimum amount that can be taken. This applies
+ | | to `partial` order types and will show `0` on
+ | | `exact` order types.
+ partial_orig_maker_size | str | The partial order original maker_size.
+ partial_orig_taker_size | str | The partial order original taker_size.
+ first_order_time | str | ISO 8601 datetime, with microseconds, of the last
+ | | time the order was updated.
+ last_order_time | str | ISO 8601 datetime, with microseconds, of when the
+ | | order was created.
+ total_reported_sent | str | Total amount of maker coin sent to traders.
+ total_reported_received | str | Total amount of taker coin received from traders.
+ total_reported_notsent | str | Total amount of maker coin not yet sent to traders.
+ total_reported_notreceived | str | Total amount of taker coin not yet received from traders.
+ total_orders_open | int | Total number of open orders.
+ total_orders_finished | int | Total number of completed orders.
+ total_orders_canceled | int | Total number of canceled orders.
+ orders | arr | All orders in the partial order chain.
+ p2sh_deposits | arr | All p2sh deposit txids sorted by "orders" data (1 for each order)
+ p2sh_deposits_counterparty | arr | All p2sh counterparty deposit txids sorted by "orders" data (1 for each order)
+
+ )"
+ },
+ RPCExamples{
+ HelpExampleCli("dxPartialOrderChainDetails", "\"6be548bc46a3dcc69b6d56529948f7e679dd96657f85f5870a017e005caa050a\"")
+ + HelpExampleRpc("dxPartialOrderChainDetails", "6be548bc46a3dcc69b6d56529948f7e679dd96657f85f5870a017e005caa050a")
+ },
+ }.ToString());
+
+ RPCTypeCheck(request.params, {UniValue::VSTR});
+ const auto orderid = uint256S(request.params[0].get_str());
+ if (orderid.IsNull())
+ return uret(xbridge::makeError(xbridge::INVALID_PARAMETERS, __FUNCTION__, "bad order id"));
+
+ xbridge::App & xapp = xbridge::App::instance();
+ auto orderChain = xapp.getPartialOrderChain(orderid);
+ if (orderChain.empty())
+ return UniValue(UniValue::VOBJ);
+
+ const auto firstOrder = orderChain[0];
+ const auto lastOrder = orderChain[orderChain.size()-1];
+ xbridge::WalletConnectorPtr connFrom = xapp.connectorByCurrency(firstOrder->fromCurrency);
+ xbridge::WalletConnectorPtr connTo = xapp.connectorByCurrency(firstOrder->toCurrency);
+ const auto firstOrderId = firstOrder->id;
+ const auto maker = firstOrder->fromCurrency;
+ const auto taker = firstOrder->toCurrency;
+ const auto makerAddress = connFrom ? connFrom->fromXAddr(firstOrder->from) : "";
+ const auto takerAddress = connTo ? connTo->fromXAddr(firstOrder->to) : "";
+ const auto partialMinimum = xbridge::xBridgeStringValueFromAmount(firstOrder->minFromAmount);
+ const auto makerOrigSize = xbridge::xBridgeStringValueFromAmount(firstOrder->origFromAmount);
+ const auto takerOrigSize = xbridge::xBridgeStringValueFromAmount(firstOrder->origToAmount);
+ const auto firstOrderTime = xbridge::iso8601(firstOrder->created);
+ const auto lastOrderTime = xbridge::iso8601(lastOrder->txtime);
+ int64_t totalSent{0}, totalReceived{0}, totalNotSent{0}, totalNotReceived{0};
+ int totalOpen{0}, totalInProgress{0}, totalFinished{0}, totalCanceled{0};
+ UniValue uvorders(UniValue::VARR);
+ UniValue uvp2sh(UniValue::VARR);
+ UniValue uvp2shcparty(UniValue::VARR);
+ for (const auto & t : orderChain) {
+ if (t->state == xbridge::TransactionDescr::trFinished) {
+ totalSent += t->fromAmount;
+ totalReceived += t->toAmount;
+ ++totalFinished;
+ } else {
+ totalNotSent += t->fromAmount;
+ totalNotReceived += t->toAmount;
+ }
+ if (t->state <= xbridge::TransactionDescr::trPending)
+ ++totalOpen;
+ if (t->state > xbridge::TransactionDescr::trPending && t->state < xbridge::TransactionDescr::trFinished)
+ ++totalInProgress;
+ if (t->state == xbridge::TransactionDescr::trCancelled)
+ ++totalCanceled;
+ uvorders.push_back(t->id.GetHex());
+ uvp2sh.push_back(t->binTxId);
+ uvp2shcparty.push_back(t->oBinTxId);
+ }
+
+ UniValue o(UniValue::VOBJ);
+ o.pushKV("first_order_id", firstOrderId.GetHex());
+ o.pushKV("maker", maker);
+ o.pushKV("maker_address", makerAddress);
+ o.pushKV("taker", taker);
+ o.pushKV("taker_address", takerAddress);
+ o.pushKV("partial_minimum", partialMinimum);
+ o.pushKV("partial_orig_maker_size", makerOrigSize);
+ o.pushKV("partial_orig_taker_size", takerOrigSize);
+ o.pushKV("first_order_time", firstOrderTime);
+ o.pushKV("last_order_time", lastOrderTime);
+ o.pushKV("total_reported_sent", xbridge::xBridgeStringValueFromAmount(totalSent));
+ o.pushKV("total_reported_received", xbridge::xBridgeStringValueFromAmount(totalReceived));
+ o.pushKV("total_reported_notsent", xbridge::xBridgeStringValueFromAmount(totalNotSent));
+ o.pushKV("total_reported_notreceived", xbridge::xBridgeStringValueFromAmount(totalNotReceived));
+ o.pushKV("total_orders_open", totalOpen);
+ o.pushKV("total_orders_finished", totalFinished);
+ o.pushKV("total_orders_canceled", totalCanceled);
+ o.pushKV("orders", uvorders);
+ o.pushKV("p2sh_deposits", uvp2sh);
+ o.pushKV("p2sh_deposits_counterparty", uvp2shcparty);
+ return o;
+}
+
UniValue dxGetTokenBalances(const JSONRPCRequest& request)
{
if (request.fHelp)
@@ -2547,38 +2933,46 @@ UniValue dxMakePartialOrder(const JSONRPCRequest& request)
"block_id": "38729344720548447445023782734923740427863289632489723984723",
"order_type": "partial",
"partial_minimum": "0.200000",
+ "partial_orig_maker_size": "2.000000",
+ "partial_orig_taker_size": "0.200000",
"partial_repost": true,
+ "partial_parent_id": "1faeba06827929f16490c61ba633522158e8d44163c47f735078eac0304c5eb6",
"status": "created"
}
- Key | Type | Description
- ----------------|------|-----------------------------------------------------
- id | str | The order ID. Reposted partial orders are given a
- | | new order ID.
- maker | str | Maker trading asset; the ticker of the asset being
- | | sold by the maker.
- maker_size | str | Maker trading size. String is used to preserve
- | | precision.
- maker_address | str | Maker address for sending the outgoing asset.
- taker | str | Taker trading asset; the ticker of the asset being
- | | sold by the taker.
- taker_size | str | Taker trading size. String is used to preserve
- | | precision.
- taker_address | str | Maker address for receiving the incoming asset.
- updated_at | str | ISO 8601 datetime, with microseconds, of the last
- | | time the order was updated.
- created_at | str | ISO 8601 datetime, with microseconds, of when the
- | | order was created.
- block_id | str | The hash of the current block on the Blocknet
- | | blockchain at the time the order was created.
- order_type | str | The order type.
- partial_minimum | str | The minimum amount that can be taken. This applies
- | | to `partial` order types and will show `0` on
- | | `exact` order types.
- partial_repost | str | Whether the order will be reposted or not. This only
- | | applies to `partial` order types and will always
- | | show `false` if you are not the maker.
- status | str | The order status.
+ Key | Type | Description
+ ------------------------|------|---------------------------------------------
+ Array | arr | An array of all orders with each order
+ | | having the following parameters.
+ id | str | The order ID.
+ maker | str | Maker trading asset; the ticker of the asset
+ | | being sold by the maker.
+ maker_size | str | Maker trading size. String is used to
+ | | preserve precision.
+ maker_address | str | Address for sending the outgoing asset.
+ taker | str | Taker trading asset; the ticker of the asset
+ | | being sold by the taker.
+ taker_size | str | Taker trading size. String is used to
+ | | preserve precision.
+ taker_address | str | Address for receiving the incoming asset.
+ updated_at | str | ISO 8601 datetime, with microseconds, of the
+ | | last time the order was updated.
+ created_at | str | ISO 8601 datetime, with microseconds, of
+ | | when the order was created.
+ order_type | str | The order type.
+ partial_minimum* | str | The minimum amount that can be taken.
+ partial_orig_maker_size*| str | The partial order original maker_size.
+ partial_orig_taker_size*| str | The partial order original taker_size.
+ partial_repost | str | Whether the order will be reposted or not.
+ | | This applies to `partial` order types and
+ | | will show `false` for `exact` order types.
+ partial_parent_id | str | The previous order id of a reposted partial
+ | | order. This will return an empty string if
+ | | there is no parent order.
+ status | str | The order status.
+
+ * This only applies to `partial` order types and will show `0` on `exact`
+ order types.
)"
},
RPCExamples{
@@ -2696,7 +3090,10 @@ UniValue dxMakePartialOrder(const JSONRPCRequest& request)
result.emplace_back(Pair("taker_address", toAddress));
result.emplace_back(Pair("order_type", "partial"));
result.emplace_back(Pair("partial_minimum", xbridge::xBridgeStringValueFromAmount(xbridge::xBridgeAmountFromReal(partialMinimum))));
+ result.emplace_back(Pair("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(xbridge::xBridgeAmountFromReal(fromAmount))));
+ result.emplace_back(Pair("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(xbridge::xBridgeAmountFromReal(toAmount))));
result.emplace_back(Pair("partial_repost", repost));
+ result.emplace_back(Pair("partial_parent_id", parseParentId(uint256())));
result.emplace_back(Pair("status", "created"));
return uret(result);
}
@@ -2742,7 +3139,10 @@ UniValue dxMakePartialOrder(const JSONRPCRequest& request)
obj.emplace_back(Pair("block_id", blockHash.GetHex()));
obj.emplace_back(Pair("order_type", "partial"));
obj.emplace_back(Pair("partial_minimum", xbridge::xBridgeStringValueFromAmount(xbridge::xBridgeAmountFromReal(partialMinimum))));
+ obj.emplace_back(Pair("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(xbridge::xBridgeAmountFromReal(fromAmount))));
+ obj.emplace_back(Pair("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(xbridge::xBridgeAmountFromReal(toAmount))));
obj.emplace_back(Pair("partial_repost", repost));
+ obj.emplace_back(Pair("partial_parent_id", parseParentId(uint256())));
obj.emplace_back(Pair("status", "created"));
return uret(obj);
@@ -2821,10 +3221,10 @@ UniValue dxSplitAddress(const JSONRPCRequest& request)
return uret(xbridge::makeError(xbridge::NO_SESSION, __FUNCTION__, token));
auto utxos = xapp.getAllLockedUtxos(token);
- const auto sa = boost::lexical_cast(splitAmount);
+ const CAmount sa = xbridge::xBridgeIntFromReal(boost::lexical_cast(splitAmount));
std::string txid, rawtx, failReason;
- double totalSplit{0};
- double splitInclFees{0};
+ CAmount totalSplit{0};
+ CAmount splitInclFees{0};
int splitCount{0};
if (!conn->splitUtxos(sa, address, includeFees, utxos, std::set{}, totalSplit, splitInclFees, splitCount, txid, rawtx, failReason))
return uret(xbridge::makeError(xbridge::BAD_REQUEST, __FUNCTION__, failReason));
@@ -2837,10 +3237,10 @@ UniValue dxSplitAddress(const JSONRPCRequest& request)
UniValue r(UniValue::VOBJ);
r.pushKV("token", token);
r.pushKV("include_fees", includeFees);
- r.pushKV("split_amount_requested", splitAmount);
- r.pushKV("split_amount_with_fees", xbridge::xBridgeStringValueFromPrice(splitInclFees, ::COIN));
+ r.pushKV("split_amount_requested", xbridge::xBridgeStringValueFromAmount(sa));
+ r.pushKV("split_amount_with_fees", xbridge::xBridgeStringValueFromAmount(splitInclFees));
r.pushKV("split_utxo_count", splitCount);
- r.pushKV("split_total", xbridge::xBridgeStringValueFromPrice(totalSplit, ::COIN));
+ r.pushKV("split_total", xbridge::xBridgeStringValueFromAmount(totalSplit));
r.pushKV("txid", txid);
r.pushKV("rawtx", showRawTx ? rawtx : "");
return r;
@@ -2935,10 +3335,10 @@ UniValue dxSplitInputs(const JSONRPCRequest& request)
return uret(xbridge::makeError(xbridge::BAD_REQUEST, __FUNCTION__, "Cannot split utxo already in use: " + vout.ToString()));
}
- const auto sa = boost::lexical_cast(splitAmount);
+ const CAmount sa = xbridge::xBridgeIntFromReal(boost::lexical_cast(splitAmount));
std::string txid, rawtx, failReason;
- double totalSplit{0};
- double splitInclFees{0};
+ CAmount totalSplit{0};
+ CAmount splitInclFees{0};
int splitCount{0};
if (!conn->splitUtxos(sa, address, includeFees, excludedUtxos, userUtxos, totalSplit, splitInclFees, splitCount, txid, rawtx, failReason))
return uret(xbridge::makeError(xbridge::BAD_REQUEST, __FUNCTION__, failReason));
@@ -2951,10 +3351,10 @@ UniValue dxSplitInputs(const JSONRPCRequest& request)
UniValue r(UniValue::VOBJ);
r.pushKV("token", token);
r.pushKV("include_fees", includeFees);
- r.pushKV("split_amount_requested", splitAmount);
- r.pushKV("split_amount_with_fees", xbridge::xBridgeStringValueFromPrice(splitInclFees, ::COIN));
+ r.pushKV("split_amount_requested", xbridge::xBridgeStringValueFromAmount(sa));
+ r.pushKV("split_amount_with_fees", xbridge::xBridgeStringValueFromAmount(splitInclFees));
r.pushKV("split_utxo_count", splitCount);
- r.pushKV("split_total", xbridge::xBridgeStringValueFromPrice(totalSplit, ::COIN));
+ r.pushKV("split_total", xbridge::xBridgeStringValueFromAmount(totalSplit));
r.pushKV("txid", txid);
r.pushKV("rawtx", showRawTx ? rawtx : "");
return r;
@@ -3053,30 +3453,32 @@ UniValue dxGetUtxos(const JSONRPCRequest& request)
// clang-format off
static const CRPCCommand commands[] =
-{ // category name actor (function) argNames
- // --------------------- -------------------------- ------------------------- ----------
- { "xbridge", "dxGetOrderFills", &dxGetOrderFills, {} },
- { "xbridge", "dxGetOrders", &dxGetOrders, {} },
- { "xbridge", "dxGetOrder", &dxGetOrder, {} },
- { "xbridge", "dxGetLocalTokens", &dxGetLocalTokens, {} },
- { "xbridge", "dxLoadXBridgeConf", &dxLoadXBridgeConf, {} },
- { "xbridge", "dxGetNewTokenAddress", &dxGetNewTokenAddress, {} },
- { "xbridge", "dxGetNetworkTokens", &dxGetNetworkTokens, {} },
- { "xbridge", "dxMakeOrder", &dxMakeOrder, {} },
- { "xbridge", "dxMakePartialOrder", &dxMakePartialOrder, {} },
- { "xbridge", "dxTakeOrder", &dxTakeOrder, {} },
- { "xbridge", "dxCancelOrder", &dxCancelOrder, {} },
- { "xbridge", "dxGetOrderHistory", &dxGetOrderHistory, {} },
- { "xbridge", "dxGetOrderBook", &dxGetOrderBook, {} },
- { "xbridge", "dxGetTokenBalances", &dxGetTokenBalances, {} },
- { "xbridge", "dxGetMyOrders", &dxGetMyOrders, {} },
- { "xbridge", "dxGetLockedUtxos", &dxGetLockedUtxos, {} },
- { "xbridge", "dxFlushCancelledOrders", &dxFlushCancelledOrders, {} },
- { "xbridge", "gettradingdata", &gettradingdata, {} },
- { "xbridge", "dxGetTradingData", &dxGetTradingData, {} },
- { "xbridge", "dxSplitAddress", &dxSplitAddress, {"token", "splitamount", "address", "include_fees", "show_rawtx", "submit"} },
- { "xbridge", "dxSplitInputs", &dxSplitInputs, {"token", "splitamount", "address", "include_fees", "show_rawtx", "submit", "utxos"} },
- { "xbridge", "dxGetUtxos", &dxGetUtxos, {"token", "include_used"} },
+{ // category name actor (function) argNames
+ // -------------------- ----------------------------- ----------------------------- ----------
+ { "xbridge", "dxGetOrderFills", &dxGetOrderFills, {} },
+ { "xbridge", "dxGetOrders", &dxGetOrders, {} },
+ { "xbridge", "dxGetOrder", &dxGetOrder, {} },
+ { "xbridge", "dxGetLocalTokens", &dxGetLocalTokens, {} },
+ { "xbridge", "dxLoadXBridgeConf", &dxLoadXBridgeConf, {} },
+ { "xbridge", "dxGetNewTokenAddress", &dxGetNewTokenAddress, {} },
+ { "xbridge", "dxGetNetworkTokens", &dxGetNetworkTokens, {} },
+ { "xbridge", "dxMakeOrder", &dxMakeOrder, {} },
+ { "xbridge", "dxMakePartialOrder", &dxMakePartialOrder, {} },
+ { "xbridge", "dxTakeOrder", &dxTakeOrder, {} },
+ { "xbridge", "dxCancelOrder", &dxCancelOrder, {} },
+ { "xbridge", "dxGetOrderHistory", &dxGetOrderHistory, {} },
+ { "xbridge", "dxGetOrderBook", &dxGetOrderBook, {} },
+ { "xbridge", "dxGetTokenBalances", &dxGetTokenBalances, {} },
+ { "xbridge", "dxGetMyOrders", &dxGetMyOrders, {} },
+ { "xbridge", "dxGetMyPartialOrderChain", &dxGetMyPartialOrderChain, {"order_id"} },
+ { "xbridge", "dxPartialOrderChainDetails", &dxPartialOrderChainDetails, {"order_id"} },
+ { "xbridge", "dxGetLockedUtxos", &dxGetLockedUtxos, {} },
+ { "xbridge", "dxFlushCancelledOrders", &dxFlushCancelledOrders, {} },
+ { "xbridge", "gettradingdata", &gettradingdata, {} },
+ { "xbridge", "dxGetTradingData", &dxGetTradingData, {} },
+ { "xbridge", "dxSplitAddress", &dxSplitAddress, {"token", "splitamount", "address", "include_fees", "show_rawtx", "submit"} },
+ { "xbridge", "dxSplitInputs", &dxSplitInputs, {"token", "splitamount", "address", "include_fees", "show_rawtx", "submit", "utxos"} },
+ { "xbridge", "dxGetUtxos", &dxGetUtxos, {"token", "include_used"} },
};
// clang-format on
diff --git a/src/xbridge/util/xutil.cpp b/src/xbridge/util/xutil.cpp
index 4efde4da9c..85215431b7 100644
--- a/src/xbridge/util/xutil.cpp
+++ b/src/xbridge/util/xutil.cpp
@@ -199,7 +199,7 @@ std::string iso8601(const boost::posix_time::ptime &time)
return ss.str();
}
-std::string xBridgeStringValueFromAmount(uint64_t amount)
+std::string xBridgeStringValueFromAmount(CAmount amount)
{
std::stringstream ss;
ss << std::fixed << std::setprecision(xBridgeSignificantDigits(xbridge::TransactionDescr::COIN)) << xBridgeValueFromAmount(amount);
@@ -220,25 +220,24 @@ std::string xBridgeStringValueFromPrice(double price, uint64_t denomination)
return ss.str();
}
-double xBridgeValueFromAmount(uint64_t amount) {
- return boost::numeric_cast(amount) /
- boost::numeric_cast(xbridge::TransactionDescr::COIN);
+double xBridgeValueFromAmount(CAmount amount) {
+ return static_cast(amount)
+ / static_cast(xbridge::TransactionDescr::COIN)
+ + 1.0 / static_cast(::COIN); // round up 1 sat
}
/**
* Does not round, but truncates because a utxo cannot pay if it's rounded up
*/
-int64_t xBridgeIntFromReal(double utxo_amount) {
+CAmount xBridgeIntFromReal(double utxo_amount) {
double d = utxo_amount * boost::numeric_cast(xbridge::TransactionDescr::COIN);
- auto r = static_cast(d);
+ d += 1.0 / static_cast(::COIN); // round up 1 sat
+ auto r = static_cast(d);
return r;
}
-uint64_t xBridgeAmountFromReal(double val)
-{
- double d = val * boost::numeric_cast(xbridge::TransactionDescr::COIN);
- auto r = (int64_t)(d > 0 ? d + 0.5 : d - 0.5);
- return (uint64_t)r;
+CAmount xBridgeAmountFromReal(double val) {
+ return xBridgeIntFromReal(val);
}
bool xBridgeValidCoin(const std::string coin)
@@ -312,6 +311,76 @@ double priceBid(const xbridge::TransactionDescrPtr ptr)
return xBridgeValueFromAmount(ptr->fromAmount) / xBridgeValueFromAmount(ptr->toAmount);
}
+CAmount xBridgeDestAmountFromPrice(const CAmount counterpartySourceAmount, const CAmount sourceAmount, const CAmount destAmount) {
+ static CAmount c = 1000000;
+ const auto csa = counterpartySourceAmount * c;
+ const auto sa = sourceAmount * c;
+ const auto da = destAmount * c;
+ CAmount newDestAmount = csa * (static_cast(da) / static_cast(sa)) + 1;
+ newDestAmount /= c; // normalize
+ if (newDestAmount < 1)
+ return 1;
+ return newDestAmount;
+}
+
+CAmount xBridgeSourceAmountFromPrice(const CAmount counterpartyDestAmount, const CAmount sourceAmount, const CAmount destAmount) {
+ static CAmount c = 1000000;
+ const auto cda = counterpartyDestAmount * c;
+ const auto sa = sourceAmount * c;
+ const auto da = destAmount * c;
+ CAmount newSourceAmount = cda * (static_cast(sa) / static_cast(da)) + 1;
+ newSourceAmount /= c; // normalize
+ if (newSourceAmount < 1)
+ return 1;
+ return newSourceAmount;
+}
+
+bool xBridgePartialOrderDriftCheck(CAmount makerSource, CAmount makerDest, CAmount otherSource, CAmount otherDest) {
+ bool success{true}; // error
+ // Exact order should always succeed
+ if (makerSource == otherDest && makerDest == otherSource)
+ return true;
+ // Taker amounts must agree with maker's asking price. Derive asking amounts from
+ // counterparty provided amounts. By deriving these amounts from the counterparty
+ // we can ensure price integrity.
+ const CAmount checkSourceAmount = xBridgeSourceAmountFromPrice(makerDest, otherDest, otherSource);
+ const CAmount checkDestAmount = xBridgeDestAmountFromPrice(makerSource, otherDest, otherSource);
+ const CAmount checkSourceAmountOther = xBridgeSourceAmountFromPrice(otherDest, makerDest, makerSource);
+ const CAmount checkDestAmountOther = xBridgeDestAmountFromPrice(otherSource, makerDest, makerSource);
+ // Price match check. The type of check changes based on whether there is a remainder when
+ // checking if the maker's total order amounts are divisible by the counterparty's partial
+ // order amounts. If the amounts are divisible then we expect an exact match. If the amounts
+ // are not divisible then we use a drift check on the smallest amount using an upper and
+ // lower bound check. This means the forgiveness could be anywhere from 100 sats to 1000 sats
+ // or more as the partially taken order sizes decrease in size.
+ if (makerSource % otherDest == 0 && makerDest % otherSource == 0) {
+ if (checkSourceAmountOther != otherSource
+ || checkDestAmountOther != otherDest
+ || checkSourceAmount != makerSource
+ || checkDestAmount != makerDest)
+ success = false;
+ } else if (checkSourceAmountOther != otherSource
+ || checkDestAmountOther != otherDest
+ || checkSourceAmount != makerSource
+ || checkDestAmount != makerDest)
+ {
+ const CAmount driftTakerSourceA = xBridgeSourceAmountFromPrice(otherDest + 1, makerDest, makerSource);
+ const CAmount driftTakerSourceB = xBridgeSourceAmountFromPrice(otherDest - 1, makerDest, makerSource);
+ const CAmount driftTakerSourceUpper = driftTakerSourceA > driftTakerSourceB ? driftTakerSourceA : driftTakerSourceB;
+ const CAmount driftTakerSourceLower = driftTakerSourceA < driftTakerSourceB ? driftTakerSourceA : driftTakerSourceB;
+ if (otherSource > driftTakerSourceUpper || otherSource < driftTakerSourceLower)
+ success = false;
+ const CAmount driftTakerDestA = xBridgeDestAmountFromPrice(otherSource + 1, makerDest, makerSource);
+ const CAmount driftTakerDestB = xBridgeDestAmountFromPrice(otherSource - 1, makerDest, makerSource);
+ const CAmount driftTakerDestUpper = driftTakerDestA > driftTakerDestB ? driftTakerDestA : driftTakerDestB;
+ const CAmount driftTakerDestLower = driftTakerDestA < driftTakerDestB ? driftTakerDestA : driftTakerDestB;
+ if (otherDest > driftTakerDestUpper || otherDest < driftTakerDestLower)
+ success = false;
+ }
+
+ return success;
+}
+
json_spirit::Object makeError(const xbridge::Error statusCode, const std::string &function, const std::string &message)
{
Object error;
diff --git a/src/xbridge/util/xutil.h b/src/xbridge/util/xutil.h
index 6491650fa0..bdebd58317 100644
--- a/src/xbridge/util/xutil.h
+++ b/src/xbridge/util/xutil.h
@@ -12,6 +12,7 @@
#include
#include
+#include
#include
#include
@@ -78,12 +79,39 @@ namespace xbridge
constexpr double xBridgeMaxPriceDeviation = 1.0 / 100000000.0;
constexpr int xBridgePartialOrderMaxUtxos = 10;
- double xBridgeValueFromAmount(uint64_t amount);
- int64_t xBridgeIntFromReal(double val);
- uint64_t xBridgeAmountFromReal(double val);
+ double xBridgeValueFromAmount(CAmount amount);
+ CAmount xBridgeIntFromReal(double val);
+ CAmount xBridgeAmountFromReal(double val);
std::string xBridgeStringValueFromPrice(double price);
std::string xBridgeStringValueFromPrice(double price, uint64_t denomination);
- std::string xBridgeStringValueFromAmount(uint64_t amount);
+ std::string xBridgeStringValueFromAmount(CAmount amount);
+
+ /**
+ * Return the counterparty destination amount from maker/taker price.
+ * @param counterpartySourceAmount
+ * @param sourceAmount
+ * @param destAmount
+ * @return
+ */
+ CAmount xBridgeDestAmountFromPrice(const CAmount counterpartySourceAmount, const CAmount sourceAmount, const CAmount destAmount);
+ /**
+ * Return the counterparty source amount from maker/taker price.
+ * @param counterpartyDestAmount
+ * @param sourceAmount
+ * @param destAmount
+ * @return
+ */
+ CAmount xBridgeSourceAmountFromPrice(const CAmount counterpartyDestAmount, const CAmount sourceAmount, const CAmount destAmount);
+
+ /**
+ * Responsible for checking for an acceptable drift in partial orders.
+ * @param makerSource
+ * @param makerDest
+ * @param otherSource
+ * @param otherDest
+ * @return
+ */
+ bool xBridgePartialOrderDriftCheck(CAmount makerSource, CAmount makerDest, CAmount otherSource, CAmount otherDest);
/**
* @brief Returns true if the input precision is supported by xbridge.
diff --git a/src/xbridge/version.h b/src/xbridge/version.h
index c5198fcac4..5540240b08 100644
--- a/src/xbridge/version.h
+++ b/src/xbridge/version.h
@@ -5,7 +5,7 @@
#ifndef BLOCKNET_XBRIDGE_VERSION_H
#define BLOCKNET_XBRIDGE_VERSION_H
-#define XBRIDGE_PROTOCOL_VERSION 54
+#define XBRIDGE_PROTOCOL_VERSION 55
#endif // BLOCKNET_XBRIDGE_VERSION_H
diff --git a/src/xbridge/xbridgeapp.cpp b/src/xbridge/xbridgeapp.cpp
index 45c9224404..bb1f144421 100644
--- a/src/xbridge/xbridgeapp.cpp
+++ b/src/xbridge/xbridgeapp.cpp
@@ -1415,14 +1415,14 @@ void App::moveTransactionToHistory(const uint256 & id)
//******************************************************************************
//******************************************************************************
xbridge::Error App::repostXBridgeTransaction(const std::string from, const std::string fromCurrency,
- const std::string to, const std::string toCurrency, const double makerPrice, const uint64_t minFromAmount,
- const std::vector utxos)
+ const std::string to, const std::string toCurrency, const CAmount makerAmount, const CAmount takerAmount,
+ const uint64_t minFromAmount, const std::vector utxos, const uint256 parentid)
{
double repostAmount{0};
for (const auto & utxo : utxos)
repostAmount += utxo.amount;
- if (utxos.empty() || repostAmount < std::numeric_limits::epsilon())
+ if (utxos.empty() || repostAmount > makerAmount)
return xbridge::Error::INSIFFICIENT_FUNDS; // do not repost an order with insufficient funds
WalletConnectorPtr connFrom = connectorByCurrency(fromCurrency);
@@ -1431,32 +1431,36 @@ xbridge::Error App::repostXBridgeTransaction(const std::string from, const std::
WARN() << "no session for <" << (connFrom ? toCurrency : fromCurrency) << "> " << __FUNCTION__;
return xbridge::Error::NO_SESSION;
}
+ if (connFrom->isDustAmount(repostAmount))
+ return xbridge::Error::DUST;
- auto fromAmount = xBridgeAmountFromReal(repostAmount);
+ auto newRepostAmount = xBridgeAmountFromReal(repostAmount);
const auto fee1 = xBridgeAmountFromReal(connFrom->minTxFee1(1, 3));
const auto fee2 = xBridgeAmountFromReal(connFrom->minTxFee2(1, 1));
- fromAmount -= (fee1 + fee2) * utxos.size();
- auto toAmount = static_cast(static_cast(fromAmount) / makerPrice);
- const bool usePartial = fromAmount > minFromAmount;
+ newRepostAmount -= (fee1 + fee2) * utxos.size();
+ const bool usePartial = newRepostAmount > minFromAmount;
- // Check the params
- const auto statusCode = checkCreateParams(fromCurrency, toCurrency, fromAmount, from);
+ // Calculate new to amount (destination amount).
+ const CAmount toAmount = xBridgeDestAmountFromPrice(newRepostAmount, makerAmount, takerAmount);
+
+ // Check the params (checks for valid amount)
+ const auto statusCode = checkCreateParams(fromCurrency, toCurrency, newRepostAmount, from);
if (statusCode != xbridge::SUCCESS)
return statusCode;
uint256 id, blockHash;
- return sendXBridgeTransaction(from, fromCurrency, fromAmount, to, toCurrency, toAmount, utxos,
- usePartial, usePartial, minFromAmount, id, blockHash);
+ return sendXBridgeTransaction(from, fromCurrency, newRepostAmount, to, toCurrency, toAmount, utxos,
+ usePartial, usePartial, minFromAmount, id, blockHash, parentid);
}
//******************************************************************************
//******************************************************************************
xbridge::Error App::sendXBridgeTransaction(const std::string & from,
const std::string & fromCurrency,
- const uint64_t & fromAmount,
+ const CAmount & fromAmount,
const std::string & to,
const std::string & toCurrency,
- const uint64_t & toAmount,
+ const CAmount & toAmount,
uint256 & id,
uint256 & blockHash)
{
@@ -1467,16 +1471,17 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
//******************************************************************************
xbridge::Error App::sendXBridgeTransaction(const std::string & from,
const std::string & fromCurrency,
- const uint64_t & fromAmount,
+ const CAmount & fromAmount,
const std::string & to,
const std::string & toCurrency,
- const uint64_t & toAmount,
+ const CAmount & toAmount,
const std::vector utxos,
const bool partialOrder,
const bool repostOrder,
- const uint64_t partialMinimum,
+ const CAmount partialMinimum,
uint256 & id,
- uint256 & blockHash)
+ uint256 & blockHash,
+ const uint256 parentid)
{
// search for service node
std::set currencies{fromCurrency, toCurrency};
@@ -1531,27 +1536,22 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
return xbridge::Error::NO_SESSION;
}
- if (connFrom->isDustAmount(static_cast(fromAmount) / TransactionDescr::COIN))
- {
+ if (connFrom->isDustAmount(xBridgeValueFromAmount(fromAmount)))
return xbridge::Error::DUST;
- }
- if (connTo->isDustAmount(static_cast(toAmount) / TransactionDescr::COIN))
- {
+ if (connTo->isDustAmount(xBridgeValueFromAmount(toAmount)))
return xbridge::Error::DUST;
- }
- const auto partialSplitAmount = xBridgeValueFromAmount(partialMinimum);
- if (partialOrder && connFrom->isDustAmount(partialSplitAmount)) {
+ if (partialOrder && connFrom->isDustAmount(xBridgeValueFromAmount(partialMinimum))) {
WARN() << "partial order minimum is dust <" << fromCurrency << "> " << __FUNCTION__;
return xbridge::Error::DUST;
}
int partialUtxosRequiredForMinimum{0};
bool partialRemainderRequired{false};
- double partialVoutsTotal{0};
- double partialPerUtxoFees{0};
- double partialRemainderVoutTotal{0};
+ CAmount partialVoutsTotal{0};
+ CAmount partialPerUtxoFees{0};
+ CAmount partialRemainderVoutTotal{0};
bool partialRemainderIsDust{false};
int partialOrderVouts{0};
bool partialExactUtxoMatch{false};
@@ -1566,21 +1566,20 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
// Estimated fees if taker were to take the minimum order
// (i.e. if 1 utxo were to be used to fulfill the partial order)
- double partialFee1 = connFrom->minTxFee1(1, 3);
- double partialFee2 = connFrom->minTxFee2(1, 1);
+ CAmount partialFee1 = xBridgeIntFromReal(connFrom->minTxFee1(1, 3));
+ CAmount partialFee2 = xBridgeIntFromReal(connFrom->minTxFee2(1, 1));
partialPerUtxoFees = partialFee1 + partialFee2;
- double partialFees = (partialUtxosRequiredForMinimum + (partialRemainderRequired ? 1 : 0)) * partialPerUtxoFees;
+ CAmount partialFees = (partialUtxosRequiredForMinimum + (partialRemainderRequired ? 1 : 0)) * partialPerUtxoFees;
- const auto fromAmountDecimal = xBridgeValueFromAmount(fromAmount);
- auto partialSplitVoutsTotal = static_cast(partialUtxosRequiredForMinimum) * partialSplitAmount;
- if (fromAmountDecimal + std::numeric_limits::epsilon() - partialSplitVoutsTotal < 0) {
+ CAmount partialSplitVoutsTotal = partialUtxosRequiredForMinimum * partialMinimum;
+ if (fromAmount - partialSplitVoutsTotal < 0) {
WARN() << "insufficient funds for partial order <" << fromCurrency << "> " << __FUNCTION__;
return xbridge::Error::INSIFFICIENT_FUNDS;
}
- partialRemainderVoutTotal = fromAmountDecimal - partialSplitVoutsTotal;
+ partialRemainderVoutTotal = fromAmount - partialSplitVoutsTotal;
if (partialRemainderVoutTotal < 0)
partialRemainderVoutTotal = 0;
- partialRemainderIsDust = connFrom->isDustAmount(partialRemainderVoutTotal + partialPerUtxoFees);
+ partialRemainderIsDust = connFrom->isDustAmount(xBridgeValueFromAmount(partialRemainderVoutTotal + partialPerUtxoFees));
partialVoutsTotal = partialFees + partialSplitVoutsTotal + (partialRemainderRequired && !partialRemainderIsDust ? partialRemainderVoutTotal : 0);
partialOrderVouts = partialUtxosRequiredForMinimum + (partialRemainderRequired && !partialRemainderIsDust ? 1 : 0);
}
@@ -1600,17 +1599,13 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
connFrom->getUnspent(outputs, excludedUtxos);
if (partialOrder) {
- const auto fee1 = connFrom->minTxFee1(1, 3);
- const auto fee2 = connFrom->minTxFee2(1, 1);
- const auto prepTxFee = connFrom->minTxFee1(10, partialOrderVouts + 1);
- const auto perUtxoFee = fee1 + fee2;
- const auto requiredAmount = xBridgeValueFromAmount(fromAmount);
- double utxoAmount{0};
- double fees{0};
+ const CAmount prepTxFee = xBridgeIntFromReal(connFrom->minTxFee1(10, partialOrderVouts + 1));
+ CAmount utxoAmount{0};
+ CAmount fees{0};
// Select utxos
- if (!selectPartialUtxos(from, outputs, requiredAmount, partialUtxosRequiredForMinimum, perUtxoFee, prepTxFee,
- partialSplitAmount, outputsForUse, utxoAmount, fees, partialExactUtxoMatch)) {
+ if (!selectPartialUtxos(from, outputs, fromAmount, partialUtxosRequiredForMinimum, partialPerUtxoFees, prepTxFee,
+ partialMinimum, partialRemainderVoutTotal, outputsForUse, utxoAmount, fees, partialExactUtxoMatch)) {
WARN() << "partial order insufficient funds for <" << fromCurrency << "> " << __FUNCTION__;
return xbridge::Error::INSIFFICIENT_FUNDS;
}
@@ -1618,9 +1613,9 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
{
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("currency", from);
- log_obj.pushKV("partial_fees", fees);
- log_obj.pushKV("utxos_amount", utxoAmount);
- log_obj.pushKV("required_amount", requiredAmount + fees);
+ log_obj.pushKV("partial_fees", xBridgeValueFromAmount(fees));
+ log_obj.pushKV("utxos_amount", xBridgeValueFromAmount(utxoAmount));
+ log_obj.pushKV("required_amount", xBridgeValueFromAmount(fromAmount + fees));
log_obj.pushKV("utxo_count", (int)outputsForUse.size());
xbridge::LogOrderMsg(log_obj, "partial order utxo selection details for order", __FUNCTION__);
}
@@ -1737,7 +1732,8 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
<< ptr->blockHash
<< outputsForUse.at(0).signature;
id = ss.GetHash();
- ptr->id = id;
+ ptr->id = id; // overwritten by partial order designation below
+ ptr->setParentOrder(parentid);
// Create partial order utxos
if (partialOrder) {
@@ -1759,12 +1755,12 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
std::vector existingUtxos;
double vinsTotal{0};
std::vector vins;
- for (const auto &vin : ptr->usedCoins) {
+ for (const auto & vin : ptr->usedCoins) {
// If we already have exact utxos, skip consuming those and subtract from expected total
- if (xBridgeAmountFromReal(vin.amount) == xBridgeAmountFromReal(partialSplitAmount + partialPerUtxoFees)) {
+ if (vin.camount() == partialMinimum + partialPerUtxoFees && partialUtxosRequiredForMinimum > 0) {
existingUtxos.push_back(vin);
partialUtxosRequiredForMinimum--;
- partialVoutsTotal -= partialSplitAmount + partialPerUtxoFees;
+ partialVoutsTotal -= partialMinimum + partialPerUtxoFees;
continue;
}
vinsTotal += vin.amount;
@@ -1773,19 +1769,19 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
std::vector> vouts;
for (int i = 0; i < partialUtxosRequiredForMinimum; ++i)
- vouts.emplace_back(ptr->fromAddr, partialSplitAmount + partialPerUtxoFees);
+ vouts.emplace_back(ptr->fromAddr, xBridgeValueFromAmount(partialMinimum + partialPerUtxoFees));
// add remainder vout if not dust
if (partialRemainderRequired && !partialRemainderIsDust)
- vouts.emplace_back(ptr->fromAddr, partialRemainderVoutTotal + partialPerUtxoFees);
+ vouts.emplace_back(ptr->fromAddr, xBridgeValueFromAmount(partialRemainderVoutTotal + partialPerUtxoFees));
// Change
- const auto changeAmount = vinsTotal - partialVoutsTotal - connFrom->minTxFee1(vins.size(), vouts.size()+1); // vouts + 1 for change
- if (changeAmount < 0) {
+ const double changeAmount = vinsTotal - xBridgeValueFromAmount(partialVoutsTotal) - connFrom->minTxFee1(vins.size(), vouts.size()+1); // vouts + 1 for change
+ if (changeAmount < std::numeric_limits::epsilon()) {
unlockCoins(ptr->fromCurrency, ptr->usedCoins);
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", "unknown");
+ log_obj.pushKV("change_amount", xBridgeStringValueFromPrice(changeAmount, connFrom->COIN));
log_obj.pushKV("from_currency", connFrom->currency);
- xbridge::LogOrderMsg(log_obj, "failed to create order, insufficient funds on partial order",
- __FUNCTION__);
+ xbridge::LogOrderMsg(log_obj, "failed to create order, insufficient funds on partial order", __FUNCTION__);
return xbridge::Error::INVALID_AMOUNT;
}
if (!connFrom->isDustAmount(changeAmount))
@@ -1797,6 +1793,7 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", "unknown");
log_obj.pushKV("from_currency", connFrom->currency);
+ log_obj.pushKV("partial_prep_tx", rawtx);
xbridge::LogOrderMsg(log_obj, "failed to create order, cannot create partial order utxos",
__FUNCTION__);
return xbridge::Error::UNKNOWN_ERROR;
@@ -1809,6 +1806,7 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", "unknown");
log_obj.pushKV("from_currency", connFrom->currency);
+ log_obj.pushKV("partial_prep_tx", rawtx);
xbridge::LogOrderMsg(log_obj, "failed to create order, cannot submit partial order utxos transaction",
__FUNCTION__);
return xbridge::Error::UNKNOWN_ERROR;
@@ -1820,7 +1818,7 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
ptr->clearUsedCoins();
ptr->usedCoins = existingUtxos; // add existing utxos
- double partialNewTotalUtxosAmount{0};
+ CAmount partialNewTotalUtxosAmount{0};
for (int i = 0; i < vouts.size(); ++i) {
xbridge::wallet::UtxoEntry entry;
entry.txId = txid;
@@ -1828,8 +1826,8 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
entry.amount = vouts[i].second;
entry.address = connFrom->fromXAddr(connFrom->toXAddr(vouts[i].first));
ptr->usedCoins.push_back(entry);
- partialNewTotalUtxosAmount += entry.amount;
- if (xBridgeIntFromReal(partialVoutsTotal - partialNewTotalUtxosAmount) <= 0)
+ partialNewTotalUtxosAmount += entry.camount();
+ if (partialVoutsTotal - partialNewTotalUtxosAmount <= 0)
break; // only need enough utxos to cover partial order (use 1 sat for rounding errors)
}
@@ -1920,6 +1918,8 @@ xbridge::Error App::sendXBridgeTransaction(const std::string & from,
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", id.GetHex());
log_obj.pushKV("snode_pubkey", HexStr(pmn.getSnodePubKey()));
+ if (!ptr->partialOrderPrepTx.empty())
+ log_obj.pushKV("partial_prep_tx", ptr->partialOrderPrepTx);
xbridge::LogOrderMsg(log_obj, "using servicenode for order", __FUNCTION__);
}
@@ -3004,31 +3004,38 @@ bool App::selectUtxos(const std::string &addr, const std::vector & outputs,
- const double requiredAmount, const int requiredUtxoCount, const double requiredFeePerUtxo,
- const double requiredPrepTxFees, const double requiredSplitSize, std::vector & outputsForUse,
- double & utxoAmount, double & fees, bool & exactUtxoMatch) const
+ const CAmount requiredAmount, const int requiredUtxoCount, const CAmount requiredFeePerUtxo,
+ const CAmount requiredPrepTxFees, const CAmount requiredSplitSize, const CAmount requiredRemainder,
+ std::vector & outputsForUse, CAmount & utxoAmount, CAmount & fees, bool & exactUtxoMatch) const
{
- utxoAmount = 0.;
- fees = 0.;
+ utxoAmount = 0;
+ fees = 0;
exactUtxoMatch = false;
std::vector utxos(outputs.begin(), outputs.end()); // copy
- double totalAmountNeeded = requiredAmount + fees;
+ CAmount totalAmountNeeded = requiredAmount + fees;
+ CAmount totalExactSplitSizeNeeded = (requiredSplitSize + requiredFeePerUtxo) * requiredUtxoCount;
// Find all ideal utxos (i.e. those matching split size and fees)
- const auto requiredSplitSizeAmt = xBridgeIntFromReal(requiredSplitSize + requiredFeePerUtxo);
+ const CAmount requiredSplitSizeAmt = requiredSplitSize + requiredFeePerUtxo;
for (auto it = utxos.begin(); it != utxos.end(); ) {
auto & utxo = *it;
- if (xBridgeIntFromReal(utxo.amount) == requiredSplitSizeAmt) {
- totalAmountNeeded = requiredAmount + fees;
- if (xBridgeIntFromReal(totalAmountNeeded - utxoAmount) <= 0)
- break; // Stop searching when we've reached the required amount
- utxoAmount += utxo.amount;
+ if (utxo.camount() == requiredSplitSizeAmt && utxoAmount < totalExactSplitSizeNeeded) {
+ utxoAmount += utxo.camount();
+ fees += requiredFeePerUtxo;
+ outputsForUse.push_back(utxo);
+ it = utxos.erase(it); // remove selected utxo
+ continue;
+ }
+ if (requiredRemainder > 0 && utxo.camount() == requiredRemainder) {
+ utxoAmount += utxo.camount();
fees += requiredFeePerUtxo;
outputsForUse.push_back(utxo);
it = utxos.erase(it); // remove selected utxo
continue;
}
+ if (totalAmountNeeded <= utxoAmount)
+ break;
++it;
}
@@ -3037,7 +3044,7 @@ bool App::selectPartialUtxos(const std::string & addr, const std::vector 0 && outputsForUse.size() == requiredUtxoCount + 1)) && totalAmountNeeded - utxoAmount <= 0) {
exactUtxoMatch = true;
return true;
}
@@ -3045,15 +3052,15 @@ bool App::selectPartialUtxos(const std::string & addr, const std::vector= requiredUtxoCount && xBridgeIntFromReal(totalAmountNeeded - utxoAmount) <= 0) {
+ if (outputsForUse.size() >= requiredUtxoCount && totalAmountNeeded - utxoAmount <= 0) {
exactUtxoMatch = true;
return true;
}
@@ -3075,14 +3082,14 @@ bool App::selectPartialUtxos(const std::string & addr, const std::vector= xBridgeIntFromReal(requiredSplitSize + requiredFeePerUtxo)) {
- utxoAmount += utxo.amount;
+ if (utxo.camount() >= requiredSplitSize + requiredFeePerUtxo) {
+ utxoAmount += utxo.camount();
fees += requiredFeePerUtxo;
outputsForUse.push_back(utxo);
it = utxos.erase(it); // remove selected utxo
@@ -3100,8 +3107,8 @@ bool App::selectPartialUtxos(const std::string & addr, const std::vector= xBridgeIntFromReal(totalAmountNeeded)) {
- utxoAmount += utxo.amount;
+ if (utxo.camount() + utxoAmount >= totalAmountNeeded) {
+ utxoAmount += utxo.camount();
outputsForUse.push_back(utxo);
it = utxos.erase(it); // remove selected utxo
break;
@@ -3115,19 +3122,19 @@ bool App::selectPartialUtxos(const std::string & addr, const std::vector b.amount;
+ return a.camount() > b.camount();
});
for (auto it = utxos.begin(); it != utxos.end(); ) {
auto & utxo = *it;
- if (xBridgeIntFromReal(totalAmountNeeded - utxoAmount) <= 0)
+ if (totalAmountNeeded - utxoAmount <= 0)
break; // Stop searching when we've reached the required amount
- utxoAmount += utxo.amount;
+ utxoAmount += utxo.camount();
outputsForUse.push_back(utxo);
it = utxos.erase(it); // remove selected utxo
}
}
- if (outputsForUse.empty() || xBridgeIntFromReal(utxoAmount - totalAmountNeeded) <= 0)
+ if (outputsForUse.empty() || utxoAmount - totalAmountNeeded <= 0)
return false;
return true;
@@ -3796,6 +3803,86 @@ uint256 App::orderWithUtxo(const wallet::UtxoEntry & utxo) {
return uint256();
}
+std::vector App::getPartialOrderChain(const uint256 orderid) {
+ xbridge::TransactionDescrPtr rorder = nullptr;
+ std::vector orders;
+
+ // Filter local partial orders
+ TransactionMap trList = transactions();
+ for (const auto & i : trList) {
+ const auto & t = i.second;
+ // Associate exact orders that are child orders of partial orders
+ if (!t->isLocal() || (t->getParentOrder().IsNull() && !t->isPartialOrderAllowed()))
+ continue;
+ if (t->id == orderid)
+ rorder = t;
+ orders.push_back(t);
+ }
+
+ // Add historical partial orders
+ TransactionMap histOrders = history();
+ for (auto & item : histOrders) {
+ const auto & t = item.second;
+ // Associate exact orders that are child orders of partial orders
+ if (!t->isLocal() || (t->getParentOrder().IsNull() && !t->isPartialOrderAllowed()))
+ continue;
+ if (t->id == orderid)
+ rorder = t;
+ orders.push_back(t);
+ }
+
+ if (orders.empty() || !rorder) // If no orders then return
+ return {};
+
+ // sort ascending by utxo sizes
+ std::sort(orders.begin(), orders.end(),
+ [](const xbridge::TransactionDescrPtr & a, const xbridge::TransactionDescrPtr & b) {
+ return (a->utxoCount()) < (b->utxoCount());
+ });
+
+ // Find the partial order chain:
+ // 1) Find all partial order children
+ // 2) Find all partial order parents
+ std::vector orderChain{rorder};
+ for (int i = 0; i < orders.size(); ++i) {
+ auto & t = orders[i];
+ if (t == rorder) {
+ // Search orders with fewer utxos than user specified order id
+ auto currentid = t->id;
+ auto currentptr = t;
+ for (int j = i-1; j >= 0; --j) {
+ auto & ptrChild = orders[j];
+ if (ptrChild->getParentOrder() == currentid) {
+ orderChain.insert(orderChain.begin(), ptrChild);
+ if (ptrChild->utxoCount() < currentptr->utxoCount())
+ currentid = currentptr->id;
+ }
+ }
+ // Search orders with more utxos than user specified order id
+ currentid = t->id;
+ currentptr = t;
+ for (int k = i+1; k < (int)orders.size(); ++k) {
+ auto & ptrParent = orders[k];
+ if (currentptr->getParentOrder() == ptrParent->id) {
+ orderChain.insert(orderChain.end(), ptrParent);
+ if (ptrParent->utxoCount() > currentptr->utxoCount())
+ currentptr = ptrParent;
+ }
+ }
+ break;
+ }
+ }
+
+ // Sort ascending by created time so that the first order in the chain
+ // is displayed first.
+ std::sort(orderChain.begin(), orderChain.end(),
+ [](const xbridge::TransactionDescrPtr & a, const xbridge::TransactionDescrPtr & b) {
+ return a->created < b->created;
+ });
+
+ return std::move(orderChain);
+}
+
std::ostream & operator << (std::ostream& out, const TransactionDescrPtr& tx)
{
UniValue log_obj(UniValue::VOBJ);
@@ -3836,6 +3923,9 @@ std::ostream & operator << (std::ostream& out, const TransactionDescrPtr& tx)
log_obj.pushKV("partial_allowed", tx->isPartialOrderAllowed());
log_obj.pushKV("partial_repost", tx->isPartialRepost());
log_obj.pushKV("partial_minimum", xbridge::xBridgeStringValueFromAmount(tx->minFromAmount));
+ log_obj.pushKV("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(tx->origFromAmount));
+ log_obj.pushKV("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(tx->origToAmount));
+ log_obj.pushKV("partial_parent_id", tx->getParentOrder().GetHex());
log_obj.pushKV("state", tx->strState());
log_obj.pushKV("block_hash", tx->blockHash.GetHex());
log_obj.pushKV("updated_at", iso8601(tx->txtime));
diff --git a/src/xbridge/xbridgeapp.h b/src/xbridge/xbridgeapp.h
index fbd75318fd..da9c23b2c2 100644
--- a/src/xbridge/xbridgeapp.h
+++ b/src/xbridge/xbridgeapp.h
@@ -251,16 +251,17 @@ class App
*/
Error sendXBridgeTransaction(const std::string & from,
const std::string & fromCurrency,
- const uint64_t & fromAmount,
+ const CAmount & fromAmount,
const std::string & to,
const std::string & toCurrency,
- const uint64_t & toAmount,
+ const CAmount & toAmount,
const std::vector utxos,
bool partialOrder,
bool repostOrder,
- uint64_t partialMinimum,
+ CAmount partialMinimum,
uint256 & id,
- uint256 & blockHash);
+ uint256 & blockHash,
+ const uint256 parentid=uint256());
/**
* @brief sendXBridgeTransaction - create new xbridge transaction and send to network
@@ -276,10 +277,10 @@ class App
*/
Error sendXBridgeTransaction(const std::string & from,
const std::string & fromCurrency,
- const uint64_t & fromAmount,
+ const CAmount & fromAmount,
const std::string & to,
const std::string & toCurrency,
- const uint64_t & toAmount,
+ const CAmount & toAmount,
uint256 & id,
uint256 & blockHash);
@@ -290,13 +291,16 @@ class App
* @param fromCurrency - source currency
* @param to - destionation amount
* @param toCurrency - destionation currency
- * @param makerPrice - use this price (priced in maker_size/taker_size) to determine taker size
+ * @param makerAmount - maker's from amount
+ * @param takerAmount - maker's to amount
* @param minFromAmount - the minimum size that can be taken from maker
* @param utxos - use these unspent transaction outputs (implies fees will be subtracted from this total)
+ * @param parentid - Parent order id
* @return xbridge::SUCCESS if success, else error code
*/
Error repostXBridgeTransaction(std::string from, std::string fromCurrency, std::string to, std::string toCurrency,
- double makerPrice, uint64_t minFromAmount, const std::vector utxos);
+ CAmount makerAmount, CAmount takerAmount, uint64_t minFromAmount, const std::vector utxos,
+ const uint256 parentid=uint256());
// TODO make protected
/**
@@ -665,6 +669,7 @@ class App
* @param requiredUtxoCount number of utxos required
* @param requiredFeePerUtxo fees per utxo required
* @param requiredSplitSize size of each utxo not including fee
+ * @param requiredRemainder size of the last utxo, the remainder
* @param requiredPrepTxFees fees required to submit partial order prep tx
* @param outputsForUse selected utxos
* @param utxoAmount total amount of selected utxos
@@ -673,9 +678,9 @@ class App
* @return
*/
bool selectPartialUtxos(const std::string & addr, const std::vector & outputs,
- const double requiredAmount, const int requiredUtxoCount, const double requiredFeePerUtxo,
- const double requiredPrepTxFees, const double requiredSplitSize, std::vector & outputsForUse,
- double & utxoAmount, double & fees, bool & exactUtxoMatch) const;
+ const CAmount requiredAmount, const int requiredUtxoCount, const CAmount requiredFeePerUtxo,
+ const CAmount requiredPrepTxFees, const CAmount requiredSplitSize, const CAmount requiredRemainder,
+ std::vector & outputsForUse, CAmount & utxoAmount, CAmount & fees, bool & exactUtxoMatch) const;
/**
* Unit tests: add xwallets
@@ -715,6 +720,13 @@ class App
*/
uint256 orderWithUtxo(const wallet::UtxoEntry & utxo);
+ /**
+ * Returns the partial order chain for the specified order.
+ * @param orderid
+ * @return
+ */
+ std::vector getPartialOrderChain(uint256 orderid);
+
protected:
void clearMempool();
diff --git a/src/xbridge/xbridgesession.cpp b/src/xbridge/xbridgesession.cpp
index 0c0226f6ce..15ba4bc684 100644
--- a/src/xbridge/xbridgesession.cpp
+++ b/src/xbridge/xbridgesession.cpp
@@ -1234,6 +1234,7 @@ bool Session::Impl::processTransactionAccepting(XBridgePacketPtr packet) const
// if trJoined = send hold to client
TransactionPtr tr = e.transaction(id);
if (!tr->matches(id)) { // ignore no matching orders
+ trPending->setAccepting(false);
xbridge::LogOrderMsg(id.GetHex(), "rejecting taker order request, no order found with id", __FUNCTION__);
sendRejectTransaction(id, crNotAccepted);
return true;
@@ -1241,6 +1242,7 @@ bool Session::Impl::processTransactionAccepting(XBridgePacketPtr packet) const
if (tr->state() != xbridge::Transaction::trJoined)
{
+ trPending->setAccepting(false);
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", tr->id().GetHex());
log_obj.pushKV("state", tr->state());
@@ -1256,6 +1258,7 @@ bool Session::Impl::processTransactionAccepting(XBridgePacketPtr packet) const
std::string errstr;
const TransactionError err = BroadcastTransaction(feeTxRef, txhash, errstr, highfee);
if (TransactionError::OK != err) {
+ trPending->setAccepting(false);
UniValue o(UniValue::VOBJ);
o.pushKV("orderid", id.GetHex());
o.pushKV("fee_tx", HexStr(feeTx));
@@ -1285,6 +1288,9 @@ bool Session::Impl::processTransactionAccepting(XBridgePacketPtr packet) const
reply1->sign(e.pubKey(), e.privKey());
sendPacketBroadcast(reply1);
+ } else {
+ trPending->setAccepting(false);
+ sendRejectTransaction(id, crNotAccepted);
}
}
@@ -1298,6 +1304,8 @@ bool Session::Impl::processTransactionAccepting(XBridgePacketPtr packet) const
log_obj.pushKV("to_currency", dcurrency);
log_obj.pushKV("to_amount", xbridge::xBridgeStringValueFromAmount(damount));
log_obj.pushKV("partial_minimum", xbridge::xBridgeStringValueFromAmount(trPending->min_partial_amount()));
+ log_obj.pushKV("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(trPending->a_initial_amount()));
+ log_obj.pushKV("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(trPending->b_initial_amount()));
xbridge::LogOrderMsg(log_obj, "accepting taker order request", __FUNCTION__);
}
@@ -1385,9 +1393,8 @@ bool Session::Impl::processTransactionHold(XBridgePacketPtr packet) const
xbridge::LogOrderMsg(id.GetHex(), "using service node " + HexStr(pksnode) + " for order", __FUNCTION__);
- double makerPrice{0};
- double takerPrice{0};
-
+ double makerPrice{0}, takerPrice{0};
+ bool failPriceCheck{false};
// Taker check that amounts match
if (xtx->role == 'B') {
makerPrice = xBridgeValueFromAmount(xtx->toAmount) / xBridgeValueFromAmount(xtx->fromAmount);
@@ -1407,6 +1414,9 @@ bool Session::Impl::processTransactionHold(XBridgePacketPtr packet) const
xbridge::LogOrderMsg(log_obj, "taker to amount from snode should match expected amount", __FUNCTION__);
return true;
}
+ // Taker's source currency is maker's dest currency. That is why
+ // toAmount and fromAmount's are flipped below
+ failPriceCheck = !xBridgePartialOrderDriftCheck(xtx->origFromAmount, xtx->origToAmount, xtx->fromAmount, xtx->toAmount);
} else if (xtx->role == 'A') { // Handle taker checks
makerPrice = xBridgeValueFromAmount(xtx->fromAmount) / xBridgeValueFromAmount(xtx->toAmount);
takerPrice = xBridgeValueFromAmount(damount) / xBridgeValueFromAmount(samount);
@@ -1441,12 +1451,12 @@ bool Session::Impl::processTransactionHold(XBridgePacketPtr packet) const
return true;
}
}
+ // Maker's source currency is taker's dest currency.
+ failPriceCheck = !xBridgePartialOrderDriftCheck(xtx->fromAmount, xtx->toAmount, samount, damount);
}
- // Taker amounts must agree with maker's asking price
- // If maker or taker prices are 0, abort
- // If the difference between maker and taker price is larger than max deviation, abort
- if (makerPrice == 0 || takerPrice == 0 || fabs(makerPrice - takerPrice) > xBridgeMaxPriceDeviation) {
+ // Price match check
+ if (failPriceCheck) {
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", id.GetHex());
log_obj.pushKV("received_price", xbridge::xBridgeStringValueFromPrice(takerPrice));
@@ -1948,16 +1958,19 @@ bool Session::Impl::processTransactionCreateA(XBridgePacketPtr packet) const
return true;
}
- double outAmount = xBridgeValueFromAmount(xtx->fromAmount);
+ const double outAmount = xBridgeValueFromAmount(xtx->fromAmount);
+ const CAmount coutAmount = xtx->fromAmount;
- double fee1 = 0;
- double fee2 = connFrom->minTxFee2(1, 1);
- double inAmount = 0;
+ double inAmount = 0;
+ CAmount cfee1{0};
+ CAmount cfee2 = xBridgeIntFromReal(connFrom->minTxFee2(1, 1));
+ CAmount cinAmount = xBridgeIntFromReal(inAmount);
+ CAmount coutAmountPlusFees{0};
std::vector usedInTx;
for (auto it = xtx->usedCoins.begin(); it != xtx->usedCoins.end(); ) {
// if we have enough utxos, skip
- if (inAmount >= outAmount+fee1+fee2) {
+ if (inAmount >= xBridgeValueFromAmount(coutAmountPlusFees)) {
if (!xtx->isPartialOrderAllowed())
break; // if not partial order, done
// If this is a partial order store unused utxos for eventual repost
@@ -1971,17 +1984,22 @@ bool Session::Impl::processTransactionCreateA(XBridgePacketPtr packet) const
}
usedInTx.push_back(*it);
inAmount += it->amount;
- fee1 = connFrom->minTxFee1(usedInTx.size(), 3);
+ cinAmount = xBridgeIntFromReal(inAmount);
+ cfee1 = xBridgeIntFromReal(connFrom->minTxFee1(usedInTx.size(), 3));
+ coutAmountPlusFees = coutAmount+cfee1+cfee2;
++it;
}
+ const double fee1 = xBridgeValueFromAmount(cfee1);
+ const double fee2 = xBridgeValueFromAmount(cfee2);
+
{
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", txid.GetHex());
- log_obj.pushKV("fee1", fee1);
- log_obj.pushKV("fee2", fee2);
+ log_obj.pushKV("fee1", xBridgeValueFromAmount(cfee1));
+ log_obj.pushKV("fee2", xBridgeValueFromAmount(cfee2));
log_obj.pushKV("in_amount", inAmount);
- log_obj.pushKV("out_amount", outAmount + fee1 + fee2);
+ log_obj.pushKV("out_amount", xBridgeValueFromAmount(coutAmountPlusFees));
UniValue log_utxos(UniValue::VARR);
for (const auto & entry : usedInTx) {
UniValue log_utxo(UniValue::VOBJ);
@@ -1995,13 +2013,13 @@ bool Session::Impl::processTransactionCreateA(XBridgePacketPtr packet) const
}
// check amount
- if (inAmount < outAmount+fee1+fee2)
+ if (inAmount < xBridgeValueFromAmount(coutAmountPlusFees))
{
// no money, cancel transaction
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", txid.GetHex());
log_obj.pushKV("in_amount", inAmount);
- log_obj.pushKV("out_amount", outAmount+fee1+fee2);
+ log_obj.pushKV("out_amount", xBridgeValueFromAmount(coutAmountPlusFees));
LogOrderMsg(log_obj, "insufficient funds for order: expecting in amount to be >= out amount, canceling", __FUNCTION__);
sendCancelTransaction(xtx, crNoMoney);
return true;
@@ -2194,8 +2212,7 @@ bool Session::Impl::processTransactionCreateA(XBridgePacketPtr packet) const
if (xtx->isPartialRepost()) {
try {
const auto status = xapp.repostXBridgeTransaction(xtx->fromAddr, xtx->fromCurrency, xtx->toAddr, xtx->toCurrency,
- xBridgeValueFromAmount(xtx->origFromAmount)/xBridgeValueFromAmount(xtx->origToAmount), xtx->minFromAmount,
- xtx->repostCoins);
+ xtx->origFromAmount, xtx->origToAmount, xtx->minFromAmount, xtx->repostCoins, xtx->id);
if (status == xbridge::INSIFFICIENT_FUNDS)
LogOrderMsg(xtx->id.GetHex(), "not reposting the partial order because all available utxos have been used up", __FUNCTION__);
else if (status != xbridge::SUCCESS)
@@ -3322,7 +3339,7 @@ bool Session::Impl::processTransactionCancel(XBridgePacketPtr packet) const
// If order is new or pending (open), then rebroadcast on another snode
// only if our client didn't initiate the cancel.
- if (xtx->state <= TransactionDescr::trPending && !iCanceled) {
+ if (xtx->isLocal() && xtx->state <= TransactionDescr::trPending && !iCanceled) {
auto requireUpdateTime = boost::posix_time::second_clock::universal_time() - boost::posix_time::seconds(241);
xtx->setUpdateTime(requireUpdateTime); // will trigger an update (240 seconds stale)
write_log(log_obj, "cancel received, rebroadcasting order on another service node");
diff --git a/src/xbridge/xbridgetransaction.cpp b/src/xbridge/xbridgetransaction.cpp
index f6f833a9b0..abd0df3e9f 100644
--- a/src/xbridge/xbridgetransaction.cpp
+++ b/src/xbridge/xbridgetransaction.cpp
@@ -512,16 +512,11 @@ bool Transaction::tryJoin(const TransactionPtr other)
if (m_partialAllowed)
{
- // Taker amounts must agree with maker's asking price
- const double makerPrice = xBridgeValueFromAmount(m_sourceAmount) / xBridgeValueFromAmount(m_destAmount);
- const double takerPrice = xBridgeValueFromAmount(other->m_destAmount) / xBridgeValueFromAmount(other->m_sourceAmount);
- // If maker or taker prices are 0, abort
- // If the difference between maker and taker price is larger than max deviation, abort
- if (makerPrice == 0 || takerPrice == 0 || fabs(makerPrice - takerPrice) > xBridgeMaxPriceDeviation) {
+ if (!xBridgePartialOrderDriftCheck(m_sourceAmount, m_destAmount, other->m_sourceAmount, other->m_destAmount)) {
UniValue log_obj(UniValue::VOBJ);
log_obj.pushKV("orderid", id().GetHex());
- log_obj.pushKV("received_price", xbridge::xBridgeStringValueFromPrice(takerPrice));
- log_obj.pushKV("expected_price", xbridge::xBridgeStringValueFromPrice(makerPrice));
+ log_obj.pushKV("received_price", xbridge::xBridgeStringValueFromPrice(xBridgeValueFromAmount(m_sourceAmount) / xBridgeValueFromAmount(m_destAmount)));
+ log_obj.pushKV("expected_price", xbridge::xBridgeStringValueFromPrice(xBridgeValueFromAmount(other->m_destAmount) / xBridgeValueFromAmount(other->m_sourceAmount)));
xbridge::LogOrderMsg(log_obj, "taker price doesn't match maker expected price (join)", __FUNCTION__);
return false;
}
@@ -641,6 +636,8 @@ std::ostream & operator << (std::ostream & out, const TransactionPtr & tx)
log_obj.pushKV("taker_addr", (!tx->b_address().empty() && connTo ? connTo->fromXAddr(tx->b_address()) : ""));
log_obj.pushKV("order_type", tx->orderType());
log_obj.pushKV("partial_minimum", xbridge::xBridgeStringValueFromAmount(tx->min_partial_amount()));
+ log_obj.pushKV("partial_orig_maker_size", xbridge::xBridgeStringValueFromAmount(tx->a_initial_amount()));
+ log_obj.pushKV("partial_orig_taker_size", xbridge::xBridgeStringValueFromAmount(tx->b_initial_amount()));
log_obj.pushKV("state", tx->strState());
log_obj.pushKV("block_hash", tx->blockHash().GetHex());
log_obj.pushKV("created_at", xbridge::iso8601(tx->createdTime()));
diff --git a/src/xbridge/xbridgetransaction.h b/src/xbridge/xbridgetransaction.h
index e5602c3b8a..81a78bc9be 100644
--- a/src/xbridge/xbridgetransaction.h
+++ b/src/xbridge/xbridgetransaction.h
@@ -211,6 +211,7 @@ class Transaction
std::string a_payTxId() const;
bool a_refunded() const { LOCK(m_lock); return m_a_refunded; }
const std::vector a_utxos() const { LOCK(m_lock); return m_a.utxos(); }
+ uint64_t a_initial_amount() const { LOCK(m_lock); return m_sourceInitialAmount; }
std::vector a_pk1() const;
@@ -226,6 +227,7 @@ class Transaction
std::string b_payTxId() const;
bool b_refunded() const { LOCK(m_lock); return m_b_refunded; }
const std::vector b_utxos() const { LOCK(m_lock); return m_b.utxos(); }
+ uint64_t b_initial_amount() const { LOCK(m_lock); return m_destInitialAmount; }
uint64_t min_partial_amount() const { LOCK(m_lock); return m_minPartialAmount; }
diff --git a/src/xbridge/xbridgetransactiondescr.h b/src/xbridge/xbridgetransactiondescr.h
index 0c79b1e89f..91493585f7 100644
--- a/src/xbridge/xbridgetransactiondescr.h
+++ b/src/xbridge/xbridgetransactiondescr.h
@@ -151,6 +151,7 @@ struct TransactionDescr
READWRITE(historical);
READWRITE(logPayTx1);
READWRITE(logPayTx2);
+ READWRITE(parentOrder);
}
void SetNull() {
@@ -221,6 +222,7 @@ struct TransactionDescr
historical = false;
logPayTx1 = false;
logPayTx2 = false;
+ parentOrder.SetNull();
}
uint256 id;
@@ -325,6 +327,8 @@ struct TransactionDescr
bool logPayTx1{false};
bool logPayTx2{false};
+ uint256 parentOrder; // Parent order id of a partial order
+
// keep track of excluded servicenodes (snodes can be excluded if they fail to post)
std::set _excludedSnodes;
void excludeNode(CPubKey &key) {
@@ -510,11 +514,33 @@ struct TransactionDescr
txtime = t;
}
+ void setParentOrder(const uint256 orderid) {
+ LOCK(_lock);
+ parentOrder = orderid;
+ }
+
+ uint256 getParentOrder() {
+ LOCK(_lock);
+ return parentOrder;
+ }
+
std::set utxos() {
LOCK(_lock);
return {usedCoins.begin(), usedCoins.end()};
}
+ std::set origUtxos() {
+ LOCK(_lock);
+ std::set r{usedCoins.begin(), usedCoins.end()};
+ r.insert(repostCoins.begin(), repostCoins.end());
+ return std::move(r);
+ }
+
+ int utxoCount() {
+ LOCK(_lock);
+ return usedCoins.size() + repostCoins.size();
+ }
+
const std::string refundAddress() {
// Find the largest utxo to use as redeem if from address is empty
if (fromAddr.empty()) {
@@ -710,6 +736,7 @@ struct TransactionDescr
historical = d.historical;
logPayTx1 = d.logPayTx1;
logPayTx2 = d.logPayTx2;
+ parentOrder = d.parentOrder;
}
};
diff --git a/src/xbridge/xbridgewallet.h b/src/xbridge/xbridgewallet.h
index 653b70a5e0..25c6a38aa7 100644
--- a/src/xbridge/xbridgewallet.h
+++ b/src/xbridge/xbridgewallet.h
@@ -8,10 +8,12 @@
#ifndef BLOCKNET_XBRIDGE_XBRIDGEWALLET_H
#define BLOCKNET_XBRIDGE_XBRIDGEWALLET_H
+#include
+#include
#include
+#include
#include
-#include
#include
#include
#include
@@ -23,6 +25,7 @@
//*****************************************************************************
namespace xbridge
{
+ CAmount xBridgeIntFromReal(double amount);
//*****************************************************************************
//*****************************************************************************
@@ -55,6 +58,14 @@ struct UtxoEntry
return (txId == r.txId) && (vout ==r.vout);
}
+ COutPoint outpoint() const {
+ return { uint256S(txId), vout };
+ }
+
+ CAmount camount() const {
+ return xBridgeIntFromReal(amount);
+ }
+
void setConfirmations(const uint32_t confs) {
confirmations = confs;
hasConfirmations = true;
diff --git a/src/xbridge/xbridgewalletconnector.h b/src/xbridge/xbridgewalletconnector.h
index 950c61378b..38199e5046 100644
--- a/src/xbridge/xbridgewalletconnector.h
+++ b/src/xbridge/xbridgewalletconnector.h
@@ -185,8 +185,8 @@ class WalletConnector : public WalletParam
std::string & txId,
std::string & rawTx) = 0;
- virtual bool splitUtxos(double splitAmount, std::string addr, bool includeFees, std::set excluded,
- std::set utxos, double & totalSplit, double & splitIncFees, int & splitCount,
+ virtual bool splitUtxos(CAmount splitAmount, std::string addr, bool includeFees, std::set excluded,
+ std::set utxos, CAmount & totalSplit, CAmount & splitIncFees, int & splitCount,
std::string & txId, std::string & rawTx, std::string & failReason) = 0;
virtual bool isUTXOSpentInTx(const std::string & txid, const std::string & utxoPrevTxId,
diff --git a/src/xbridge/xbridgewalletconnectorbtc.cpp b/src/xbridge/xbridgewalletconnectorbtc.cpp
index ef8f1487a6..61f07cdaa5 100644
--- a/src/xbridge/xbridgewalletconnectorbtc.cpp
+++ b/src/xbridge/xbridgewalletconnectorbtc.cpp
@@ -2625,15 +2625,15 @@ bool BtcWalletConnector::createPartialTransaction(const std::vec
//******************************************************************************
//******************************************************************************
template
-bool BtcWalletConnector::splitUtxos(const double splitAmount, const std::string addr,
+bool BtcWalletConnector::splitUtxos(const CAmount splitAmount, const std::string addr,
const bool includeFees, const std::set excluded,
- const std::set utxos, double & totalSplit,
- double & splitIncFees, int & splitCount, std::string & txId,
+ const std::set utxos, CAmount & totalSplit,
+ CAmount & splitIncFees, int & splitCount, std::string & txId,
std::string & rawTx, std::string & failReason)
{
const auto hasUserSpecifiedUtxos = !utxos.empty();
- if (isDustAmount(splitAmount)) {
- failReason = "split amount is dust [" + std::to_string(splitAmount) + "]";
+ if (isDustAmount(xBridgeValueFromAmount(splitAmount))) {
+ failReason = "split amount is dust [" + xBridgeStringValueFromAmount(splitAmount) + "]";
return false;
}
if (!isValidAddress(addr)) {
@@ -2662,18 +2662,16 @@ bool BtcWalletConnector::splitUtxos(const double splitAmount, co
unspent = newUnspent; // only use user specified utxos
}
- const auto fee1 = minTxFee1(1, 3);
- const auto fee2 = minTxFee2(1, 1);
- const auto feesPerUtxo = fee1 + fee2;
- // Round split size to the nearest xbridge unit
- static double oneSubSat = 0.000000001; // help promote rounding to full sat
- const auto splitSize = xBridgeValueFromAmount(xBridgeAmountFromReal(splitAmount + (includeFees ? feesPerUtxo : 0.))) + oneSubSat;
+ const CAmount fee1 = xBridgeIntFromReal(minTxFee1(1, 3));
+ const CAmount fee2 = xBridgeIntFromReal(minTxFee2(1, 1));
+ const CAmount feesPerUtxo = fee1 + fee2;
+ const CAmount splitSize = splitAmount + (includeFees ? feesPerUtxo : 0);
if (!hasUserSpecifiedUtxos) {
// Remove all utxos that already match the expected size or that don't match the specified address
unspent.erase(std::remove_if(unspent.begin(), unspent.end(),
[splitSize, addr](const wallet::UtxoEntry & entry) {
- return xBridgeIntFromReal(splitSize) - xBridgeIntFromReal(entry.amount) == 0 || entry.address != addr;
+ return splitSize - entry.camount() == 0 || entry.address != addr;
}), unspent.end());
}
@@ -2686,10 +2684,10 @@ bool BtcWalletConnector::splitUtxos(const double splitAmount, co
if (unspent.size() > 100)
unspent.erase(unspent.begin()+100, unspent.begin()+unspent.size());
- double vinsTotal{0};
+ CAmount vinsTotal{0};
std::vector vins;
for (const auto & vin : unspent) {
- vinsTotal += vin.amount;
+ vinsTotal += vin.camount();
vins.emplace_back(vin.txId, vin.vout, vin.amount);
}
@@ -2701,25 +2699,25 @@ bool BtcWalletConnector::splitUtxos(const double splitAmount, co
if (outputCount > 100)
outputCount = 100;
- const auto remainder = vinsTotal - (outputCount * splitSize);
- std::vector> vouts;
+ const CAmount remainder = vinsTotal - (outputCount * splitSize);
+ std::vector> vouts;
for (int i = 0; i < outputCount; ++i)
vouts.emplace_back(addr, splitSize);
- const double txFees = minTxFee1(vins.size(), vouts.size());
- const auto change = remainder - txFees;
+ const CAmount txFees = xBridgeIntFromReal(minTxFee1(vins.size(), vouts.size()));
+ const CAmount change = remainder - txFees;
// add remainder vout if not dust
- if (!isDustAmount(change))
- vouts.emplace_back(addr, change); // subtract fees
+ if (!isDustAmount(xBridgeValueFromAmount(change)))
+ vouts.emplace_back(addr, change);
else {
// Remove any utxos consumed by fees
- auto feesLeft = txFees;
- while (feesLeft > std::numeric_limits::epsilon() && !vouts.empty()) {
+ CAmount feesLeft = txFees;
+ while (feesLeft > 0 && !vouts.empty()) {
auto & vout = vouts[vouts.size()-1];
- const auto voutAmount = vout.second;
- const auto voutNewAmount = voutAmount - feesLeft;
+ const CAmount voutAmount = vout.second;
+ const CAmount voutNewAmount = voutAmount - feesLeft;
// If vout doesn't cover the fee move to the next one (i.e. if new amount is too small or negative)
- if (voutNewAmount <= std::numeric_limits::epsilon()) {
+ if (voutNewAmount <= 0) {
vouts.erase(vouts.begin() + vouts.size());
outputCount -= 1;
feesLeft -= voutAmount; // subtract vout amount from leftover fees
@@ -2727,7 +2725,7 @@ bool BtcWalletConnector::splitUtxos(const double splitAmount, co
}
vout.second = voutNewAmount;
- if (isDustAmount(vout.second))
+ if (isDustAmount(xBridgeValueFromAmount(vout.second)))
vouts.erase(vouts.begin()+vouts.size()); // remove output if dust
outputCount -= 1;
@@ -2740,7 +2738,10 @@ bool BtcWalletConnector::splitUtxos(const double splitAmount, co
return false;
}
- xbridge::CTransactionPtr tx = createTransaction(*this, vins, vouts, COIN, txVersion, 0, txWithTimeField);
+ std::vector