diff --git a/LeftPanel.qml b/LeftPanel.qml index d543c1f..ac2e115 100644 --- a/LeftPanel.qml +++ b/LeftPanel.qml @@ -42,10 +42,13 @@ Rectangle { property alias unlockedBalanceLabelVisible: unlockedBalanceLabel.visible property alias balanceLabelText: balanceLabel.text property alias balanceText: balanceText.text + property alias lockedBalanceLabelText: lockedBalanceLabel.text + property alias lockedBalanceText: lockedBalanceText.text property alias networkStatus : networkStatus property alias progressBar : progressBar property alias daemonProgressBar : daemonProgressBar property alias minutesToUnlockTxt: unlockedBalanceLabel.text + property alias heightsToUnlockTxt: lockedBalanceLabel.text property int titleBarHeight: 50 property string copyValue: "" Clipboard { id: clipboard } @@ -62,6 +65,7 @@ Rectangle { signal keysClicked() signal merchantClicked() signal accountClicked() + signal stakeClicked() function selectItem(pos) { menuColumn.previousButton.checked = false @@ -78,6 +82,7 @@ Rectangle { else if(pos === "Advanced") menuColumn.previousButton = advancedButton else if(pos === "Keys") menuColumn.previousButton = keysButton else if(pos === "Account") menuColumn.previousButton = accountButton + else if(pos === "Stake") menuColumn.previousButton = stakeButton menuColumn.previousButton.checked = true } @@ -100,7 +105,7 @@ Rectangle { visible: true z: 2 id: column1 - height: 210 + height: 230 anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top @@ -118,7 +123,7 @@ Rectangle { width: 260 * scaleRatio Image { - width: 260; height: 170 + width: 260; height: 230 fillMode: Image.PreserveAspectFit source: "images/card-background.png" } @@ -196,7 +201,7 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: 20 anchors.top: parent.top - anchors.topMargin: 76 + anchors.topMargin: 96 font.family: "Arial" color: "#FFFFFF" text: "N/A" @@ -234,7 +239,45 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: 20 anchors.top: parent.top - anchors.topMargin: 126 + anchors.topMargin: 136 + font.family: "Arial" + color: "#FFFFFF" + text: "N/A" + // dynamically adjust text size + font.pixelSize: { + var digits = text.split('.')[0].length + var defaultSize = 20; + if(digits > 3) { + return defaultSize - 0.6*digits + } + return defaultSize; + } + + MouseArea { + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onEntered: { + parent.color = MoneroComponents.Style.orange + } + onExited: { + parent.color = MoneroComponents.Style.white + } + onClicked: { + console.log("Copied to clipboard"); + clipboard.setText(parent.text); + appWindow.showStatusMessage(qsTr("Copied to clipboard"),3) + } + } + } + + Text { + id: lockedBalanceText + visible: true + anchors.left: parent.left + anchors.leftMargin: 20 + anchors.top: parent.top + anchors.topMargin: 176 font.family: "Arial" color: "#FFFFFF" text: "N/A" @@ -266,6 +309,17 @@ Rectangle { } } + MoneroComponents.Label { + id: lockedBalanceLabel + visible: true + text: qsTr("Locked balance") + translationManager.emptyString + fontSize: 14 + anchors.left: parent.left + anchors.leftMargin: 20 + anchors.top: parent.top + anchors.topMargin: 160 + } + MoneroComponents.Label { id: unlockedBalanceLabel visible: true @@ -274,7 +328,7 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: 20 anchors.top: parent.top - anchors.topMargin: 110 + anchors.topMargin: 120 } MoneroComponents.Label { @@ -285,10 +339,11 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: 20 anchors.top: parent.top - anchors.topMargin: 60 + anchors.topMargin: 80 elide: Text.ElideRight textWidth: 238 } + Item { //separator anchors.left: parent.left anchors.right: parent.right @@ -627,7 +682,7 @@ Rectangle { color: "#313131" height: 1 } - // ------------- Sign/verify tab --------------- + // ------------- Seed&Keys tab --------------- MoneroComponents.MenuButton { id: keysButton visible: appWindow.walletMode >= 2 @@ -651,6 +706,27 @@ Rectangle { color: "#313131" height: 1 } + // ------------- Stake tab --------------- + MoneroComponents.MenuButton { + id: stakeButton + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("Stake") + translationManager.emptyString + symbol: qsTr("F") + translationManager.emptyString + dotColor: "#F43D19" + onClicked: { + parent.previousButton.checked = false + parent.previousButton = stakeButton + panel.stakeClicked() + } + } + + Rectangle { + visible: stakeButton.present && appWindow.walletMode >= 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + } } // Column diff --git a/MiddlePanel.qml b/MiddlePanel.qml index 1e4c693..3c3d6c8 100644 --- a/MiddlePanel.qml +++ b/MiddlePanel.qml @@ -68,8 +68,9 @@ Rectangle { property AddressBook addressBookView: AddressBook { } property Keys keysView: Keys { } property Account accountView: Account { } + property Stake stakeView: Stake { } - signal paymentClicked(string address, string paymentId, string amount, int mixinCount, int priority, string description) + signal paymentClicked(string address, string paymentId, string amount, int mixinCount, int priority, string description, string unlocktime) signal sweepUnmixableClicked() signal generatePaymentIdInvoked() signal getProofClicked(string txid, string address, string message); @@ -106,6 +107,7 @@ Rectangle { function updateStatus(){ transferView.updateStatus(); + stakeView.updateStatus(); } // send from AddressBook @@ -164,7 +166,12 @@ Rectangle { name: "Account" PropertyChanges { target: root; currentView: accountView } PropertyChanges { target: mainFlickable; contentHeight: minHeight } - } + }, State { + name: "Stake" + PropertyChanges { target: stakeView; model: appWindow.currentWallet ? appWindow.currentWallet.lockedModel : null } + PropertyChanges { target: root; currentView: stakeView } + PropertyChanges { target: mainFlickable; contentHeight: stakeView.transferHeight1 + 80 } + } ] // color stripe at the top @@ -257,11 +264,20 @@ Rectangle { target: transferView onPaymentClicked : { console.log("MiddlePanel: paymentClicked") - paymentClicked(address, paymentId, amount, mixinCount, priority, description) + paymentClicked(address, paymentId, amount, mixinCount, priority, description, unlocktime) } onSweepUnmixableClicked : { console.log("MiddlePanel: sweepUnmixableClicked") sweepUnmixableClicked() } } + + Connections { + ignoreUnknownSignals: false + target: stakeView + onPaymentClicked : { + console.log("MiddlePanel: paymentClicked") + paymentClicked(address, paymentId, amount, mixinCount, priority, description, unlocktime) + } + } } diff --git a/components/StakeTable.qml b/components/StakeTable.qml new file mode 100644 index 0000000..9b215db --- /dev/null +++ b/components/StakeTable.qml @@ -0,0 +1,360 @@ +// Copyright (c) 2014-2018, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import QtQuick 2.0 +import moneroComponents.Clipboard 1.0 +import moneroComponents.AddressBookModel 1.0 + +import "../components" as MoneroComponents +import "../js/TxUtils.js" as TxUtils + +ListView { + id: listView + clip: true + boundsBehavior: ListView.StopAtBounds + property var previousItem + property int rowSpacing: 12 + property var addressBookModel: null + + function buildTxDetailsString(tx_id, paymentId, tx_key,tx_note, destinations, rings, address, address_label) { + var trStart = '', + trMiddle = '', + trEnd = ""; + + return '' + + (tx_id ? trStart + qsTr("Tx ID:") + trMiddle + tx_id + trEnd : "") + + (address_label ? trStart + qsTr("Address label:") + trMiddle + address_label + trEnd : "") + + (address ? trStart + qsTr("Address:") + trMiddle + address + trEnd : "") + + (paymentId ? trStart + qsTr("Payment ID:") + trMiddle + paymentId + trEnd : "") + + (tx_key ? trStart + qsTr("Tx key:") + trMiddle + tx_key + trEnd : "") + + (tx_note ? trStart + qsTr("Tx note:") + trMiddle + tx_note + trEnd : "") + + (destinations ? trStart + qsTr("Destinations:") + trMiddle + destinations + trEnd : "") + + (rings ? trStart + qsTr("Rings:") + trMiddle + rings + trEnd : "") + + "
" + + translationManager.emptyString; + } + + function lookupPaymentID(paymentId) { + if (!addressBookModel) + return "" + var idx = addressBookModel.lookupPaymentID(paymentId) + if (idx < 0) + return "" + idx = addressBookModel.index(idx, 0) + return addressBookModel.data(idx, AddressBookModel.AddressBookDescriptionRole) + } + + header: Rectangle { + height: 27 * scaleRatio + width: listView.width + color: "transparent" + + Rectangle{ + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 1 + color: "#404040" + } + + Rectangle{ + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 1 + color: "#404040" + } + + Rectangle{ + anchors.right: parent.right + anchors.bottom: parent.top + anchors.left: parent.left + height: 1 + color: "#404040" + } + + Rectangle{ + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.left: parent.left + height: 1 + color: "#404040" + } + + Text { + id: stakeHeader + width: 200 * scaleRatio + anchors.left: parent.left + anchors.leftMargin: 40 * scaleRatio + font.family: "Arial" + font.pixelSize: 14 + font.bold: true + color: "#808080" + text: qsTr("Staked:") + translationManager.emptyString + } + Text { + id: dateHeader + width: 200 * scaleRatio + anchors.left: stakeHeader.right + anchors.leftMargin: 80 * scaleRatio + font.family: "Arial" + font.pixelSize: 14 + font.bold: true + color: "#808080" + text: qsTr("Date:") + translationManager.emptyString + } + Text { + id: locktimeHeader + width: 150 * scaleRatio + anchors.left: dateHeader.right + anchors.leftMargin: 100 * scaleRatio + font.family: "Arial" + font.pixelSize: 14 + font.bold: true + color: "#808080" + text: qsTr("Lock time: (block/~days)") + translationManager.emptyString + } + Text { + id: expirateHeader + width: 100 * scaleRatio + anchors.left: locktimeHeader.right + anchors.leftMargin: 200 * scaleRatio + font.family: "Arial" + font.pixelSize: 14 + font.bold: true + color: "#808080" + text: qsTr("Expirate time: (height/~time)") + translationManager.emptyString + } + } + + footer: Rectangle { + height: 127 * scaleRatio + width: listView.width + color: "transparent" + + Text { + anchors.centerIn: parent + font.family: "Arial" + font.pixelSize: 14 + color: "#545454" + text: qsTr("No more results") + translationManager.emptyString + } + } + + delegate: Rectangle { + id: delegate + property bool collapsed: false + height: 70 * scaleRatio + width: listView.width + color: "transparent" + + // borders + Rectangle{ + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: 1 + color: "#404040" + } + + Rectangle{ + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: collapsed ? 2 : 1 + color: collapsed ? "#BBBBBB" : "#404040" + } + + Rectangle{ + anchors.right: parent.right + anchors.bottom: parent.top + anchors.left: parent.left + height: 1 + color: "#404040" + } + + Rectangle{ + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.left: parent.left + height: 1 + color: "#404040" + } + + Rectangle { + id: row1 + anchors.left: parent.left + anchors.leftMargin: 20 * scaleRatio + anchors.right: parent.right + anchors.rightMargin: 20 * scaleRatio + anchors.top: parent.top + anchors.topMargin: 15 * scaleRatio + height: 40 * scaleRatio + color: "transparent" + + MouseArea { + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onEntered: { + parent.color = "#404040" + } + onExited: { + parent.color = "transparent" + } + onClicked: { + console.log("Copied txid to clipboard"); + clipboard.setText(hash); + appWindow.showStatusMessage(qsTr("Copied txid to clipboard"),3) + } + } + + Image { + id: arrowImage + source: "../images/lockIcon.png" + height: 18 * scaleRatio + width: (confirmationsRequired === 60 ? 18 : 12) * scaleRatio + anchors.verticalCenter: parent.verticalCenter + } + + Text { + id: amountLabel + anchors.left: arrowImage.right + anchors.leftMargin: 18 * scaleRatio + anchors.verticalCenter: parent.verticalCenter + font.family: MoneroComponents.Style.fontBold.name + font.pixelSize: 18 * scaleRatio + font.bold: true + text: { + var _amount = amount; + if(_amount === 0){ + // *sometimes* amount is 0, while the 'destinations string' + // has the correct amount, so we try to fetch it from that instead. + _amount = TxUtils.destinationsToAmount(destinations); + _amount = (_amount *1); + } + + if(_amount === 0){ + _amount = currentWallet.revealTxOut(hash); + } + + return _amount + " XMC"; + } + color: MoneroComponents.Style.white + + MouseArea { + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onEntered: { + parent.color = MoneroComponents.Style.orange + } + onExited: { + parent.color = MoneroComponents.Style.white + } + onClicked: { + console.log("Copied to clipboard"); + clipboard.setText(parent.text.split(" ")[0]); + appWindow.showStatusMessage(qsTr("Copied to clipboard"),3) + } + } + } + + Rectangle { + id: timeRect + anchors.leftMargin: 300 * scaleRatio + anchors.left: parent.left + width: 200 * scaleRatio + height: parent.height + color: "transparent" + + Text { + id: dateLabel + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + font.family: MoneroComponents.Style.fontRegular.name + font.pixelSize: 18 * scaleRatio + font.bold: true + text: date + color: "#808080" + } + + Text { + id: timeLabel + anchors.verticalCenter: parent.verticalCenter + anchors.left: dateLabel.right + anchors.leftMargin: 7 * scaleRatio + font.pixelSize: 18 * scaleRatio + font.bold: true + text: time + color: "#808080" + } + } + + Rectangle { + id: locktimeRect + anchors.leftMargin: 100 * scaleRatio + anchors.left: timeRect.right + width: 300 * scaleRatio + height: parent.height + color: "transparent" + + Text { + id: locktimeLabel + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: 18 * scaleRatio + font.bold: true + text: qsTr("%1/%2").arg(confirmationsRequired).arg(confirmationsRequired/720) + color: "#808080" + } + } + + Rectangle { + id: expiratetimeRect + anchors.leftMargin: 50 * scaleRatio + anchors.left: locktimeRect.right + width: 300 * scaleRatio + height: parent.height + color: "transparent" + + Text { + id: expirateLabel + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: 18 * scaleRatio + font.bold: true + text: unlockTime + "/" + expirateTime + color: unlockTime < blockHeight + 10000 ? "red" : "#808080" + } + } + } + } + + Clipboard { id: clipboard } +} diff --git a/main.cpp b/main.cpp index 3620ce2..aee4fa1 100644 --- a/main.cpp +++ b/main.cpp @@ -248,6 +248,15 @@ int main(int argc, char *argv[]) engine.rootContext()->setContextProperty("walletLogPath", logPath); + QString path = QCoreApplication::applicationDirPath(); +#ifdef Q_OS_MACOS + int pos = path.lastIndexOf("/Contents/MacOS"); + path = path.left(pos); + pos = path.lastIndexOf("/"); + path = path.left(pos); +#endif + engine.rootContext()->setContextProperty("applicationDirPath", path); + // Exclude daemon manager from IOS #ifndef Q_OS_IOS const QStringList arguments = (QStringList) QCoreApplication::arguments().at(0); diff --git a/main.qml b/main.qml index 30d5dee..59b31c9 100644 --- a/main.qml +++ b/main.qml @@ -69,6 +69,7 @@ ApplicationWindow { property bool viewOnly: false property bool foundNewBlock: false property int timeToUnlock: 0 + property int heightToUnlock: 0 property bool qrScannerEnabled: (typeof builtWithScanner != "undefined") && builtWithScanner property int blocksToSync: 1 property var isMobile: (appWindow.width > 700 && !isAndroid) ? false : true @@ -138,6 +139,7 @@ ApplicationWindow { else if(seq === "Ctrl+Y") leftPanel.keysClicked() else if(seq === "Ctrl+D") middlePanel.state = "Advanced" else if(seq === "Ctrl+T") middlePanel.state = "Account" + else if(seq === "Ctrl+F") middlePanel.state = "Stake" else if(seq === "Ctrl+Tab" || seq === "Alt+Tab") { /* if(middlePanel.state === "Transfer") middlePanel.state = "Receive" @@ -149,7 +151,7 @@ ApplicationWindow { else if(middlePanel.state === "Mining") middlePanel.state = "Sign" else if(middlePanel.state === "Sign") middlePanel.state = "Settings" */ - if(middlePanel.state === "Settings") middlePanel.state = "Account" + if(middlePanel.state === "Stake") middlePanel.state = "Account" else if(middlePanel.state === "Account") middlePanel.state = "Transfer" else if(middlePanel.state === "Transfer") middlePanel.state = "AddressBook" else if(middlePanel.state === "AddressBook") middlePanel.state = "Receive" @@ -159,6 +161,7 @@ ApplicationWindow { else if(middlePanel.state === "TxKey") middlePanel.state = "SharedRingDB" else if(middlePanel.state === "SharedRingDB") middlePanel.state = "Sign" else if(middlePanel.state === "Sign") middlePanel.state = "Settings" + else if(middlePanel.state === "Settings") middlePanel.state = "Stake" } else if(seq === "Ctrl+Shift+Backtab" || seq === "Alt+Shift+Backtab") { /* if(middlePanel.state === "Settings") middlePanel.state = "Sign" @@ -179,7 +182,8 @@ ApplicationWindow { else if(middlePanel.state === "Receive") middlePanel.state = "AddressBook" else if(middlePanel.state === "AddressBook") middlePanel.state = "Transfer" else if(middlePanel.state === "Transfer") middlePanel.state = "Account" - else if(middlePanel.state === "Account") middlePanel.state = "Settings" + else if(middlePanel.state === "Account") middlePanel.state = "Stake" + else if(middlePanel.state === "Stake") middlePanel.state = "Settings" } if (middlePanel.state !== "Advanced") updateBalance(); @@ -272,6 +276,7 @@ ApplicationWindow { // Hide titlebar based on persistentSettings.customDecorations titleBar.visible = persistentSettings.customDecorations; + titleBar.maximizeClicked(); } function closeWallet(callback) { @@ -396,15 +401,18 @@ ApplicationWindow { var balance_unlocked = qsTr("HIDDEN"); var balance = qsTr("HIDDEN"); + var balance_locked = qsTr("HIDDEN"); if(!hideBalanceForced && !persistentSettings.hideBalance){ balance_unlocked = walletManager.displayAmount(currentWallet.unlockedBalance(currentWallet.currentSubaddressAccount)); balance = walletManager.displayAmount(currentWallet.balance(currentWallet.currentSubaddressAccount)); + balance_locked = walletManager.displayAmount(currentWallet.balance(currentWallet.currentSubaddressAccount) - currentWallet.unlockedBalance(currentWallet.currentSubaddressAccount)); } middlePanel.unlockedBalanceText = balance_unlocked; leftPanel.unlockedBalanceText = balance_unlocked; middlePanel.balanceText = balance; leftPanel.balanceText = balance; + leftPanel.lockedBalanceText = balance_locked; var accountLabel = currentWallet.getSubaddressLabel(currentWallet.currentSubaddressAccount, 0); leftPanel.balanceLabelText = qsTr("Balance (#%1%2)").arg(currentWallet.currentSubaddressAccount).arg(accountLabel === "" ? "" : (" – " + accountLabel)); @@ -480,6 +488,8 @@ ApplicationWindow { currentWallet.history.refresh(currentWallet.currentSubaddressAccount) timeToUnlock = currentWallet.history.minutesToUnlock leftPanel.minutesToUnlockTxt = (timeToUnlock > 0)? (timeToUnlock == 20)? qsTr("Unlocked balance (waiting for block)") : qsTr("Unlocked balance (~%1 min)").arg(timeToUnlock) : qsTr("Unlocked balance"); + heightToUnlock = currentWallet.history.blockToUnlock + leftPanel.heightsToUnlockTxt = (heightToUnlock > 60) ? qsTr("Locked balance (~%1 height)").arg(heightToUnlock) : qsTr("Locked balance"); } } @@ -686,6 +696,7 @@ ApplicationWindow { // + ", fee: " + walletManager.displayAmount(transaction.fee/1000000000000.0)); console.log("######## Transaction created, amount: " + transaction.amount + ", fee: " + walletManager.displayAmount(transaction.fee)); + console.log("%%%%%%% Tx id: " + transaction.txid); // here we show confirmation popup; transactionConfirmationPopup.title = qsTr("Please confirm transaction:\n") + translationManager.emptyString; @@ -710,15 +721,16 @@ ApplicationWindow { } - // called on "transfer" - function handlePayment(address, paymentId, amount, mixinCount, priority, description, createFile) { + // called on "transfer" or "stake" + function handlePayment(address, paymentId, amount, mixinCount, priority, description, unlocktime, createFile) { console.log("Creating transaction: ") console.log("\taddress: ", address, ", payment_id: ", paymentId, ", amount: ", amount, ", mixins: ", mixinCount, ", priority: ", priority, - ", description: ", description); + ", description: ", description, + ", unlock_time: ", unlocktime); showProcessingSplash("Creating transaction"); @@ -755,10 +767,14 @@ ApplicationWindow { } } + // we use this function to parse string to quint64 + var unlockheight = walletManager.heightFromString(unlocktime); + console.log("unlockheight: " + unlockheight) + if (amount === "(all)") - currentWallet.createTransactionAllAsync(address, paymentId, mixinCount, priority); + currentWallet.createTransactionAllAsync(address, paymentId, mixinCount, priority, unlockheight); else - currentWallet.createTransactionAsync(address, paymentId, amountxmr, mixinCount, priority); + currentWallet.createTransactionAsync(address, paymentId, amountxmr, mixinCount, priority, unlockheight); } //Choose where to save transaction @@ -873,6 +889,7 @@ ApplicationWindow { // Clear tx fields middlePanel.transferView.clearFields() + middlePanel.stakeView.clearFields() } informationPopup.onCloseCallback = null @@ -1510,6 +1527,12 @@ ApplicationWindow { } onKeysClicked: Utils.showSeedPage(); + + onStakeClicked: { + middlePanel.state = "Stake"; + middlePanel.flickable.contentY = 0; + updateBalance(); + } onAccountClicked: { middlePanel.state = "Account"; diff --git a/moneroclassic-wallet-gui.pro b/moneroclassic-wallet-gui.pro index 4e186f5..db57b5e 100644 --- a/moneroclassic-wallet-gui.pro +++ b/moneroclassic-wallet-gui.pro @@ -13,10 +13,9 @@ CONFIG += c++11 link_pkgconfig packagesExist(hidapi-libusb) { PKGCONFIG += hidapi-libusb } -!win32 { - QMAKE_CXXFLAGS += -fPIC -fstack-protector -fstack-protector-strong - QMAKE_LFLAGS += -fstack-protector -fstack-protector-strong -} + +QMAKE_CXXFLAGS += -fPIC -fstack-protector -fstack-protector-strong +QMAKE_LFLAGS += -fstack-protector -fstack-protector-strong # cleaning "auto-generated" bitmonero directory on "make distclean" QMAKE_DISTCLEAN += -r $$WALLET_ROOT @@ -25,7 +24,8 @@ INCLUDEPATH += $$WALLET_ROOT/include \ $$PWD/src/libwalletqt \ $$PWD/src/QR-Code-generator \ $$PWD/src \ - $$WALLET_ROOT/src + $$WALLET_ROOT/src \ + /usr/local/include HEADERS += \ filter.h \ @@ -435,7 +435,11 @@ TRANSLATION_TARGET_DIR = $$OUT_PWD/translations PRE_TARGETDEPS += langupd compiler_langrel_make_all RESOURCES += qml.qrc -CONFIG += qtquickcompiler +#CONFIG += qtquickcompiler + +CONFIG += declarative_debug +CONFIG += qml_debug +QML_IMPORT_TRACE = 1 # Additional import path used to resolve QML modules in Qt Creator's code model QML_IMPORT_PATH = fonts diff --git a/pages/Stake.qml b/pages/Stake.qml new file mode 100644 index 0000000..2756885 --- /dev/null +++ b/pages/Stake.qml @@ -0,0 +1,404 @@ +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls 1.4 +import moneroComponents.Clipboard 1.0 +import moneroComponents.PendingTransaction 1.0 +import moneroComponents.Wallet 1.0 +import moneroComponents.NetworkType 1.0 +import moneroComponents.TransactionHistory 1.0 +import moneroComponents.TransactionInfo 1.0 +import moneroComponents.TransactionHistoryModel 1.0 +import FontAwesome 1.0 +import "../components" +import "../components" as MoneroComponents +import "." 1.0 +import "../js/TxUtils.js" as TxUtils + +Rectangle { + id: root + + signal paymentClicked(string address, string paymentId, string amount, int mixinCount, int priority, string description, string unlocktime) + + color: "transparent" + property alias transferHeight1: pageRoot.height + property int mixin: 10 // (ring size 11) + property string warningContent: "" + property string sendButtonWarning: "" + property string startLinkText: qsTr(" (Start daemon)") + translationManager.emptyString + property string settingsPath: applicationDirPath + "/pos.json" + // @TODO: remove after pid removal hardfork + property bool warningLongPidTransfer: false + property var model + property int tableHeight: !isMobile ? table.contentHeight : tableMobile.contentHeight + + QtObject { + id: d + property bool initialized: false + } + + Clipboard { id: clipboard } + + onModelChanged: { + if (typeof model !== 'undefined' && model != null) { + if (!d.initialized) { + model.sortRole = TransactionHistoryModel.TransactionBlockHeightRole + model.sort(0, Qt.DescendingOrder); + d.initialized = true + } + } + } + + function clearFields() { + amountLine.text = "" + locktimeDropdown.currentIndex = 0 + updateLocktimeDropdown() + } + + ColumnLayout { + id: pageRoot + anchors.margins: 20 + anchors.topMargin: 40 + + anchors.left: parent.left + anchors.top: parent.top + anchors.right: parent.right + + spacing: 30 + + RowLayout { + visible: root.warningContent !== "" + + MoneroComponents.WarningBox { + text: warningContent + onLinkActivated: { + appWindow.startDaemon(appWindow.persistentSettings.daemonFlags); + } + } + } + + GridLayout { + columns: appWindow.walletMode < 2 ? 1 : 2 + Layout.fillWidth: true + columnSpacing: 32 + + ColumnLayout { + Layout.fillWidth: true + Layout.minimumWidth: 200 + + // Amount input + LineEdit { + id: amountLine + Layout.fillWidth: true + inlineIcon: true + labelText: qsTr(" Amount ") + + translationManager.emptyString + placeholderText: "0.00" + width: 100 + fontBold: true + inlineButtonText: qsTr("All") + translationManager.emptyString + inlineButton.onClicked: amountLine.text = "(all)" + onTextChanged: { + if(amountLine.text.indexOf('.') === 0){ + amountLine.text = '0' + amountLine.text; + } + } + + validator: RegExpValidator { + regExp: /^(\d{1,8})?([\.]\d{1,12})?$/ + } + } + } + + ColumnLayout { + visible: appWindow.walletMode >= 2 + Layout.fillWidth: true + Label { + id: transactionLocktime + Layout.topMargin: 12 + text: qsTr("Lock time (days)") + translationManager.emptyString + fontBold: false + fontSize: 16 + } + // Note: workaround for translations in listElements + // ListElement: cannot use script for property value, so + // code like this wont work: + // ListElement { column1: qsTr("LOW") + translationManager.emptyString ; column2: ""; priority: PendingTransaction.Priority_Low } + // For translations to work, the strings need to be listed in + // the file components/StandardDropdown.qml too. + + // Priorites after v5 + ListModel { + id: locktimeModelV5 + + // Add more 200 height for block confirm spent + ListElement {column1: qsTr("90"); column2: ""; locktime: "65000"} + ListElement {column1: qsTr("180"); column2: ""; locktime: "129800"} + ListElement {column1: qsTr("360"); column2: ""; locktime: "259400"} + } + + StandardDropdown { + Layout.fillWidth: true + id: locktimeDropdown + Layout.topMargin: 5 + currentIndex: 0 + } + } + // Make sure dropdown is on top + z: parent.z + 1 + } + + RowLayout { + StandardButton { + id: sendButton + rightIcon: "qrc:///images/rightArrow.png" + rightIconInactive: "qrc:///images/rightArrowInactive.png" + Layout.topMargin: 40 + width: 200 * scaleRatio + text: qsTr("Stake") + translationManager.emptyString + enabled: { + return updateSendButton() + } + onClicked: { + console.log("Stake: paymentClicked") + var unlocktime = locktimeModelV5.get(locktimeDropdown.currentIndex).locktime + var mainaddress = currentWallet.address(0, 0) + console.log("locktime: " + unlocktime) + console.log("amount: " + amountLine.text) + console.log("main address: " + mainaddress); + root.paymentClicked(mainaddress, "", amountLine.text, root.mixin, 0, "", unlocktime) + } + } + StandardButton { + id: exportStakeSettingsButton + Layout.topMargin: 40 + anchors.left: sendButton.right + anchors.leftMargin: 150 + width: 200 * scaleRatio + text: qsTr("Export Stake Settings") + translationManager.emptyString + small: true + visible: !appWindow.viewOnly + enabled: table.count > 0 + onClicked: { + console.log("Stake: export stake settings clicked") + exportStakeSettingsDialog.open(); + } + } + MoneroComponents.CheckBox { + id: autoStakeCheckBox + Layout.topMargin: 40 + anchors.left: exportStakeSettingsButton.right + anchors.leftMargin: 150 + //fontBold: false + fontSize: 16 * scaleRatio + //checked: persistentSettings.hideBalance + checked: false + onClicked: { + //persistentSettings.hideBalance = !persistentSettings.hideBalance + //appWindow.updateBalance(); + currentWallet.setAutoStake(autoStakeCheckBox.checked); + } + text: qsTr("Auto stake when one expirates") + translationManager.emptyString + } + } + + FileDialog { + id: exportStakeSettingsDialog + selectMultiple: false + selectExisting: false + onAccepted: { + settingsPath = walletManager.urlToLocalPath(exportStakeSettingsDialog.fileUrl) + console.log("stake settings path: " + settingsPath) + currentWallet.exportStakedSettings(settingsPath); + } + onRejected: { + console.log("Canceled"); + } + } + + MoneroComponents.WarningBox { + id: sendButtonWarningBox + text: root.sendButtonWarning + visible: root.sendButtonWarning !== "" + } + + GridLayout { + id: tableHeader + columns: 1 + columnSpacing: 0 + rowSpacing: 0 + Layout.topMargin: 20 + Layout.fillWidth: true + + Label { + fontSize: 16 + text: qsTr("Staked transaction list") + translationManager.emptyString + } + + Rectangle { + height: 10 + } + + RowLayout{ + Layout.preferredHeight: 10 + Layout.fillWidth: true + + Rectangle { + id: header + Layout.fillWidth: true + visible: table.count > 0 + + height: 10 + color: "transparent" + + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 10 + anchors.leftMargin: 10 + + height: 1 + color: "#404040" + } + + Image { + anchors.top: parent.top + anchors.left: parent.left + + width: 10 + height: 10 + + source: "../images/historyBorderRadius.png" + } + + Image { + anchors.top: parent.top + anchors.right: parent.right + + width: 10 + height: 10 + + source: "../images/historyBorderRadius.png" + rotation: 90 + } + } + } + + RowLayout { + Layout.preferredHeight: table.contentHeight + Layout.fillWidth: true + Layout.fillHeight: true + + StakeTable { + id: table + visible: true + onContentYChanged: flickableScroll.flickableContentYChanged() + model: root.model + addressBookModel: null + + Layout.fillWidth: true + Layout.fillHeight: true + } + + } + } + } // pageRoot + + Component.onCompleted: { + //Disable password page until enabled by updateStatus + pageRoot.enabled = false + } + + // fires on every page load + function onPageCompleted() { + console.log("stake page loaded") + updateStatus(); + updateLocktimeDropdown() + + if(currentWallet != null && typeof currentWallet.history !== "undefined" ) { + currentWallet.history.refresh(currentWallet.currentSubaddressAccount) + } + } + + function updateLocktimeDropdown() { + locktimeDropdown.dataModel = locktimeModelV5; + locktimeDropdown.update() + } + + function updateStatus() { + var messageNotConnected = qsTr("Wallet is not connected to daemon."); + if(appWindow.walletMode >= 2) messageNotConnected += root.startLinkText; + pageRoot.enabled = true; + if(typeof currentWallet === "undefined") { + root.warningContent = messageNotConnected; + return; + } + + if (currentWallet.viewOnly) { + // warningText.text = qsTr("Wallet is view only.") + //return; + } + //pageRoot.enabled = false; + + switch (currentWallet.connected()) { + case Wallet.ConnectionStatus_Disconnected: + root.warningContent = messageNotConnected; + break + case Wallet.ConnectionStatus_WrongVersion: + root.warningContent = qsTr("Connected daemon is not compatible with GUI. \n" + + "Please upgrade or connect to another daemon") + break + default: + if(!appWindow.daemonSynced){ + root.warningContent = qsTr("Waiting on daemon synchronization to finish.") + } else { + // everything OK, enable transfer page + // Light wallet is always ready + pageRoot.enabled = true; + root.warningContent = ""; + } + } + } + + function updateSendButton(){ + // reset message + root.sendButtonWarning = ""; + + // Currently opened wallet is not view-only + if(appWindow.viewOnly){ + root.sendButtonWarning = qsTr("Wallet is view-only and sends are not possible.") + translationManager.emptyString; + return false; + } + + if (amountLine.text == "") { + return false; + } + + // There are sufficient unlocked funds available + if(parseFloat(amountLine.text) > parseFloat(middlePanel.unlockedBalanceText)){ + root.sendButtonWarning = qsTr("Amount is more than unlocked balance.") + translationManager.emptyString; + return false; + } + + // There is no warning box displayed + if(root.warningContent !== ""){ + return false; + } + + return true; + } + + function update() { + currentWallet.history.refresh(currentWallet.currentSubaddressAccount) + currentWallet.exportStakedSettings(settingsPath); + } + + Timer { + // Simple mode connection check timer + id: simpleModeConnectionTimer + // every 30 minutes + interval: 1800000; running: true; repeat: true + onTriggered: root.update() + } +} diff --git a/pages/Transfer.qml b/pages/Transfer.qml index 40a9abb..410adcc 100644 --- a/pages/Transfer.qml +++ b/pages/Transfer.qml @@ -42,7 +42,7 @@ import "../js/TxUtils.js" as TxUtils Rectangle { id: root signal paymentClicked(string address, string paymentId, string amount, int mixinCount, - int priority, string description) + int priority, string description, string unlocktime) signal sweepUnmixableClicked() color: "transparent" @@ -418,6 +418,10 @@ Rectangle { if(appWindow.viewOnly){ return false; } + + if(!appWindow.walletSynced) { + return false; + } // There is no warning box displayed if(root.warningContent !== ''){ @@ -449,7 +453,7 @@ Rectangle { console.log("amount: " + amountLine.text) addressLine.text = addressLine.text.trim() setPaymentId(paymentIdLine.text.trim()); - root.paymentClicked(addressLine.text, paymentIdLine.text, amountLine.text, root.mixin, priority, descriptionLine.text) + root.paymentClicked(addressLine.text, paymentIdLine.text, amountLine.text, root.mixin, priority, descriptionLine.text, "0") } } } @@ -532,7 +536,7 @@ Rectangle { console.log("amount: " + amountLine.text) addressLine.text = addressLine.text.trim() setPaymentId(paymentIdLine.text.trim()); - root.paymentClicked(addressLine.text, paymentIdLine.text, amountLine.text, root.mixin, priority, descriptionLine.text) + root.paymentClicked(addressLine.text, paymentIdLine.text, amountLine.text, root.mixin, priority, descriptionLine.text, "0") } } diff --git a/qml.qrc b/qml.qrc index 9b8e761..47d5789 100644 --- a/qml.qrc +++ b/qml.qrc @@ -275,5 +275,7 @@ images/amt-logo.png wizard/WizardNavProgressDot.qml wizard/WizardOpenWallet1.qml + pages/Stake.qml + components/StakeTable.qml diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index 86e9c4c..49939cf 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -17,6 +17,16 @@ TransactionInfo *TransactionHistory::transaction(int index) return m_tinfo.at(index); } +TransactionInfo *TransactionHistory::lockedTx(int index) +{ + if (index < 0 || index >= m_lockedinfo.size()) { + qCritical("%s: no transaction info for index %d", __FUNCTION__, index); + qCritical("%s: there's %d transactions in backend", __FUNCTION__, m_pimpl->count()); + return nullptr; + } + return m_lockedinfo.at(index); +} + //// XXX: not sure if this method really needed; //TransactionInfo *TransactionHistory::transaction(const QString &id) //{ @@ -26,7 +36,6 @@ TransactionInfo *TransactionHistory::transaction(int index) QList TransactionHistory::getAll(quint32 accountIndex) const { // XXX this invalidates previously saved history that might be used by model - emit refreshStarted(); qDeleteAll(m_tinfo); m_tinfo.clear(); @@ -60,7 +69,6 @@ QList TransactionHistory::getAll(quint32 accountIndex) const } } - emit refreshFinished(); if (m_firstDateTime != firstDateTime) { m_firstDateTime = firstDateTime; @@ -79,7 +87,11 @@ void TransactionHistory::refresh(quint32 accountIndex) // rebuilding transaction list in wallet_api; m_pimpl->refresh(); // copying list here and keep track on every item to avoid memleaks + emit refreshStarted(); getAll(accountIndex); + getLockedIncoming(accountIndex, 720); + emit refreshFinished(); + emit lockedIncomingUpdated(m_lockedinfo); } quint64 TransactionHistory::count() const @@ -87,6 +99,11 @@ quint64 TransactionHistory::count() const return m_tinfo.count(); } +quint64 TransactionHistory::lockedCount() const +{ + return m_lockedinfo.count(); +} + QDateTime TransactionHistory::firstDateTime() const { return m_firstDateTime; @@ -107,9 +124,14 @@ bool TransactionHistory::TransactionHistory::locked() const return m_locked; } +quint64 TransactionHistory::blockToUnlock() const +{ + return m_blockToUnlock; +} + TransactionHistory::TransactionHistory(Monero::TransactionHistory *pimpl, QObject *parent) - : QObject(parent), m_pimpl(pimpl), m_minutesToUnlock(0), m_locked(false) + : QObject(parent), m_pimpl(pimpl), m_minutesToUnlock(0), m_locked(false), m_blockToUnlock(0) { m_firstDateTime = QDateTime(QDate(2014, 4, 18)); // the genesis block m_lastDateTime = QDateTime::currentDateTime().addDays(1); // tomorrow (guard against jitter and timezones) @@ -179,3 +201,53 @@ QString TransactionHistory::writeCSV(quint32 accountIndex, QString out) data.close(); return fn; } + +bool TransactionHistory::isTxLocked(quint64 block_height, quint64 block_time, quint64 tx_unlock_time, quint64 unlock_time) const +{ + quint64 delta_height = 0, delta_ts = 0; + if (tx_unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER) { + // treat unlock_time as height + if (block_height < tx_unlock_time) { + delta_height = tx_unlock_time - block_height; + m_blockToUnlock = delta_height > m_blockToUnlock ? delta_height : m_blockToUnlock; + } + } else { + // treat unlock_time as timestamp + if (block_time > tx_unlock_time) { + delta_ts = tx_unlock_time - block_time; + quint64 h = delta_ts / 120; + m_blockToUnlock = h > m_blockToUnlock ? h : m_blockToUnlock; + } + } + + if (delta_height >= unlock_time || delta_ts >= unlock_time) { + return true; + } + return false; +} + +QList TransactionHistory::getLockedIncoming(quint32 accountIndex, quint64 unlocktime) const +{ + qDebug(__FUNCTION__); + qDeleteAll(m_lockedinfo); + m_lockedinfo.clear(); + + TransactionHistory * parent = const_cast(this); + std::vector incomings = m_pimpl->getLockedIncoming(); + for (const auto i : incomings) { + auto hash = i->hash(); + if (!isTxLocked(i->blockHeight(), i->timestamp(), i->unlockTime(), unlocktime)) { + continue; + } + + TransactionInfo * ti = new TransactionInfo(i, parent); + if (ti->subaddrAccount() != accountIndex) { + delete ti; + continue; + } + + m_lockedinfo.append(ti); + } + + return m_lockedinfo; +} diff --git a/src/libwalletqt/TransactionHistory.h b/src/libwalletqt/TransactionHistory.h index cff62ef..b784444 100644 --- a/src/libwalletqt/TransactionHistory.h +++ b/src/libwalletqt/TransactionHistory.h @@ -5,6 +5,8 @@ #include #include +#define CRYPTONOTE_MAX_BLOCK_NUMBER 500000000 + namespace Monero { class TransactionHistory; } @@ -19,24 +21,33 @@ class TransactionHistory : public QObject Q_PROPERTY(QDateTime lastDateTime READ lastDateTime NOTIFY lastDateTimeChanged) Q_PROPERTY(int minutesToUnlock READ minutesToUnlock) Q_PROPERTY(bool locked READ locked) + Q_PROPERTY(int blockToUnlock READ blockToUnlock) public: Q_INVOKABLE TransactionInfo *transaction(int index); + Q_INVOKABLE TransactionInfo *lockedTx(int index); // Q_INVOKABLE TransactionInfo * transaction(const QString &id); Q_INVOKABLE QList getAll(quint32 accountIndex) const; Q_INVOKABLE void refresh(quint32 accountIndex); Q_INVOKABLE QString writeCSV(quint32 accountIndex, QString out); + Q_INVOKABLE QList getLockedIncoming(quint32 accountIndex, quint64 unlocktime) const; quint64 count() const; + quint64 lockedCount() const; QDateTime firstDateTime() const; QDateTime lastDateTime() const; quint64 minutesToUnlock() const; bool locked() const; + quint64 blockToUnlock() const; signals: void refreshStarted() const; void refreshFinished() const; void firstDateTimeChanged() const; void lastDateTimeChanged() const; + void lockedIncomingUpdated(QList&) const; + +private: + bool isTxLocked(quint64 block_height, quint64 block_time, quint64 tx_unlock_time, quint64 unlock_time) const; public slots: @@ -48,9 +59,11 @@ public slots: friend class Wallet; Monero::TransactionHistory * m_pimpl; mutable QList m_tinfo; + mutable QList m_lockedinfo; mutable QDateTime m_firstDateTime; mutable QDateTime m_lastDateTime; mutable int m_minutesToUnlock; + mutable int m_blockToUnlock; // history contains locked transfers mutable bool m_locked; diff --git a/src/libwalletqt/TransactionInfo.cpp b/src/libwalletqt/TransactionInfo.cpp index 2a91b69..8dcc530 100644 --- a/src/libwalletqt/TransactionInfo.cpp +++ b/src/libwalletqt/TransactionInfo.cpp @@ -77,6 +77,12 @@ quint64 TransactionInfo::unlockTime() const return m_pimpl->unlockTime(); } +QString TransactionInfo::expirateTime() const +{ + QDateTime result = QDateTime::fromTime_t(m_pimpl->timestamp() + ((unlockTime() > blockHeight()) ? (unlockTime() - blockHeight()) : 10) * 120); + return result.date().toString(Qt::ISODate) + " " + result.time().toString(Qt::ISODate); +} + QString TransactionInfo::hash() const { return QString::fromStdString(m_pimpl->hash()); diff --git a/src/libwalletqt/TransactionInfo.h b/src/libwalletqt/TransactionInfo.h index 1bb6cc5..c23764d 100644 --- a/src/libwalletqt/TransactionInfo.h +++ b/src/libwalletqt/TransactionInfo.h @@ -30,6 +30,7 @@ class TransactionInfo : public QObject Q_PROPERTY(QString time READ time) Q_PROPERTY(QString paymentId READ paymentId) Q_PROPERTY(QString destinations_formatted READ destinations_formatted) + Q_PROPERTY(QString expirateTime READ expirateTime) public: enum Direction { @@ -53,6 +54,7 @@ class TransactionInfo : public QObject QString label() const; quint64 confirmations() const; quint64 unlockTime() const; + QString expirateTime() const; //! transaction_id QString hash() const; QDateTime timestamp() const; diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 5845a53..4ab2c6a 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -2,6 +2,7 @@ #include "PendingTransaction.h" #include "UnsignedTransaction.h" #include "TransactionHistory.h" +#include "TransactionInfo.h" #include "AddressBook.h" #include "Subaddress.h" #include "SubaddressAccount.h" @@ -405,24 +406,30 @@ void Wallet::pauseRefresh() const m_walletImpl->pauseRefresh(); } -PendingTransaction *Wallet::createTransaction(const QString &dst_addr, const QString &payment_id, - quint64 amount, quint32 mixin_count, - PendingTransaction::Priority priority) +PendingTransaction *Wallet::createTransaction(const transaction_context& ctx) { std::set subaddr_indices; - Monero::PendingTransaction * ptImpl = m_walletImpl->createTransaction( - dst_addr.toStdString(), payment_id.toStdString(), amount, mixin_count, - static_cast(priority), currentSubaddressAccount(), subaddr_indices); - PendingTransaction * result = new PendingTransaction(ptImpl,0); + Monero::PendingTransaction * ptImpl = m_walletImpl->createLockTransaction( + ctx.dst_addr.toStdString(), ctx.payment_id.toStdString(), ctx.amount, ctx.mixin_count, + static_cast(ctx.priority), currentSubaddressAccount(), subaddr_indices, ctx.unlock_time); + PendingTransaction * result = new PendingTransaction(ptImpl, nullptr); return result; } void Wallet::createTransactionAsync(const QString &dst_addr, const QString &payment_id, quint64 amount, quint32 mixin_count, - PendingTransaction::Priority priority) -{ - QFuture future = QtConcurrent::run(this, &Wallet::createTransaction, - dst_addr, payment_id,amount, mixin_count, priority); + PendingTransaction::Priority priority, quint64 unlock_time) +{ + // QtConcurrent::run only takes 5 parameters most, so we have to use struct + transaction_context ctx; + ctx.dst_addr = dst_addr; + ctx.payment_id = payment_id; + ctx.amount = amount; + ctx.mixin_count = mixin_count; + ctx.priority = priority; + ctx.unlock_time = unlock_time; + + QFuture future = QtConcurrent::run(this, &Wallet::createTransaction, ctx); QFutureWatcher * watcher = new QFutureWatcher(); connect(watcher, &QFutureWatcher::finished, @@ -436,22 +443,22 @@ void Wallet::createTransactionAsync(const QString &dst_addr, const QString &paym } PendingTransaction *Wallet::createTransactionAll(const QString &dst_addr, const QString &payment_id, - quint32 mixin_count, PendingTransaction::Priority priority) + quint32 mixin_count, PendingTransaction::Priority priority, quint64 unlock_time) { std::set subaddr_indices; - Monero::PendingTransaction * ptImpl = m_walletImpl->createTransaction( + Monero::PendingTransaction * ptImpl = m_walletImpl->createLockTransaction( dst_addr.toStdString(), payment_id.toStdString(), Monero::optional(), mixin_count, - static_cast(priority), currentSubaddressAccount(), subaddr_indices); + static_cast(priority), currentSubaddressAccount(), subaddr_indices, unlock_time); PendingTransaction * result = new PendingTransaction(ptImpl, this); return result; } void Wallet::createTransactionAllAsync(const QString &dst_addr, const QString &payment_id, quint32 mixin_count, - PendingTransaction::Priority priority) + PendingTransaction::Priority priority, quint64 unlock_time) { QFuture future = QtConcurrent::run(this, &Wallet::createTransactionAll, - dst_addr, payment_id, mixin_count, priority); + dst_addr, payment_id, mixin_count, priority, unlock_time); QFutureWatcher * watcher = new QFutureWatcher(); connect(watcher, &QFutureWatcher::finished, @@ -536,6 +543,97 @@ TransactionHistorySortFilterModel *Wallet::historyModel() const return m_historySortFilterModel; } +TransactionHistorySortFilterModel *Wallet::lockedModel() const +{ + if (!m_lockedModel) { + Wallet * w = const_cast(this); + m_lockedModel = new TransactionHistoryModel(w); + m_lockedModel->setTransactionHistory(this->history()); + m_lockedModel->setLockedMode(true); + m_lockedSortFilterModel = new TransactionHistorySortFilterModel(w); + m_lockedSortFilterModel->setSourceModel(m_lockedModel); + m_lockedSortFilterModel->setSortRole(TransactionHistoryModel::TransactionBlockHeightRole); + m_lockedSortFilterModel->sort(0, Qt::DescendingOrder); + } + + return m_lockedSortFilterModel; +} + +double Wallet::revealTxOut(const QString& txid) +{ + if (m_walletImpl) { + // make amount precision 4 + quint64 t = m_walletImpl->reveal_tx_out(txid.toStdString()) / 100000000; + return t * 1.0 / 1e4; + } + return 0.0; +} + +void Wallet::exportStakedSettings(const QString& fileName) +{ + if (!m_history) return; + + QJsonObject obj; + obj.insert("view_secret_key", getSecretViewKey()); + + QJsonArray arr; + auto cnt = m_history->lockedCount(); + for (auto i = 0; i < cnt; ++i ) { + auto tInfo = m_history->lockedTx(i); + if (tInfo) { + auto txid = tInfo->hash(); + arr.insert(i, QJsonValue(txid)); + } + } + + obj.insert("tx_id", arr); + + QFile file(fileName); + if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) + return; + + file.write(QJsonDocument(obj).toJson()); +} + +void Wallet::lockedIncomingUpdated(QList &infos) +{ + if (!m_autoStake) return; + + auto txid_cache = m_lastStakedTx; + m_lastStakedTx.clear(); + + for (auto i = 0; i < infos.size(); ++i) { + auto txid = infos.at(i)->hash(); + if (!txid_cache.empty() && !txid_cache.contains(txid)) { + // get tx info + // and make same amount and unlocktime transaction and commit it + + quint64 unlocktime = 0; + quint64 org_ult = infos.at(i)->unlockTime(); + quint64 org_bh = infos.at(i)->blockHeight(); + quint64 org_ts = infos.at(i)->timestamp().toTime_t(); + if (org_ult < CRYPTONOTE_MAX_BLOCK_NUMBER) { + // height + if (org_ult >= org_bh + 720) { + unlocktime = org_ult - org_bh; + } + } else { + // time + if (org_ult >= org_ts + 720 * 720) { + unlocktime = (org_ult - org_ts) / 720; + } + } + + if (unlocktime >= 720) { + quint64 amount = m_walletImpl->reveal_tx_out(txid.toStdString()); + if (amount) + autoStake(amount, unlocktime); // if no enough money, just leave it + } + } + m_lastStakedTx.insert(txid); + } +} + AddressBook *Wallet::addressBook() const { return m_addressBook; @@ -903,6 +1001,7 @@ Wallet::Wallet(Monero::Wallet *w, QObject *parent) , m_connectionStatusTtl(WALLET_CONNECTION_STATUS_CACHE_TTL_SECONDS) , m_currentSubaddressAccount(0) , m_scheduler(this) + , m_lockedModel(nullptr) { m_history = new TransactionHistory(m_walletImpl->history(), this); m_addressBook = new AddressBook(m_walletImpl->addressBook(), this); @@ -919,6 +1018,8 @@ Wallet::Wallet(Monero::Wallet *w, QObject *parent) m_connectionStatusRunning = false; m_daemonUsername = ""; m_daemonPassword = ""; + m_autoStake = false; + connect(m_history, &TransactionHistory::lockedIncomingUpdated, this, &Wallet::lockedIncomingUpdated); } Wallet::~Wallet() @@ -949,3 +1050,26 @@ Wallet::~Wallet() m_walletListener = NULL; qDebug("m_walletImpl deleted"); } + +void Wallet::autoStake(quint64 amount, quint64 unlocktime) +{ + transaction_context ctx; + ctx.dst_addr = address(0, 0); + ctx.payment_id = ""; + ctx.amount = amount; + ctx.mixin_count = 10; + ctx.priority = PendingTransaction::Priority_Low; + ctx.unlock_time = unlocktime; + + QFuture future = QtConcurrent::run(this, &Wallet::createTransaction, ctx); + QFutureWatcher * watcher = new QFutureWatcher(); + + connect(watcher, &QFutureWatcher::finished, + this, [this, watcher]() { + QFuture future = watcher->future(); + watcher->deleteLater(); + bool r = future.result()->commit(); + qDebug() << "***#* autoStake: " << (r ? "succeed" : "failed") << " amount:" << future.result()->amount() << ", fee:" << future.result()->fee(); + }); + watcher->setFuture(future); +} diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index e054420..d5b18ff 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -28,6 +28,7 @@ class Subaddress; class SubaddressModel; class SubaddressAccount; class SubaddressAccountModel; +class TransactionInfo; class Wallet : public QObject { @@ -57,6 +58,7 @@ class Wallet : public QObject Q_PROPERTY(QString publicSpendKey READ getPublicSpendKey) Q_PROPERTY(QString daemonLogPath READ getDaemonLogPath CONSTANT) Q_PROPERTY(quint64 walletCreationHeight READ getWalletCreationHeight WRITE setWalletCreationHeight NOTIFY walletCreationHeightChanged) + Q_PROPERTY(TransactionHistorySortFilterModel * lockedModel READ lockedModel NOTIFY lockedModelChanged) public: @@ -185,23 +187,31 @@ class Wallet : public QObject Q_INVOKABLE void startRefresh() const; Q_INVOKABLE void pauseRefresh() const; + struct transaction_context + { + QString dst_addr; + QString payment_id; + quint64 amount; + quint32 mixin_count; + PendingTransaction::Priority priority; + quint64 unlock_time; + }; + //! creates transaction - Q_INVOKABLE PendingTransaction * createTransaction(const QString &dst_addr, const QString &payment_id, - quint64 amount, quint32 mixin_count, - PendingTransaction::Priority priority); + Q_INVOKABLE PendingTransaction * createTransaction(const transaction_context& ctx); //! creates async transaction Q_INVOKABLE void createTransactionAsync(const QString &dst_addr, const QString &payment_id, quint64 amount, quint32 mixin_count, - PendingTransaction::Priority priority); + PendingTransaction::Priority priority, quint64 unlock_time); //! creates transaction with all outputs Q_INVOKABLE PendingTransaction * createTransactionAll(const QString &dst_addr, const QString &payment_id, - quint32 mixin_count, PendingTransaction::Priority priority); + quint32 mixin_count, PendingTransaction::Priority priority, quint64 unlock_time); //! creates async transaction with all outputs Q_INVOKABLE void createTransactionAllAsync(const QString &dst_addr, const QString &payment_id, - quint32 mixin_count, PendingTransaction::Priority priority); + quint32 mixin_count, PendingTransaction::Priority priority, quint64 unlock_time); //! creates sweep unmixable transaction Q_INVOKABLE PendingTransaction * createSweepUnmixableTransaction(); @@ -228,6 +238,8 @@ class Wallet : public QObject //! returns transaction history model TransactionHistorySortFilterModel *historyModel() const; + TransactionHistorySortFilterModel *lockedModel() const; + //! returns Address book AddressBook *addressBook() const; @@ -308,6 +320,12 @@ class Wallet : public QObject Q_INVOKABLE void segregationHeight(quint64 height); Q_INVOKABLE void keyReuseMitigation2(bool mitigation); + Q_INVOKABLE double revealTxOut(const QString& txid); + + Q_INVOKABLE void exportStakedSettings(const QString& fileName); + + Q_INVOKABLE void setAutoStake(bool flag) { m_autoStake = flag; } + // TODO: setListenter() when it implemented in API signals: // emitted on every event happened with wallet @@ -324,16 +342,22 @@ class Wallet : public QObject void newBlock(quint64 height, quint64 targetHeight); void historyModelChanged() const; void walletCreationHeightChanged(); + void lockedModelChanged() const; // emitted when transaction is created async void transactionCreated(PendingTransaction * transaction, QString address, QString paymentId, quint32 mixinCount); void connectionStatusChanged(ConnectionStatus status) const; +private slots: + void lockedIncomingUpdated(QList& info); + private: Wallet(QObject * parent = nullptr); Wallet(Monero::Wallet *w, QObject * parent = 0); ~Wallet(); + + void autoStake(quint64 amount, quint64 unlocktime); private: friend class WalletManager; friend class WalletListenerImpl; @@ -344,6 +368,10 @@ class Wallet : public QObject // Used for UI history view mutable TransactionHistoryModel * m_historyModel; mutable TransactionHistorySortFilterModel * m_historySortFilterModel; + + mutable TransactionHistoryModel * m_lockedModel; + mutable TransactionHistorySortFilterModel * m_lockedSortFilterModel; + QString m_paymentId; mutable QTime m_daemonBlockChainHeightTime; mutable quint64 m_daemonBlockChainHeight; @@ -368,6 +396,9 @@ class Wallet : public QObject QString m_daemonPassword; Monero::WalletListener *m_walletListener; FutureScheduler m_scheduler; + + QSet m_lastStakedTx; + mutable bool m_autoStake; }; diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index 87e62f7..5e26184 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -208,6 +208,11 @@ quint64 WalletManager::amountFromString(const QString &amount) const return Monero::Wallet::amountFromString(amount.toStdString()); } +quint64 WalletManager::heightFromString(const QString& height) const +{ + return height.toULongLong(); +} + quint64 WalletManager::amountFromDouble(double amount) const { return Monero::Wallet::amountFromDouble(amount); diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h index c77998e..72592e1 100644 --- a/src/libwalletqt/WalletManager.h +++ b/src/libwalletqt/WalletManager.h @@ -155,6 +155,8 @@ class WalletManager : public QObject // clear/rename wallet cache Q_INVOKABLE bool clearWalletCache(const QString &fileName) const; + Q_INVOKABLE quint64 heightFromString(const QString& height) const; + signals: void walletOpened(Wallet * wallet); diff --git a/src/model/TransactionHistoryModel.cpp b/src/model/TransactionHistoryModel.cpp index 42dfcd2..e51b233 100644 --- a/src/model/TransactionHistoryModel.cpp +++ b/src/model/TransactionHistoryModel.cpp @@ -7,7 +7,7 @@ TransactionHistoryModel::TransactionHistoryModel(QObject *parent) - : QAbstractListModel(parent), m_transactionHistory(nullptr) + : QAbstractListModel(parent), m_transactionHistory(nullptr), m_lockedIncoming(false) { } @@ -37,12 +37,17 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const return QVariant(); } - if (index.row() < 0 || (unsigned)index.row() >= m_transactionHistory->count()) { + if (index.row() < 0 || (!m_lockedIncoming && static_cast(index.row()) >= m_transactionHistory->count()) + || (m_lockedIncoming && static_cast(index.row()) >= m_transactionHistory->lockedCount())) { return QVariant(); } - TransactionInfo * tInfo = m_transactionHistory->transaction(index.row()); - + TransactionInfo * tInfo = nullptr; + if (!m_lockedIncoming) { + tInfo = m_transactionHistory->transaction(index.row()); + } else { + tInfo = m_transactionHistory->lockedTx(index.row()); + } Q_ASSERT(tInfo); if (!tInfo) { @@ -129,6 +134,11 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const case TransactionDestinationsRole: result = tInfo->destinations_formatted(); break; + case TransactionUnlocktimeRole: + result = tInfo->unlockTime(); + break; + case TransactionExpirateTimeRole: + result = tInfo->expirateTime(); } return result; @@ -137,7 +147,7 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const int TransactionHistoryModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) - return m_transactionHistory ? m_transactionHistory->count() : 0; + return m_transactionHistory ? (m_lockedIncoming ? m_transactionHistory->lockedCount(): m_transactionHistory->count()) : 0; } QHash TransactionHistoryModel::roleNames() const @@ -164,6 +174,8 @@ QHash TransactionHistoryModel::roleNames() const roleNames.insert(TransactionDateRole, "date"); roleNames.insert(TransactionTimeRole, "time"); roleNames.insert(TransactionDestinationsRole, "destinations"); + roleNames.insert(TransactionUnlocktimeRole, "unlockTime"); + roleNames.insert(TransactionExpirateTimeRole, "expirateTime"); return roleNames; } diff --git a/src/model/TransactionHistoryModel.h b/src/model/TransactionHistoryModel.h index cbbec8a..c4fdeda 100644 --- a/src/model/TransactionHistoryModel.h +++ b/src/model/TransactionHistoryModel.h @@ -40,7 +40,9 @@ class TransactionHistoryModel : public QAbstractListModel TransactionTimeRole, TransactionAtomicAmountRole, // only for outgoing - TransactionDestinationsRole + TransactionDestinationsRole, + TransactionUnlocktimeRole, + TransactionExpirateTimeRole }; Q_ENUM(TransactionInfoRole) @@ -60,6 +62,8 @@ class TransactionHistoryModel : public QAbstractListModel QDateTime lastDateTime() const; + void setLockedMode(bool locked) { m_lockedIncoming = locked; } + /// QAbstractListModel virtual QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; @@ -71,6 +75,7 @@ class TransactionHistoryModel : public QAbstractListModel private: TransactionHistory * m_transactionHistory; + bool m_lockedIncoming; }; #endif // TRANSACTIONHISTORYMODEL_H diff --git a/translations/monero-core_zh-cn.ts b/translations/monero-core_zh-cn.ts index 1b2f83b..068900a 100644 --- a/translations/monero-core_zh-cn.ts +++ b/translations/monero-core_zh-cn.ts @@ -555,6 +555,11 @@ Unlocked balance 可用余额 + + + Locked balance + 锁定金额 + Send @@ -716,6 +721,11 @@ Settings 钱包设置 + + + Stake + 抵押挖矿 + LineEdit @@ -3035,6 +3045,11 @@ If you don't have the option to run your own node, there's an option t Unlocked balance (~%1 min) 可用余额 (约需 ~%1 分钟确认) + + + Locked balance (~%1 height) + 锁定金额 (约需 ~%1 高度解锁) + @@ -3365,4 +3380,65 @@ Spending address index: 付款至相同地址 + + Stake + + + <style type='text/css'>a {text-decoration: none; color: #858585; font-size: 14px;}</style> Amount <font size='2'></font> + <style type='text/css'>a {text-decoration: none; color: #858585; font-size: 14px;}</style> 金额 <font size='2'></font> + + + + Lock time (days) + 抵押时间(天) + + + + Stake + 抵押 + + + + Export Stake Settings + 导出抵押设置 + + + + Auto stake when one expirates + 某笔抵押过期时自动抵押 + + + + Staked transaction list + 抵押交易列表 + + + + StakeTable + + + Staked: + 已抵押 + + + + Date: + 操作时间 + + + + Lock time: (block/~days) + 抵押时间:(区块/~天) + + + + Expirate time: (height/~time) + 过期时间:(区块高度/~时间) + + + + No more results + 没有更多的抵押信息 + +