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
+ 没有更多的抵押信息
+
+