Add vouchers view to leader claim with pending counter

This commit is contained in:
Daniel 2026-06-22 17:52:14 +02:00
parent c47fc8ddea
commit 8ae4c07b4b
5 changed files with 220 additions and 36 deletions

View File

@ -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) {

View File

@ -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,

View File

@ -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))

View File

@ -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)) }
)

View File

@ -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": "<hex>", "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)
}
}
}