mirror of
https://github.com/logos-blockchain/logos-blockchain-ui.git
synced 2026-07-08 08:49:35 +00:00
feat: Deposit ui (#23)
* Implement deposit ui * Update lock file * Fix accounts * Make deposit work
This commit is contained in:
parent
f953d4e3d6
commit
32257d20e2
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
162
src/qml/controls/NoteSelector.qml
Normal file
162
src/qml/controls/NoteSelector.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
542
src/qml/views/ChannelDepositView.qml
Normal file
542
src/qml/views/ChannelDepositView.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user