feat: Deposit ui (#23)

* Implement deposit ui

* Update lock file

* Fix accounts

* Make deposit work
This commit is contained in:
Daniel Sanchez 2026-06-20 14:29:03 +02:00 committed by GitHub
parent f953d4e3d6
commit 32257d20e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 919 additions and 13 deletions

View File

@ -4,6 +4,7 @@
#include <QByteArray>
#include <QClipboard>
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
@ -16,6 +17,8 @@
#include <QUrl>
#include <QVariant>
#include <algorithm>
const QString BlockchainBackend::BLOCKCHAIN_MODULE_NAME =
QStringLiteral("liblogos_blockchain_module");
@ -56,6 +59,44 @@ static QString toDisplayMessage(const LogosResult& result)
return result.success ? result.value.toString() : toErrorMessage(result);
}
// Decode a base58 (Bitcoin alphabet) string to raw bytes. On an invalid
// character *ok is set to false and an empty array is returned.
static QByteArray decodeBase58(const QString& input, bool* ok)
{
static const QByteArray kAlphabet =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const QByteArray s = input.trimmed().toLatin1();
QByteArray bytes; // little-endian while building, reversed at the end
bytes.append('\0');
for (const char c : s) {
const int value = kAlphabet.indexOf(c);
if (value < 0) {
if (ok) *ok = false;
return {};
}
int carry = value;
for (int j = 0; j < bytes.size(); ++j) {
carry += static_cast<unsigned char>(bytes[j]) * 58;
bytes[j] = static_cast<char>(carry & 0xff);
carry >>= 8;
}
while (carry > 0) {
bytes.append(static_cast<char>(carry & 0xff));
carry >>= 8;
}
}
// Each leading '1' maps to a leading zero byte.
for (int i = 0; i < s.size() && s[i] == '1'; ++i)
bytes.append('\0');
std::reverse(bytes.begin(), bytes.end());
if (ok) *ok = true;
return bytes;
}
BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent)
: BlockchainBackendSimpleSource(parent)
, m_logosAPI(logosAPI)
@ -205,9 +246,28 @@ void BlockchainBackend::refreshAccounts()
const LogosResult result = toLogosResult(m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "wallet_get_known_addresses"));
if (!result.success) {
qWarning() << "refreshAccounts: failed:" << result.error.toString();
return;
}
// The SDK marshals the JSON array into a QVariantList; rely on toList()
// rather than canConvert<QStringList>() (which is unreliable for a
// QVariantList under Qt6), and fall back to toStringList() for the rare
// case where the value already arrives as a QStringList.
QStringList list;
if (result.success && result.value.canConvert<QStringList>())
const QVariantList items = result.value.toList();
if (!items.isEmpty()) {
for (const QVariant& item : items) {
const QString addr = item.toString();
if (!addr.isEmpty())
list << addr;
}
} else {
list = result.value.toStringList();
}
qDebug() << "refreshAccounts: loaded" << list.size() << "addresses";
m_accountsModel->setAddresses(list);
@ -310,6 +370,46 @@ int BlockchainBackend::generateConfig(
return result.success ? 0 : -1;
}
QString BlockchainBackend::getNotes(QString walletAddressHex, QString optionalTipHex)
{
if (!m_blockchainClient)
return QStringLiteral("Error: Module not initialized.");
return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "wallet_get_notes",
walletAddressHex, optionalTipHex)));
}
QString BlockchainBackend::channelDepositWithNotes(
QString channelIdHex, QStringList inputNoteIdHexes, QString metadataBase58,
QString changePublicKeyHex, QStringList fundingPublicKeyHexes,
QString maxTxFee, QString optionalTipHex)
{
if (!m_blockchainClient)
return QStringLiteral("Error: Module not initialized.");
// The metadata arrives base58-encoded; the module expects metadata_hex, so
// decode to bytes and hex-encode. Empty stays empty (metadata is optional).
QString metadataHex;
if (!metadataBase58.trimmed().isEmpty()) {
bool ok = false;
const QByteArray bytes = decodeBase58(metadataBase58, &ok);
if (!ok)
return QStringLiteral("Error: Invalid base58 metadata.");
metadataHex = QString::fromLatin1(bytes.toHex());
}
// 7 positional args exceed the variadic invokeRemoteMethod overloads
// (max 5), so pass them through the QVariantList form.
QVariantList args;
args << channelIdHex << inputNoteIdHexes << metadataHex << changePublicKeyHex
<< fundingPublicKeyHexes << maxTxFee << optionalTipHex;
return toDisplayMessage(toLogosResult(m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, QStringLiteral("channel_deposit_with_notes"),
args)));
}
void BlockchainBackend::clearLogs()
{
m_logModel->clear();
@ -317,6 +417,14 @@ void BlockchainBackend::clearLogs()
void BlockchainBackend::copyToClipboard(QString text)
{
if (QGuiApplication::clipboard())
QGuiApplication::clipboard()->setText(text);
// The backend runs in a non-GUI ViewModuleHost subprocess, where there is
// no QGuiApplication and accessing the clipboard segfaults. Clipboard is
// handled QML-side (see BlockchainView.copyText); guard here so any stray
// call is a no-op rather than a crash.
if (!qobject_cast<QGuiApplication*>(QCoreApplication::instance())) {
qWarning() << "copyToClipboard: no GUI application; ignoring";
return;
}
if (QClipboard* clipboard = QGuiApplication::clipboard())
clipboard->setText(text);
}

View File

@ -48,6 +48,14 @@ public slots:
int blendPort, QString httpAddr, QString externalAddress,
bool noPublicIpCheck, int deploymentMode,
QString deploymentConfigPath, QString statePath) override;
QString getNotes(QString walletAddressHex, QString optionalTipHex) override;
QString channelDepositWithNotes(QString channelIdHex,
QStringList inputNoteIdHexes,
QString metadataBase58,
QString changePublicKeyHex,
QStringList fundingPublicKeyHexes,
QString maxTxFee,
QString optionalTipHex) override;
void clearLogs() override;
void copyToClipboard(QString text) override;

