diff --git a/src/BlockchainBackend.cpp b/src/BlockchainBackend.cpp index 2ca7a91..96c3f93 100644 --- a/src/BlockchainBackend.cpp +++ b/src/BlockchainBackend.cpp @@ -230,6 +230,15 @@ QVariantMap BlockchainBackend::getPeerId() BLOCKCHAIN_MODULE_NAME, QStringLiteral("get_peer_id"), userConfig()))); } +QVariantMap BlockchainBackend::getClaimableVouchers() +{ + if (!m_blockchainClient) + return result::toVariantMap(result::err(QStringLiteral("Module not initialized."))); + + return result::toVariantMap(result::toLogosResult(m_blockchainClient->invokeRemoteMethod( + BLOCKCHAIN_MODULE_NAME, QStringLiteral("wallet_get_claimable_vouchers")))); +} + void BlockchainBackend::startBlockchain() { if (!m_blockchainClient) { diff --git a/src/BlockchainBackend.h b/src/BlockchainBackend.h index 5a52fe1..bf3ccc0 100644 --- a/src/BlockchainBackend.h +++ b/src/BlockchainBackend.h @@ -47,6 +47,7 @@ public slots: QVariantMap claimLeaderRewards() override; QVariantMap getCryptarchiaInfo() override; QVariantMap getPeerId() override; + QVariantMap getClaimableVouchers() override; QVariantMap generateConfig(QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, diff --git a/src/BlockchainBackend.rep b/src/BlockchainBackend.rep index 4b280bc..10190dd 100644 --- a/src/BlockchainBackend.rep +++ b/src/BlockchainBackend.rep @@ -17,6 +17,7 @@ class BlockchainBackend SLOT(QVariantMap claimLeaderRewards()) SLOT(QVariantMap getCryptarchiaInfo()) SLOT(QVariantMap getPeerId()) + SLOT(QVariantMap getClaimableVouchers()) SLOT(QVariantMap generateConfig(QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, QString deploymentConfigPath, QString statePath)) SLOT(QVariantMap getNotes(QString walletAddressHex, QString optionalTipHex)) SLOT(QVariantMap channelDepositWithNotes(QString channelIdHex, QStringList inputNoteIdHexes, QString metadataBase58, QString changePublicKeyHex, QStringList fundingPublicKeyHexes, QString maxTxFee, QString optionalTipHex)) diff --git a/src/qml/BlockchainView.qml b/src/qml/BlockchainView.qml index 151e069..6656a5d 100644 --- a/src/qml/BlockchainView.qml +++ b/src/qml/BlockchainView.qml @@ -147,6 +147,39 @@ Rectangle { } } + // Wallet's claimable ("pending") vouchers. Auto-refreshed on every incoming + // block, and once when the node starts running. + property string claimableVouchersJson: "" + + function refreshClaimableVouchers() { + if (!root.backend || root.backend.status !== BlockchainBackend.Running) + return + logos.watch( + root.backend.getClaimableVouchers(), + function(result) { if (result.success) root.claimableVouchersJson = result.value }, + function(error) { /* keep last known list on transient errors */ } + ) + } + + // Incoming blocks arrive as row insertions on the remoted block model. + Connections { + target: root.blockModel + enabled: root.blockModel !== null + ignoreUnknownSignals: true + function onRowsInserted() { root.refreshClaimableVouchers() } + } + + // Initial load when the node reaches Running (before the next block). + Connections { + target: root.backend + enabled: root.backend !== null + ignoreUnknownSignals: true + function onStatusChanged() { + if (root.backend.status === BlockchainBackend.Running) + root.refreshClaimableVouchers() + } + } + QtObject { id: _d function errorText(message) { @@ -478,6 +511,7 @@ Rectangle { LeaderRewardsView { id: leaderRewardsView + vouchersJson: root.claimableVouchersJson onClaimLeaderRewardsRequested: function() { if (!root.backend) return @@ -489,6 +523,8 @@ Rectangle { } else { leaderRewardsView.setLeaderClaimResult(_d.errorText(result.error)) } + // Reflect the claim in the pending list. + root.refreshClaimableVouchers() }, function(error) { leaderRewardsView.setLeaderClaimResult(_d.errorText(error)) } ) diff --git a/src/qml/views/LeaderRewardsView.qml b/src/qml/views/LeaderRewardsView.qml index e14d6db..cbe5e4d 100644 --- a/src/qml/views/LeaderRewardsView.qml +++ b/src/qml/views/LeaderRewardsView.qml @@ -7,10 +7,16 @@ import Logos.Controls import "../controls" -// Leader rewards panel. Extracted from the former WalletView. +// Leader rewards panel: claim action plus a read-only, horizontally-sliding +// list of the wallet's claimable ("pending") vouchers with a count. The +// protocol picks which voucher a claim consumes — the list is informational. ColumnLayout { id: root + // JSON from wallet_get_claimable_vouchers: + // { "tip": "", "vouchers": [ {commitment, nullifier}, ... ] } + property string vouchersJson: "" + signal claimLeaderRewardsRequested() signal copyToClipboard(string text) @@ -18,65 +24,168 @@ ColumnLayout { leaderClaimResultText.text = text } + readonly property var _parsed: safeParse(vouchersJson) + readonly property var vouchers: (_parsed && _parsed.vouchers) + ? _parsed.vouchers + : (Array.isArray(_parsed) ? _parsed : []) + readonly property string tip: (_parsed && _parsed.tip) ? String(_parsed.tip) : "" + + function safeParse(s) { + try { return s && s.length > 0 ? JSON.parse(s) : null } catch (e) { return null } + } + spacing: Theme.spacing.large + // ---- Claimable vouchers card ---- Rectangle { - id: leaderRewardsRect - Layout.fillWidth: true - Layout.preferredHeight: leaderRewardsCol.height + 2 * Theme.spacing.large + Layout.preferredHeight: vouchersCol.implicitHeight + 2 * Theme.spacing.large color: Theme.palette.backgroundTertiary radius: Theme.spacing.radiusLarge border.color: Theme.palette.border border.width: 1 ColumnLayout { - id: leaderRewardsCol + id: vouchersCol anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: Theme.spacing.large spacing: Theme.spacing.small - LogosText { - text: qsTr("Leader rewards") - font.pixelSize: Theme.typography.secondaryText - font.bold: true - } - RowLayout { Layout.fillWidth: true - - LogosButton { - id: leaderClaimButton - Layout.preferredWidth: 140 - text: qsTr("Claim") - onClicked: root.claimLeaderRewardsRequested() + spacing: Theme.spacing.small + LogosText { + text: qsTr("Claimable vouchers") + font.pixelSize: Theme.typography.secondaryText + font.bold: true } - - LogosButton { - Layout.fillWidth: true - enabled: true - padding: Theme.spacing.small - contentItem: RowLayout { - width: parent.width + // Pending counter badge + Rectangle { + radius: Theme.spacing.radiusSmall + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.border + border.width: 1 + implicitWidth: pendingText.implicitWidth + 2 * Theme.spacing.small + implicitHeight: pendingText.implicitHeight + Theme.spacing.tiny + LogosText { + id: pendingText anchors.centerIn: parent + text: qsTr("%1 pending").arg(root.vouchers.length) + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + } + Item { Layout.fillWidth: true } + LogosText { + visible: root.tip.length > 0 + text: qsTr("tip %1").arg(root.tip) + elide: Text.ElideMiddle + Layout.maximumWidth: 140 + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + } + + // Horizontally-sliding list of voucher cards. + ListView { + id: vouchersList + Layout.fillWidth: true + Layout.preferredHeight: 78 + visible: root.vouchers.length > 0 + orientation: ListView.Horizontal + clip: true + spacing: Theme.spacing.small + model: root.vouchers + snapMode: ListView.SnapToItem + ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded } + + delegate: Rectangle { + width: 260 + height: ListView.view.height + radius: Theme.spacing.radiusSmall + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.border + border.width: 1 + + ColumnLayout { + anchors.fill: parent + anchors.margins: Theme.spacing.small + spacing: Theme.spacing.tiny + LogosText { - id: leaderClaimResultText - Layout.fillWidth: true - color: Theme.palette.textSecondary + text: qsTr("Voucher %1").arg(index + 1) font.pixelSize: Theme.typography.secondaryText - font.weight: Theme.typography.weightMedium - wrapMode: Text.WordWrap - elide: Text.ElideRight + font.bold: true + color: Theme.palette.textSecondary } - LogosCopyButton { - Layout.alignment: Qt.AlignRight - Layout.preferredHeight: 40 - Layout.preferredWidth: 40 - onCopyText: root.copyToClipboard(leaderClaimResultText.text) - visible: leaderClaimResultText.text + VoucherField { + label: qsTr("cm") + value: modelData && modelData.commitment ? String(modelData.commitment) : "" } + VoucherField { + label: qsTr("nf") + value: modelData && modelData.nullifier ? String(modelData.nullifier) : "" + } + } + } + } + + LogosText { + visible: root.vouchers.length === 0 + text: qsTr("No claimable vouchers.") + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + } + } + } + + // ---- Claim action ---- + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: claimRow.height + 2 * Theme.spacing.large + color: Theme.palette.backgroundTertiary + radius: Theme.spacing.radiusLarge + border.color: Theme.palette.border + border.width: 1 + + RowLayout { + id: claimRow + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacing.large + + LogosButton { + id: leaderClaimButton + Layout.preferredWidth: 140 + text: qsTr("Claim") + onClicked: root.claimLeaderRewardsRequested() + } + + LogosButton { + Layout.fillWidth: true + enabled: true + padding: Theme.spacing.small + contentItem: RowLayout { + width: parent.width + anchors.centerIn: parent + LogosText { + id: leaderClaimResultText + Layout.fillWidth: true + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + font.weight: Theme.typography.weightMedium + wrapMode: Text.WordWrap + elide: Text.ElideRight + } + LogosCopyButton { + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 + onCopyText: root.copyToClipboard(leaderClaimResultText.text) + visible: leaderClaimResultText.text } } } @@ -84,4 +193,32 @@ ColumnLayout { } Item { Layout.fillHeight: true } + + // A labelled, elided, copyable hash inside a voucher card. + component VoucherField: RowLayout { + id: vf + property string label: "" + property string value: "" + Layout.fillWidth: true + spacing: Theme.spacing.tiny + LogosText { + text: vf.label + Layout.preferredWidth: 20 + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + } + LogosText { + Layout.fillWidth: true + text: vf.value || "—" + elide: Text.ElideMiddle + font.pixelSize: Theme.typography.secondaryText + font.family: "monospace" + } + LogosCopyButton { + Layout.preferredHeight: 24 + Layout.preferredWidth: 24 + visible: vf.value.length > 0 + onCopyText: root.copyToClipboard(vf.value) + } + } }