View File

@ -16,6 +16,8 @@ class BlockchainBackend
SLOT(QString transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr))
SLOT(QString claimLeaderRewards())
SLOT(int generateConfig(QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, QString deploymentConfigPath, QString statePath))
SLOT(QString getNotes(QString walletAddressHex, QString optionalTipHex))
SLOT(QString channelDepositWithNotes(QString channelIdHex, QStringList inputNoteIdHexes, QString metadataBase58, QString changePublicKeyHex, QStringList fundingPublicKeyHexes, QString maxTxFee, QString optionalTipHex))
SLOT(void clearLogs())
SLOT(void copyToClipboard(QString text))
}

View File

@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
// BlockchainStatus enum (NotStarted/Starting/Running/Stopping/Stopped/Error)
// declared in BlockchainBackend.rep registered with QML by the replica
// factory plugin.
@ -39,6 +40,23 @@ Rectangle {
readonly property var accountsModel: logos.model("blockchain_ui", "accounts")
readonly property var logModel: logos.model("blockchain_ui", "logs")
// Clipboard must be handled here in the UI-host (GUI) process. The backend
// .rep source runs in a separate, non-GUI ViewModuleHost subprocess where
// QGuiApplication::clipboard() segfaults (process exits with code 11), so
// we copy from QML via a hidden TextEdit instead of calling the backend.
function copyText(text) {
clipboardHelper.text = text || ""
clipboardHelper.selectAll()
clipboardHelper.copy()
clipboardHelper.deselect()
clipboardHelper.text = ""
}
TextEdit {
id: clipboardHelper
visible: false
}
QtObject {
id: _d
function getStatusString(s) {
@ -150,7 +168,27 @@ Rectangle {
}
}
// Page 2: Node control, wallet, logs
// Page 2: Node control, wallet, logs + Channel Deposit (tabbed)
ColumnLayout {
spacing: Theme.spacing.medium
LogosTabBar {
id: operationTabBar
Layout.fillWidth: true
LogosTabButton { text: qsTr("Node & Wallet") }
LogosTabButton {
text: qsTr("Channel Deposit")
enabled: root.backend
&& root.backend.status === BlockchainBackend.Running
}
}
StackLayout {
id: operationStack
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: operationTabBar.currentIndex
SplitView {
orientation: Qt.Vertical
@ -207,7 +245,7 @@ Rectangle {
)
}
onCopyToClipboard: (text) => {
if (root.backend) root.backend.copyToClipboard(text)
root.copyText(text)
}
onTransferRequested: function(fromKeyHex, toKeyHex, amount) {
if (!root.backend) return
@ -225,6 +263,7 @@ Rectangle {
function(error) { walletView.setLeaderClaimResult("Error: " + error) }
)
}
onRefreshAccountsRequested: if (root.backend) root.backend.refreshAccounts()
}
Item {
@ -239,7 +278,39 @@ Rectangle {
logModel: root.logModel
onClearRequested: if (root.backend) root.backend.clearLogs()
onCopyToClipboard: (text) => {
if (root.backend) root.backend.copyToClipboard(text)
root.copyText(text)
}
}
}
ChannelDepositView {
id: channelDepositView
accountsModel: root.accountsModel
nodeRunning: root.backend
? root.backend.status === BlockchainBackend.Running
: false
onGetNotesRequested: function(addressHex, optionalTipHex) {
if (!root.backend) return
logos.watch(
root.backend.getNotes(addressHex, optionalTipHex),
function(result) { channelDepositView.setNotesResult(result) },
function(error) { channelDepositView.setNotesResult("Error: " + error) }
)
}
onSubmitRequested: function(channelIdHex, inputNoteIdHexes, metadataBase58, changePublicKeyHex, fundingPublicKeyHexes, maxTxFee, optionalTipHex) {
if (!root.backend) return
logos.watch(
root.backend.channelDepositWithNotes(
channelIdHex, inputNoteIdHexes, metadataBase58,
changePublicKeyHex, fundingPublicKeyHexes, maxTxFee, optionalTipHex),
function(result) { channelDepositView.setSubmitResult(result) },
function(error) { channelDepositView.setSubmitResult("Error: " + error) }
)
}
onCopyToClipboard: (text) => {
root.copyText(text)
}
}
}
}

View File

@ -0,0 +1,162 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
// Widget for selecting spendable notes (UTXOs) to consume in a channel deposit.
//
// The owner sets `notes` (a JS array of { id, value } parsed from
// wallet_get_notes) and reads back the selection via selectedIds() and the
// selectedCount / selectedTotal properties.
ColumnLayout {
id: root
// Array of { id: "<hex>", value: "<u64-string>" }. Assign to (re)populate.
property var notes: []
property bool loading: false
property string errorText: ""
// Observable selection summary bindings can't track ListModel reads, so
// these properties are recomputed explicitly on every selection change.
property int selectedCount: 0
property double selectedTotal: 0
function selectedIds() {
var ids = []
for (var i = 0; i < notesModel.count; ++i) {
var n = notesModel.get(i)
if (n.selected)
ids.push(n.noteId)
}
return ids
}
function recompute() {
var c = 0
var total = 0
for (var i = 0; i < notesModel.count; ++i) {
var n = notesModel.get(i)
if (n.selected) {
c++
total += Number(n.value)
}
}
root.selectedCount = c
root.selectedTotal = total
}
function clearSelection() {
for (var i = 0; i < notesModel.count; ++i)
notesModel.setProperty(i, "selected", false)
recompute()
}
onNotesChanged: {
notesModel.clear()
var arr = root.notes || []
for (var i = 0; i < arr.length; ++i) {
notesModel.append({
noteId: String(arr[i].id),
value: String(arr[i].value),
selected: false
})
}
recompute()
}
spacing: Theme.spacing.small
ListModel { id: notesModel }
LogosText {
Layout.fillWidth: true
visible: root.loading
text: qsTr("Loading notes…")
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
}
LogosText {
Layout.fillWidth: true
visible: !root.loading && root.errorText !== ""
text: root.errorText
color: Theme.palette.error
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
}
LogosText {
Layout.fillWidth: true
visible: !root.loading && root.errorText === "" && notesModel.count === 0
text: qsTr("No notes found for this address. Load notes for a funded address.")
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 220
visible: notesModel.count > 0
color: Theme.palette.backgroundTertiary
radius: Theme.spacing.radiusLarge
border.color: Theme.palette.border
border.width: 1
ListView {
id: notesList
anchors.fill: parent
anchors.margins: Theme.spacing.small
clip: true
model: notesModel
spacing: Theme.spacing.tiny
delegate: RowLayout {
width: notesList.width
spacing: Theme.spacing.small
LogosCheckbox {
checked: model.selected
onClicked: {
notesModel.setProperty(index, "selected", checked)
root.recompute()
}
}
LogosText {
Layout.fillWidth: true
text: model.noteId
elide: Text.ElideMiddle
font.pixelSize: Theme.typography.secondaryText
}
LogosText {
Layout.alignment: Qt.AlignRight
text: model.value
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
}
}
}
}
RowLayout {
Layout.fillWidth: true
visible: notesModel.count > 0
spacing: Theme.spacing.small
LogosText {
text: qsTr("%1 selected").arg(root.selectedCount)
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
Item { Layout.fillWidth: true }
LogosText {
text: qsTr("Total: %1").arg(root.selectedTotal)
font.pixelSize: Theme.typography.secondaryText
font.bold: true
}
}
}

View File

@ -0,0 +1,542 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
import "../controls"
// Multi-step wizard for channel_deposit_with_notes:
// 1. Select notes wallet_get_notes, pick UTXOs to consume
// 2. Fill fields channel id, change/funding keys, fee, metadata, tip
// 3. Confirm review the exact payload
// 4. Result tx hash (copyable) or error
ColumnLayout {
id: root
// Known wallet addresses (auto-remoted accounts model). Also used as
// public keys for the change/funding key pickers.
property var accountsModel: null
property bool nodeRunning: false
signal getNotesRequested(string addressHex, string optionalTipHex)
signal submitRequested(string channelIdHex, var inputNoteIdHexes, string metadataBase58, string changePublicKeyHex, var fundingPublicKeyHexes, string maxTxFee, string optionalTipHex)
signal copyToClipboard(string text)
// --- Called by the parent after async backend calls return ---
function setNotesLoading() {
noteSelector.loading = true
noteSelector.errorText = ""
}
function setNotesResult(jsonStr) {
noteSelector.loading = false
var s = jsonStr || ""
if (s.indexOf("Error") === 0) {
noteSelector.errorText = s
noteSelector.notes = []
return
}
try {
var parsed = JSON.parse(s)
d.notesTip = parsed.tip || ""
noteSelector.errorText = ""
noteSelector.notes = parsed.notes || []
} catch (e) {
noteSelector.errorText = qsTr("Failed to parse notes: %1").arg(s)
noteSelector.notes = []
}
}
function setSubmitResult(resultStr) {
d.resultPending = false
var s = resultStr || ""
d.resultSuccess = (s.indexOf("Error") !== 0)
d.resultText = s
}
spacing: Theme.spacing.large
QtObject {
id: d
property int step: 0
readonly property int stepCount: 4
property string notesTip: ""
// result state
property bool resultPending: false
property bool resultSuccess: false
property string resultText: ""
function fundingKeyList() {
return fundingKeysArea.text.split("\n")
.map(function(s) { return s.trim() })
.filter(function(s) { return s.length > 0 })
}
// --- Metadata (base58-encoded bytes) ---
// The actual base58 bytes decoding happens in the C++ backend; here we
// only check the input uses the base58 (Bitcoin) alphabet. Plain base58
// has no checksum, so a valid-alphabet string always decodes.
function metadataIsValid() {
var s = metadataField.text.trim()
if (s === "")
return true // optional
return /^[1-9A-HJ-NP-Za-km-z]+$/.test(s)
}
function canAdvance() {
switch (step) {
case 0:
return noteSelector.selectedCount > 0
case 1:
return channelIdField.text.trim().length > 0
&& changeKeyField.text.trim().length > 0
&& fundingKeyList().length > 0
&& maxFeeField.text.trim().length > 0
&& metadataIsValid()
default:
return true
}
}
function goNext() {
if (step === 1) {
// entering confirm nothing else
}
if (step < stepCount - 1)
step++
// Prefill key fields from the selected wallet when first reaching step 1.
if (step === 1) {
if (changeKeyField.text.trim() === "")
changeKeyField.text = walletField.text.trim()
if (fundingKeysArea.text.trim() === "" && walletField.text.trim() !== "")
fundingKeysArea.text = walletField.text.trim()
}
}
function goBack() {
if (step > 0)
step--
}
function submit() {
d.resultPending = true
d.resultSuccess = false
d.resultText = ""
step = 3
root.submitRequested(
channelIdField.text.trim(),
noteSelector.selectedIds(),
metadataField.text.trim(),
changeKeyField.text.trim(),
fundingKeyList(),
maxFeeField.text.trim(),
tipField.text.trim())
}
function reset() {
noteSelector.clearSelection()
noteSelector.notes = []
noteSelector.errorText = ""
channelIdField.text = ""
changeKeyField.text = ""
fundingKeysArea.text = ""
maxFeeField.text = ""
metadataField.text = ""
tipField.text = ""
notesTip = ""
resultText = ""
resultPending = false
step = 0
}
function summaryLines() {
var ids = noteSelector.selectedIds()
return [
{ k: qsTr("Channel ID"), v: channelIdField.text.trim() },
{ k: qsTr("Notes to consume (%1)").arg(ids.length), v: ids.join("\n") },
{ k: qsTr("Total amount"), v: String(noteSelector.selectedTotal) },
{ k: qsTr("Change public key"), v: changeKeyField.text.trim() },
{ k: qsTr("Funding public keys"), v: fundingKeyList().join("\n") },
{ k: qsTr("Max tx fee"), v: maxFeeField.text.trim() },
{ k: qsTr("Metadata (base58)"), v: metadataField.text.trim() || qsTr("(none)") },
{ k: qsTr("Optional tip hex"), v: tipField.text.trim() || qsTr("(current tip)") }
]
}
}
// ---- Header / step indicator ----
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.medium
LogosText {
text: qsTr("Channel Deposit")
font.pixelSize: Theme.typography.primaryText
font.bold: true
}
Item { Layout.fillWidth: true }
LogosText {
text: qsTr("Step %1 of %2").arg(d.step + 1).arg(d.stepCount)
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
}
LogosText {
Layout.fillWidth: true
visible: !root.nodeRunning
text: qsTr("Start the node before making a deposit.")
color: Theme.palette.warning
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
}
StackLayout {
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: d.step
// ---- Step 0: Select notes ----
ColumnLayout {
spacing: Theme.spacing.medium
LogosText {
Layout.fillWidth: true
text: qsTr("Select the notes (UTXOs) to deposit into the channel. Their full value is consumed.")
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosComboBox {
Layout.preferredWidth: 200
placeholderText: qsTr("Known address…")
model: root.accountsModel
textRole: "address"
currentIndex: -1
onActivated: function(index) { walletField.text = currentText }
}
LogosTextField {
id: walletField
Layout.fillWidth: true
placeholderText: qsTr("Wallet address hex")
}
LogosButton {
text: qsTr("Load notes")
enabled: root.nodeRunning && walletField.text.trim().length > 0
onClicked: {
root.setNotesLoading()
root.getNotesRequested(walletField.text.trim(), "")
}
}
}
NoteSelector {
id: noteSelector
Layout.fillWidth: true
}
Item { Layout.fillHeight: true }
}
// ---- Step 1: Fields ----
ScrollView {
id: fieldsScroll
clip: true
ColumnLayout {
width: fieldsScroll.availableWidth
spacing: Theme.spacing.medium
LogosText {
text: qsTr("Channel ID hex")
font.pixelSize: Theme.typography.secondaryText
}
LogosTextField {
id: channelIdField
Layout.fillWidth: true
placeholderText: qsTr("Channel ID hex")
}
LogosText {
text: qsTr("Change public key (receives change)")
font.pixelSize: Theme.typography.secondaryText
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosComboBox {
Layout.preferredWidth: 200
placeholderText: qsTr("Known address…")
model: root.accountsModel
textRole: "address"
currentIndex: -1
onActivated: function(index) { changeKeyField.text = currentText }
}
LogosTextField {
id: changeKeyField
Layout.fillWidth: true
placeholderText: qsTr("Change public key hex")
}
}
LogosText {
text: qsTr("Funding public keys (one per line, fund the gas fee)")
font.pixelSize: Theme.typography.secondaryText
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
Item { Layout.fillWidth: true }
LogosComboBox {
Layout.preferredWidth: 220
placeholderText: qsTr("Add known address…")
model: root.accountsModel
textRole: "address"
currentIndex: -1
onActivated: function(index) {
var t = fundingKeysArea.text.trim()
fundingKeysArea.text = (t.length > 0 ? t + "\n" : "") + currentText
}
}
}
ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: 80
clip: true
TextArea {
id: fundingKeysArea
background: Rectangle {
radius: Theme.spacing.radiusSmall
color: Theme.palette.backgroundSecondary
border.width: 1
border.color: Theme.palette.backgroundElevated
}
placeholderText: qsTr("Funding public key hex, one per line")
placeholderTextColor: Theme.palette.textTertiary
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.text
}
}
LogosText {
text: qsTr("Max tx fee")
font.pixelSize: Theme.typography.secondaryText
}
LogosTextField {
id: maxFeeField
Layout.fillWidth: true
placeholderText: qsTr("Maximum transaction fee")
}
LogosText {
text: qsTr("Metadata (base58, optional)")
font.pixelSize: Theme.typography.secondaryText
}
LogosTextField {
id: metadataField
Layout.fillWidth: true
placeholderText: qsTr("Base58-encoded metadata bytes")
}
LogosText {
Layout.fillWidth: true
text: qsTr("Input must be base58-encoded; it is decoded to bytes before submission.")
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
}
LogosText {
Layout.fillWidth: true
visible: metadataField.text.trim() !== "" && !d.metadataIsValid()
text: qsTr("Invalid base58 input")
color: Theme.palette.error
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
}
LogosText {
text: qsTr("Optional tip hex (leave empty for current tip)")
font.pixelSize: Theme.typography.secondaryText
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosTextField {
id: tipField
Layout.fillWidth: true
placeholderText: qsTr("Tip hex")
}
LogosButton {
text: qsTr("Use query tip")
enabled: d.notesTip !== ""
onClicked: tipField.text = d.notesTip
}
}
}
}
// ---- Step 2: Confirm ----
ColumnLayout {
spacing: Theme.spacing.medium
LogosText {
Layout.fillWidth: true
text: qsTr("Review the deposit. This is the exact payload that will be submitted.")
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.palette.backgroundTertiary
radius: Theme.spacing.radiusLarge
border.color: Theme.palette.border
border.width: 1
ScrollView {
id: confirmScroll
anchors.fill: parent
anchors.margins: Theme.spacing.large
clip: true
ColumnLayout {
width: confirmScroll.availableWidth
spacing: Theme.spacing.medium
Repeater {
// Re-evaluated when the confirm step is shown so it
// reflects the latest field values.
model: d.step === 2 ? d.summaryLines() : []
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Theme.spacing.tiny
LogosText {
text: modelData.k
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
LogosText {
Layout.fillWidth: true
text: modelData.v
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WrapAnywhere
}
}
}
}
}
}
}
// ---- Step 3: Result ----
ColumnLayout {
spacing: Theme.spacing.medium
LogosText {
Layout.alignment: Qt.AlignHCenter
visible: d.resultPending
text: qsTr("Submitting deposit…")
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
}
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
visible: d.resultPending
running: d.resultPending
}
LogosText {
Layout.fillWidth: true
visible: !d.resultPending
text: d.resultSuccess ? qsTr("Deposit submitted") : qsTr("Deposit failed")
color: d.resultSuccess ? Theme.palette.success : Theme.palette.error
font.pixelSize: Theme.typography.primaryText
font.bold: true
}
// Success: tx hash + copy
RowLayout {
Layout.fillWidth: true
visible: !d.resultPending && d.resultSuccess
spacing: Theme.spacing.small
Rectangle {
Layout.fillWidth: true
implicitHeight: txHashText.implicitHeight + Theme.spacing.medium
color: Theme.palette.backgroundTertiary
radius: Theme.spacing.radiusSmall
border.color: Theme.palette.border
border.width: 1
LogosText {
id: txHashText
anchors.fill: parent
anchors.margins: Theme.spacing.small
text: d.resultText
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WrapAnywhere
verticalAlignment: Text.AlignVCenter
}
}
LogosCopyButton {
Layout.preferredHeight: 40
Layout.preferredWidth: 40
onCopyText: root.copyToClipboard(d.resultText)
}
}
// Error
LogosText {
Layout.fillWidth: true
visible: !d.resultPending && !d.resultSuccess && d.resultText !== ""
text: d.resultText
color: Theme.palette.error
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
}
Item { Layout.fillHeight: true }
}
}
// ---- Footer navigation ----
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosButton {
text: qsTr("Back")
visible: d.step > 0 && d.step < 3
onClicked: d.goBack()
}
Item { Layout.fillWidth: true }
// Steps 0 & 1: Next
LogosButton {
text: qsTr("Next")
visible: d.step < 2
enabled: d.canAdvance()
onClicked: d.goNext()
}
// Step 2: Confirm & submit
LogosButton {
text: qsTr("Confirm & deposit")
visible: d.step === 2
enabled: root.nodeRunning
onClicked: d.submit()
}
// Step 3: New deposit (after completion)
LogosButton {
text: qsTr("New deposit")
visible: d.step === 3 && !d.resultPending
onClicked: d.reset()
}
}
}

View File

@ -70,7 +70,9 @@ Control {
delegate: ItemDelegate{
width: ListView.view.width
contentItem: LogosText {
text: model.text
// The remoted log model can briefly report an undefined
// "text" role while the replica syncs coerce to "".
text: model.text || ""
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.Wrap
}
@ -78,7 +80,7 @@ Control {
color: hovered ? Theme.palette.background: "transparent"
radius: 2
}
onClicked: root.copyToClipboard(model.text)
onClicked: root.copyToClipboard(model.text || "")
}
LogosText {

View File

@ -16,6 +16,7 @@ RowLayout {
property string lastBalanceErrorAddress: ""
signal getBalanceRequested(string addressHex)
signal refreshAccountsRequested()
signal transferRequested(string fromKeyHex, string toKeyHex, string amount)
signal claimLeaderRewardsRequested()
signal copyToClipboard(string text)
@ -48,10 +49,19 @@ RowLayout {
anchors.margins: Theme.spacing.large
spacing: Theme.spacing.large
LogosText {
text: qsTr("Accounts")
font.pixelSize: Theme.typography.secondaryText
font.bold: true
RowLayout {
Layout.fillWidth: true
LogosText {
text: qsTr("Accounts")
font.pixelSize: Theme.typography.secondaryText
font.bold: true
}
Item { Layout.fillWidth: true }
LogosButton {
text: qsTr("Refresh")
padding: Theme.spacing.small
onClicked: root.refreshAccountsRequested()
}
}
LogosText {

View File

@ -4,4 +4,5 @@ LogsView 1.0 LogsView.qml
WalletView 1.0 WalletView.qml
GenerateConfigView 1.0 GenerateConfigView.qml
ConfigChoiceView 1.0 ConfigChoiceView.qml
SetConfigPathView 1.0 SetConfigPathView.qml
SetConfigPathView 1.0 SetConfigPathView.qml
ChannelDepositView 1.0 ChannelDepositView.qml