mirror of
https://github.com/logos-blockchain/logos-execution-zone-wallet-ui.git
synced 2026-07-29 14:13:52 +00:00
Merge pull request #20 from logos-blockchain/schouhy/add-withdrawal-tab
Add withdrawal tab
This commit is contained in:
commit
b343b48ef9
@ -20,6 +20,8 @@ logos_module(
|
||||
src/LEZWalletAccountModel.cpp
|
||||
src/LEZAccountFilterModel.h
|
||||
src/LEZAccountFilterModel.cpp
|
||||
src/LEZClaimableAccountFilterModel.h
|
||||
src/LEZClaimableAccountFilterModel.cpp
|
||||
FIND_PACKAGES
|
||||
Qt6Gui
|
||||
LINK_LIBRARIES
|
||||
|
||||
722
flake.lock
generated
722
flake.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||||
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
|
||||
logos_execution_zone.url = "github:logos-blockchain/logos-execution-zone-module";
|
||||
logos_execution_zone.url = "github:logos-blockchain/logos-execution-zone-module?rev=b27f7b4e600e9041694e8cd29c03579ccdab9fbb";
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, ... }:
|
||||
|
||||
19
src/LEZClaimableAccountFilterModel.cpp
Normal file
19
src/LEZClaimableAccountFilterModel.cpp
Normal file
@ -0,0 +1,19 @@
|
||||
#include "LEZClaimableAccountFilterModel.h"
|
||||
|
||||
LEZClaimableAccountFilterModel::LEZClaimableAccountFilterModel(QObject* parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
connect(this, &QAbstractItemModel::rowsInserted, this, &LEZClaimableAccountFilterModel::countChanged);
|
||||
connect(this, &QAbstractItemModel::rowsRemoved, this, &LEZClaimableAccountFilterModel::countChanged);
|
||||
connect(this, &QAbstractItemModel::modelReset, this, &LEZClaimableAccountFilterModel::countChanged);
|
||||
connect(this, &QAbstractItemModel::layoutChanged, this, &LEZClaimableAccountFilterModel::countChanged);
|
||||
}
|
||||
|
||||
bool LEZClaimableAccountFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
|
||||
{
|
||||
if (!sourceModel())
|
||||
return false;
|
||||
const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
const QString vaultBalance = sourceModel()->data(idx, LEZWalletAccountModel::VaultBalanceRole).toString();
|
||||
return !vaultBalance.isEmpty() && vaultBalance != QStringLiteral("0");
|
||||
}
|
||||
22
src/LEZClaimableAccountFilterModel.h
Normal file
22
src/LEZClaimableAccountFilterModel.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include "LEZWalletAccountModel.h"
|
||||
|
||||
// Filters the account model down to accounts with a nonzero claimable vault balance
|
||||
// (see LEZWalletBackend::refreshVaultBalances), for the Bridge tab's Claim Deposit list.
|
||||
class LEZClaimableAccountFilterModel : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
|
||||
public:
|
||||
explicit LEZClaimableAccountFilterModel(QObject* parent = nullptr);
|
||||
|
||||
int count() const { return rowCount(); }
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
};
|
||||
@ -1,5 +1,8 @@
|
||||
#include "LEZWalletAccountModel.h"
|
||||
#include <QJsonObject>
|
||||
#include <QHash>
|
||||
#include <QPair>
|
||||
#include <QVariantMap>
|
||||
#include <algorithm>
|
||||
|
||||
LEZWalletAccountModel::LEZWalletAccountModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
@ -23,7 +26,11 @@ QVariant LEZWalletAccountModel::data(const QModelIndex& index, int role) const
|
||||
case NameRole: return e.name;
|
||||
case AccountIdRole: return e.accountId;
|
||||
case BalanceRole: return e.balance;
|
||||
case VaultBalanceRole: return e.vaultBalance;
|
||||
case IsPublicRole: return e.isPublic;
|
||||
case SectionKeyRole: return e.sectionKey;
|
||||
case KeysJsonRole: return e.keysJson;
|
||||
case IsFirstInGroupRole: return e.isFirstInGroup;
|
||||
default: return QVariant();
|
||||
}
|
||||
}
|
||||
@ -34,53 +41,63 @@ QHash<int, QByteArray> LEZWalletAccountModel::roleNames() const
|
||||
{ NameRole, "name" },
|
||||
{ AccountIdRole, "accountId" },
|
||||
{ BalanceRole, "balance" },
|
||||
{ IsPublicRole, "isPublic" }
|
||||
{ VaultBalanceRole, "vaultBalance" },
|
||||
{ IsPublicRole, "isPublic" },
|
||||
{ SectionKeyRole, "sectionKey" },
|
||||
{ KeysJsonRole, "keysJson" },
|
||||
{ IsFirstInGroupRole, "isFirstInGroup" }
|
||||
};
|
||||
}
|
||||
|
||||
void LEZWalletAccountModel::replaceFromJsonArray(const QJsonArray& arr)
|
||||
{
|
||||
beginResetModel();
|
||||
int oldCount = m_entries.size();
|
||||
m_entries.clear();
|
||||
for (const QJsonValue& v : arr) {
|
||||
LEZWalletAccountEntry e;
|
||||
e.balance = QString();
|
||||
if (v.isObject()) {
|
||||
const QJsonObject obj = v.toObject();
|
||||
e.accountId = obj.value(QStringLiteral("account_id")).toString();
|
||||
e.isPublic = obj.value(QStringLiteral("is_public")).toBool(true);
|
||||
} else {
|
||||
e.accountId = v.toString();
|
||||
e.isPublic = true;
|
||||
}
|
||||
e.name = QString();
|
||||
m_entries.append(e);
|
||||
}
|
||||
endResetModel();
|
||||
if (oldCount != m_entries.size())
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void LEZWalletAccountModel::replaceFromVariantList(const QVariantList& list)
|
||||
{
|
||||
// Rebuilding from scratch loses any balance/vaultBalance already fetched for an
|
||||
// account that's still present — carry those over so periodic re-listing (e.g. to
|
||||
// pick up newly discovered private accounts) doesn't make the claimable list and
|
||||
// balances flicker empty until the next refresh repopulates them.
|
||||
QHash<QString, QPair<QString, QString>> previousBalances;
|
||||
previousBalances.reserve(m_entries.size());
|
||||
for (const LEZWalletAccountEntry& e : m_entries)
|
||||
previousBalances.insert(e.accountId, qMakePair(e.balance, e.vaultBalance));
|
||||
|
||||
beginResetModel();
|
||||
int oldCount = m_entries.size();
|
||||
m_entries.clear();
|
||||
for (const QVariant& v : list) {
|
||||
LEZWalletAccountEntry e;
|
||||
e.balance = QString();
|
||||
if (v.canConvert<QVariantMap>()) {
|
||||
const QVariantMap obj = v.toMap();
|
||||
e.accountId = obj.value(QStringLiteral("account_id")).toString();
|
||||
e.isPublic = obj.value(QStringLiteral("is_public"), true).toBool();
|
||||
e.name = QString();
|
||||
if (v.type() == QVariant::Map) {
|
||||
const QVariantMap map = v.toMap();
|
||||
e.accountId = map.value(QStringLiteral("account_id")).toString();
|
||||
e.isPublic = map.value(QStringLiteral("is_public"), true).toBool();
|
||||
if (e.isPublic) {
|
||||
e.sectionKey = PublicSectionKey;
|
||||
} else {
|
||||
e.sectionKey = map.value(QStringLiteral("npk")).toString();
|
||||
e.keysJson = map.value(QStringLiteral("keys_json")).toString();
|
||||
}
|
||||
} else {
|
||||
e.accountId = v.toString();
|
||||
e.isPublic = true;
|
||||
e.sectionKey = PublicSectionKey;
|
||||
}
|
||||
const auto previous = previousBalances.find(e.accountId);
|
||||
if (previous != previousBalances.end()) {
|
||||
e.balance = previous->first;
|
||||
e.vaultBalance = previous->second;
|
||||
}
|
||||
e.name = QString();
|
||||
m_entries.append(e);
|
||||
}
|
||||
// Keep entries grouped by section (public first) so consecutive rows of the same
|
||||
// group are contiguous, then mark each group's first row for the QML header.
|
||||
std::stable_sort(m_entries.begin(), m_entries.end(),
|
||||
[](const LEZWalletAccountEntry& a, const LEZWalletAccountEntry& b) {
|
||||
if (a.isPublic != b.isPublic) return a.isPublic;
|
||||
return a.sectionKey < b.sectionKey;
|
||||
});
|
||||
for (int i = 0; i < m_entries.size(); ++i)
|
||||
m_entries[i].isFirstInGroup = (i == 0) || (m_entries[i].sectionKey != m_entries[i - 1].sectionKey);
|
||||
endResetModel();
|
||||
if (oldCount != m_entries.size())
|
||||
emit countChanged();
|
||||
@ -99,3 +116,26 @@ void LEZWalletAccountModel::setBalanceByAccountId(const QString& accountId, cons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LEZWalletAccountModel::setVaultBalanceByAccountId(const QString& accountId, const QString& vaultBalance)
|
||||
{
|
||||
for (int i = 0; i < m_entries.size(); ++i) {
|
||||
if (m_entries.at(i).accountId == accountId) {
|
||||
if (m_entries.at(i).vaultBalance != vaultBalance) {
|
||||
m_entries[i].vaultBalance = vaultBalance;
|
||||
QModelIndex idx = index(i, 0);
|
||||
emit dataChanged(idx, idx, { VaultBalanceRole });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool LEZWalletAccountModel::isPublicAccount(const QString& accountId, bool defaultValue) const
|
||||
{
|
||||
for (const LEZWalletAccountEntry& e : m_entries) {
|
||||
if (e.accountId == accountId)
|
||||
return e.isPublic;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@ -1,17 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
|
||||
// Public accounts have no key group, so they all share the PublicSectionKey section.
|
||||
// Private accounts section by NPK (the key group they belong to) — see
|
||||
// LEZWalletBackend::buildEnrichedAccountList, which attaches "npk"/"keys_json" per entry.
|
||||
inline const QString PublicSectionKey = QStringLiteral("public");
|
||||
|
||||
struct LEZWalletAccountEntry {
|
||||
QString name;
|
||||
QString accountId;
|
||||
QString balance;
|
||||
QString vaultBalance; // claimable balance held in this account's bridge vault PDA
|
||||
bool isPublic = true;
|
||||
QString sectionKey;
|
||||
QString keysJson; // {nullifier_public_key, viewing_public_key} shared by the whole section; private only
|
||||
bool isFirstInGroup = false; // QML renders the section header above rows where this is true
|
||||
};
|
||||
|
||||
// Note: this model is exposed to QML via Qt Remote Objects model replication (see
|
||||
// logos.model() in ExecutionZoneWalletView.qml), which only replicates roles — not
|
||||
// arbitrary Q_PROPERTYs. Anything QML needs must be a role on the row, not a property
|
||||
// on the model itself.
|
||||
class LEZWalletAccountModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
@ -20,7 +33,11 @@ public:
|
||||
NameRole = Qt::UserRole + 1,
|
||||
AccountIdRole,
|
||||
BalanceRole,
|
||||
IsPublicRole
|
||||
VaultBalanceRole,
|
||||
IsPublicRole,
|
||||
SectionKeyRole,
|
||||
KeysJsonRole,
|
||||
IsFirstInGroupRole
|
||||
};
|
||||
Q_ENUM(Role)
|
||||
|
||||
@ -30,11 +47,17 @@ public:
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void replaceFromJsonArray(const QJsonArray& arr);
|
||||
void replaceFromVariantList(const QVariantList& list);
|
||||
void setBalanceByAccountId(const QString& accountId, const QString& balance);
|
||||
void setVaultBalanceByAccountId(const QString& accountId, const QString& vaultBalance);
|
||||
int count() const { return m_entries.size(); }
|
||||
|
||||
// Authoritative isPublic lookup by account ID — used to validate/derive the flag
|
||||
// server-side instead of trusting a caller-supplied value, since this model is the
|
||||
// source of truth for which accounts the wallet actually owns. Falls back to
|
||||
// `defaultValue` if the account isn't found.
|
||||
bool isPublicAccount(const QString& accountId, bool defaultValue = true) const;
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
@ -30,6 +29,10 @@ namespace {
|
||||
// Timeout(-1) means "wait indefinitely", matching Qt's own convention
|
||||
// for infinite waits (e.g. QRemoteObjectPendingCall::waitForFinished(-1)).
|
||||
const Timeout NO_TIMEOUT{-1};
|
||||
// Warm-up retry budget for openIfPathsConfigured(), 50ms x up to 100 attempts
|
||||
// ~= 5s, matching LogosAPIConsumer's own connect-retry budget.
|
||||
const int MODULE_WARMUP_RETRY_MS = 50;
|
||||
const int MODULE_WARMUP_MAX_ATTEMPTS = 100;
|
||||
|
||||
// Convert a decimal amount string to 32-char hex (16 bytes little-endian)
|
||||
// for transfer_public/transfer_private/transfer_private_owned.
|
||||
@ -61,12 +64,14 @@ LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
m_accountModel(new LEZWalletAccountModel(this)),
|
||||
m_filteredAccountModel(new LEZAccountFilterModel(this)),
|
||||
m_privateAccountModel(new LEZAccountFilterModel(this)),
|
||||
m_claimableAccountModel(new LEZClaimableAccountFilterModel(this)),
|
||||
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("lez_wallet_ui", this)),
|
||||
m_logos(new LogosModules(m_logosAPI))
|
||||
{
|
||||
m_filteredAccountModel->setSourceModel(m_accountModel);
|
||||
m_privateAccountModel->setFilterByPublic(false);
|
||||
m_privateAccountModel->setSourceModel(m_accountModel);
|
||||
m_claimableAccountModel->setSourceModel(m_accountModel);
|
||||
|
||||
// Initialise PROP defaults via the generated setters.
|
||||
setIsWalletOpen(false);
|
||||
@ -118,29 +123,74 @@ void LEZWalletBackend::persistStoragePath(const QString& path)
|
||||
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(STORAGE_PATH_KEY, localPath);
|
||||
}
|
||||
|
||||
void LEZWalletBackend::openIfPathsConfigured()
|
||||
void LEZWalletBackend::openIfPathsConfigured(int attempt)
|
||||
{
|
||||
if (configPath().isEmpty() || storagePath().isEmpty()) return;
|
||||
|
||||
// The first cross-process call this module makes to logos_execution_zone can race
|
||||
// the inter-module capability/auth-token handshake: if it goes out before the
|
||||
// target has been informed of our token, ModuleProxy rejects it and the call
|
||||
// resolves to a default-constructed return value -- 0 for open()'s int64_t, which
|
||||
// is indistinguishable from WALLET_FFI_SUCCESS. Warm up with a harmless,
|
||||
// wallet-state-free call first: version() defaults to "" on that same rejection,
|
||||
// which IS distinguishable from its real non-empty value, so we can retry here
|
||||
// until the handshake has settled before trusting open()'s result. Once any call
|
||||
// to logos_execution_zone succeeds, its token is cached for the rest of the
|
||||
// session, so open() itself won't hit this race afterwards.
|
||||
if (m_logos->logos_execution_zone.version().isEmpty() && attempt < MODULE_WARMUP_MAX_ATTEMPTS) {
|
||||
QTimer::singleShot(MODULE_WARMUP_RETRY_MS, this,
|
||||
[this, attempt]() { openIfPathsConfigured(attempt + 1); });
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "LEZWalletBackend: opening wallet with config" << configPath()
|
||||
<< "storage" << storagePath();
|
||||
int err = m_logos->logos_execution_zone.open(configPath(), storagePath());
|
||||
if (err == WALLET_FFI_SUCCESS) {
|
||||
qDebug() << "LEZWalletBackend: wallet opened successfully";
|
||||
setIsWalletOpen(true);
|
||||
m_accountModel->replaceFromVariantList(m_logos->logos_execution_zone.list_accounts());
|
||||
fetchAndUpdateBlockHeights();
|
||||
startChunkedSync();
|
||||
refreshSequencerAddr();
|
||||
finishOpeningWallet();
|
||||
} else {
|
||||
qWarning() << "LEZWalletBackend: failed to open wallet, error" << err
|
||||
<< "config:" << configPath() << "storage:" << storagePath();
|
||||
}
|
||||
}
|
||||
|
||||
// Tags each private account with the NPK of the key group it belongs to (plus that
|
||||
// group's {nullifier_public_key, viewing_public_key} JSON), so the model can section
|
||||
// accounts by key group instead of listing them flat. Public accounts are untouched.
|
||||
QVariantList LEZWalletBackend::buildEnrichedAccountList()
|
||||
{
|
||||
QVariantList raw = m_logos->logos_execution_zone.list_accounts();
|
||||
QVariantList enriched;
|
||||
enriched.reserve(raw.size());
|
||||
for (const QVariant& v : raw) {
|
||||
QVariantMap map = v.toMap();
|
||||
if (!map.value(QStringLiteral("is_public"), true).toBool()) {
|
||||
const QString accountId = map.value(QStringLiteral("account_id")).toString();
|
||||
const QString keysJson = getPrivateAccountKeys(accountId);
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(keysJson.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
map[QStringLiteral("npk")] = doc.object().value(QStringLiteral("nullifier_public_key")).toString();
|
||||
map[QStringLiteral("keys_json")] = keysJson;
|
||||
}
|
||||
}
|
||||
enriched.append(map);
|
||||
}
|
||||
return enriched;
|
||||
}
|
||||
|
||||
void LEZWalletBackend::finishOpeningWallet()
|
||||
{
|
||||
setIsWalletOpen(true);
|
||||
m_accountModel->replaceFromVariantList(buildEnrichedAccountList());
|
||||
fetchAndUpdateBlockHeights();
|
||||
startChunkedSync();
|
||||
refreshSequencerAddr();
|
||||
}
|
||||
|
||||
void LEZWalletBackend::refreshAccounts()
|
||||
{
|
||||
m_accountModel->replaceFromVariantList(m_logos->logos_execution_zone.list_accounts());
|
||||
m_accountModel->replaceFromVariantList(buildEnrichedAccountList());
|
||||
fetchAndUpdateBlockHeights();
|
||||
if (!m_syncing)
|
||||
startChunkedSync();
|
||||
@ -169,6 +219,9 @@ void LEZWalletBackend::syncNextChunk()
|
||||
const quint64 synced = static_cast<quint64>(lastSyncedBlock());
|
||||
if (synced >= m_syncTarget) {
|
||||
m_syncing = false;
|
||||
// Sync may have discovered new private accounts (e.g. shielded transfers to a
|
||||
// foreign NPK/VPK); re-list so the model picks them up without a restart.
|
||||
m_accountModel->replaceFromVariantList(buildEnrichedAccountList());
|
||||
fetchAndUpdateBlockHeights();
|
||||
updateBalances();
|
||||
return;
|
||||
@ -333,6 +386,50 @@ QString LEZWalletBackend::transferDeshielded(QString fromHex, QString toHex, QSt
|
||||
NO_TIMEOUT).toString();
|
||||
}
|
||||
|
||||
QString LEZWalletBackend::bridgeWithdraw(QString fromHex, QString bedrockAccountPkHex, quint64 amount)
|
||||
{
|
||||
return m_logos->logos_execution_zone.bridge_withdraw(fromHex, bedrockAccountPkHex, amount);
|
||||
}
|
||||
|
||||
QString LEZWalletBackend::getVaultBalance(const QString& accountIdHex)
|
||||
{
|
||||
return m_logos->logos_execution_zone.get_vault_balance(accountIdHex);
|
||||
}
|
||||
|
||||
void LEZWalletBackend::refreshVaultBalances()
|
||||
{
|
||||
if (!m_accountModel) return;
|
||||
for (int i = 0; i < m_accountModel->count(); ++i) {
|
||||
const QModelIndex idx = m_accountModel->index(i, 0);
|
||||
const QString addr = m_accountModel->data(idx, LEZWalletAccountModel::AccountIdRole).toString();
|
||||
const QString vaultBal = getVaultBalance(addr);
|
||||
if (!vaultBal.isEmpty())
|
||||
m_accountModel->setVaultBalanceByAccountId(addr, vaultBal);
|
||||
}
|
||||
}
|
||||
|
||||
QString LEZWalletBackend::vaultClaim(QString fromHex, bool isPublic, QString amountStr)
|
||||
{
|
||||
const QString amountHex = amountToLe16Hex(amountStr);
|
||||
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
||||
// Don't trust the caller-supplied isPublic — the account model is the source of
|
||||
// truth for which accounts the wallet owns and whether each is public or private.
|
||||
const bool actuallyPublic = m_accountModel
|
||||
? m_accountModel->isPublicAccount(fromHex, isPublic)
|
||||
: isPublic;
|
||||
if (actuallyPublic)
|
||||
return m_logos->logos_execution_zone.vault_claim(fromHex, amountHex);
|
||||
|
||||
// vault_claim_private generates a proof, like transfer_private/transfer_shielded
|
||||
// above — go through invokeRemoteMethod with NO_TIMEOUT instead of the generated
|
||||
// accessor, which applies the SDK's default 20s Timeout and returns before the
|
||||
// proof is actually done and the tx submitted.
|
||||
return m_logosAPI->getClient(LEZ_MODULE)->invokeRemoteMethod(
|
||||
LEZ_MODULE, "vault_claim_private",
|
||||
QVariantList{fromHex.trimmed(), amountHex},
|
||||
NO_TIMEOUT).toString();
|
||||
}
|
||||
|
||||
void LEZWalletBackend::applySequencerAddrToConfig(const QString& configPath, const QString& sequencerAddr)
|
||||
{
|
||||
QJsonObject obj;
|
||||
@ -354,24 +451,40 @@ void LEZWalletBackend::applySequencerAddrToConfig(const QString& configPath, con
|
||||
file.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
|
||||
bool LEZWalletBackend::createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr)
|
||||
QString LEZWalletBackend::createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr)
|
||||
{
|
||||
const QString localConfigPath = toLocalPath(configPath);
|
||||
const QString localStoragePath = toLocalPath(storagePath);
|
||||
|
||||
// Both files already on disk: this is most likely an existing wallet the
|
||||
// user pointed us at (e.g. from the setup screen), not a request to
|
||||
// overwrite it. Try to load it instead of blindly creating a new one.
|
||||
if (QFile::exists(localConfigPath) && QFile::exists(localStoragePath)) {
|
||||
int err = m_logos->logos_execution_zone.open(localConfigPath, localStoragePath);
|
||||
if (err != WALLET_FFI_SUCCESS) {
|
||||
return QStringLiteral(
|
||||
"Could not load the wallet at the selected paths. Pick "
|
||||
"different existing config/storage files, or choose paths "
|
||||
"that don't exist yet to create a new wallet there.");
|
||||
}
|
||||
persistConfigPath(localConfigPath);
|
||||
persistStoragePath(localStoragePath);
|
||||
finishOpeningWallet();
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (!sequencerAddr.isEmpty())
|
||||
applySequencerAddrToConfig(localConfigPath, sequencerAddr);
|
||||
|
||||
const QString mnemonic = m_logos->logos_execution_zone.create_new(
|
||||
localConfigPath, localStoragePath, password);
|
||||
if (mnemonic.isEmpty()) return false;
|
||||
if (mnemonic.isEmpty())
|
||||
return QStringLiteral("Failed to create wallet. Check paths and try again.");
|
||||
|
||||
persistConfigPath(localConfigPath);
|
||||
persistStoragePath(localStoragePath);
|
||||
setIsWalletOpen(true);
|
||||
refreshAccounts();
|
||||
refreshSequencerAddr();
|
||||
return true;
|
||||
finishOpeningWallet();
|
||||
return QString();
|
||||
}
|
||||
|
||||
void LEZWalletBackend::copyToClipboard(QString text)
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "rep_LEZWalletBackend_source.h"
|
||||
|
||||
#include "LEZAccountFilterModel.h"
|
||||
#include "LEZClaimableAccountFilterModel.h"
|
||||
#include "LEZWalletAccountModel.h"
|
||||
|
||||
class LogosAPI;
|
||||
@ -20,6 +21,7 @@ class LEZWalletBackend : public LEZWalletBackendSimpleSource {
|
||||
Q_PROPERTY(LEZWalletAccountModel* accountModel READ accountModel CONSTANT)
|
||||
Q_PROPERTY(LEZAccountFilterModel* filteredAccountModel READ filteredAccountModel CONSTANT)
|
||||
Q_PROPERTY(LEZAccountFilterModel* privateAccountModel READ privateAccountModel CONSTANT)
|
||||
Q_PROPERTY(LEZClaimableAccountFilterModel* claimableAccountModel READ claimableAccountModel CONSTANT)
|
||||
|
||||
public:
|
||||
explicit LEZWalletBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
||||
@ -28,6 +30,7 @@ public:
|
||||
LEZWalletAccountModel* accountModel() const { return m_accountModel; }
|
||||
LEZAccountFilterModel* filteredAccountModel() const { return m_filteredAccountModel; }
|
||||
LEZAccountFilterModel* privateAccountModel() const { return m_privateAccountModel; }
|
||||
LEZClaimableAccountFilterModel* claimableAccountModel() const { return m_claimableAccountModel; }
|
||||
|
||||
public slots:
|
||||
// Overrides of the pure-virtual slots generated from the .rep.
|
||||
@ -45,7 +48,10 @@ public slots:
|
||||
QString transferShielded(QString fromHex, QString toKeysJson, QString amountStr) override;
|
||||
QString transferShieldedOwned(QString fromHex, QString toHex, QString amountStr) override;
|
||||
QString transferDeshielded(QString fromHex, QString toHex, QString amountStr) override;
|
||||
bool createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr) override;
|
||||
QString bridgeWithdraw(QString fromHex, QString bedrockAccountPkHex, quint64 amount) override;
|
||||
void refreshVaultBalances() override;
|
||||
QString vaultClaim(QString fromHex, bool isPublic, QString amountStr) override;
|
||||
QString createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr) override;
|
||||
void copyToClipboard(QString text) override;
|
||||
|
||||
private slots:
|
||||
@ -57,11 +63,14 @@ private:
|
||||
void applySequencerAddrToConfig(const QString& configPath, const QString& sequencerAddr);
|
||||
void fetchAndUpdateBlockHeights();
|
||||
void startChunkedSync();
|
||||
QVariantList buildEnrichedAccountList();
|
||||
|
||||
void updateBalances();
|
||||
QString getVaultBalance(const QString& accountIdHex);
|
||||
void refreshSequencerAddr();
|
||||
void saveWallet();
|
||||
void openIfPathsConfigured();
|
||||
void openIfPathsConfigured(int attempt = 0);
|
||||
void finishOpeningWallet();
|
||||
|
||||
bool m_syncing = false;
|
||||
quint64 m_syncTarget = 0;
|
||||
@ -70,6 +79,7 @@ private:
|
||||
LEZWalletAccountModel* m_accountModel;
|
||||
LEZAccountFilterModel* m_filteredAccountModel;
|
||||
LEZAccountFilterModel* m_privateAccountModel;
|
||||
LEZClaimableAccountFilterModel* m_claimableAccountModel;
|
||||
|
||||
LogosAPI* m_logosAPI;
|
||||
LogosModules* m_logos;
|
||||
|
||||
@ -23,7 +23,12 @@ class LEZWalletBackend
|
||||
SLOT(QString transferShieldedOwned(QString fromHex, QString toHex, QString amountStr))
|
||||
SLOT(QString transferDeshielded(QString fromHex, QString toHex, QString amountStr))
|
||||
|
||||
SLOT(bool createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr))
|
||||
SLOT(QString bridgeWithdraw(QString fromHex, QString bedrockAccountPkHex, quint64 amount))
|
||||
|
||||
SLOT(void refreshVaultBalances())
|
||||
SLOT(QString vaultClaim(QString fromHex, bool isPublic, QString amountStr))
|
||||
|
||||
SLOT(QString createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr))
|
||||
|
||||
SLOT(void copyToClipboard(QString text))
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ Rectangle {
|
||||
readonly property var accountModel: logos.model("lez_wallet_ui", "accountModel")
|
||||
readonly property var publicAccountModel: logos.model("lez_wallet_ui", "filteredAccountModel")
|
||||
readonly property var privateAccountModel: logos.model("lez_wallet_ui", "privateAccountModel")
|
||||
readonly property var claimableAccountModel: logos.model("lez_wallet_ui", "claimableAccountModel")
|
||||
property bool ready: false
|
||||
|
||||
Connections {
|
||||
@ -118,10 +119,13 @@ Rectangle {
|
||||
configPath: backend ? backend.configPath : ""
|
||||
onCreateWallet: function(configPath, storagePath, password, sequencerUrl) {
|
||||
if (!backend) return
|
||||
// createNew() returns an empty string on success, or a
|
||||
// human-readable error message (e.g. existing files at
|
||||
// the chosen paths failed to load) otherwise.
|
||||
logos.watch(backend.createNew(configPath, storagePath, password, sequencerUrl),
|
||||
function(ok) {
|
||||
if (!ok)
|
||||
createError = qsTr("Failed to create wallet. Check paths and try again.")
|
||||
function(errorMessage) {
|
||||
if (errorMessage)
|
||||
createError = errorMessage
|
||||
},
|
||||
function(error) {
|
||||
createError = qsTr("Error creating wallet: %1").arg(error)
|
||||
@ -138,6 +142,7 @@ Rectangle {
|
||||
accountModel: root.accountModel
|
||||
publicAccountModel: root.publicAccountModel
|
||||
privateAccountModel: root.privateAccountModel
|
||||
claimableAccountModel: root.claimableAccountModel
|
||||
lastSyncedBlock: backend ? backend.lastSyncedBlock : 0
|
||||
currentBlockHeight: backend ? backend.currentBlockHeight : 0
|
||||
|
||||
@ -229,21 +234,49 @@ Rectangle {
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onBridgeWithdrawRequested: (fromId, bedrockAccountPkHex, amount) => {
|
||||
if (!backend) return
|
||||
var parsedAmount = Number(amount)
|
||||
if (!Number.isInteger(parsedAmount) || parsedAmount <= 0) {
|
||||
dashboardView.transferResult = qsTr("Error: Invalid amount.")
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
return
|
||||
}
|
||||
logos.watch(backend.bridgeWithdraw(fromId, bedrockAccountPkHex, parsedAmount),
|
||||
function(raw) { ffiErrors.applyTransferResult(dashboardView, raw) },
|
||||
function(error) {
|
||||
dashboardView.transferResult = qsTr("Error: %1").arg(error)
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onVaultClaimRequested: (fromId, isPublic, amount) => {
|
||||
if (!backend) return
|
||||
dashboardView.transferPending = !isPublic
|
||||
logos.watch(backend.vaultClaim(fromId, isPublic, amount),
|
||||
function(raw) {
|
||||
dashboardView.transferPending = false
|
||||
ffiErrors.applyTransferResult(dashboardView, raw)
|
||||
backend.refreshVaultBalances()
|
||||
backend.refreshBalances()
|
||||
},
|
||||
function(error) {
|
||||
dashboardView.transferPending = false
|
||||
dashboardView.transferResult = qsTr("Error: %1").arg(error)
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onRefreshClaimableDepositsRequested: {
|
||||
if (!backend) return
|
||||
backend.refreshVaultBalances() // void slot, fire-and-forget
|
||||
}
|
||||
onCopyRequested: (copyText) => {
|
||||
clipHelper.text = copyText
|
||||
clipHelper.selectAll()
|
||||
clipHelper.copy()
|
||||
}
|
||||
onCopyPublicKeysRequested: (accountIdHex) => {
|
||||
if (!backend) return
|
||||
logos.watch(backend.getPrivateAccountKeys(accountIdHex),
|
||||
function(keys) {
|
||||
clipHelper.text = keys
|
||||
clipHelper.selectAll()
|
||||
clipHelper.copy()
|
||||
},
|
||||
function(error) { console.warn("getPrivateAccountKeys failed:", error) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ ComboBox {
|
||||
// Forwarded from AccountDelegate's copy button — bubble up to the parent
|
||||
// view, which calls backend.copyToClipboard().
|
||||
signal copyRequested(string text)
|
||||
signal copyPublicKeysRequested(string accountIdHex)
|
||||
|
||||
leftPadding: 12
|
||||
rightPadding: 12
|
||||
@ -62,7 +61,6 @@ ComboBox {
|
||||
width: root.popup ? (root.popup.width - root.popup.leftPadding - root.popup.rightPadding) : 368
|
||||
highlighted: root.highlightedIndex === index
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
onCopyPublicKeysRequested: (id) => root.copyPublicKeysRequested(id)
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
|
||||
@ -14,7 +14,6 @@ ItemDelegate {
|
||||
// the global QML scope for `backend` since it now lives behind the
|
||||
// logos.module() bridge in the parent view.
|
||||
signal copyRequested(string text)
|
||||
signal copyPublicKeysRequested(string accountIdHex)
|
||||
|
||||
leftPadding: Theme.spacing.medium
|
||||
rightPadding: Theme.spacing.medium
|
||||
@ -78,9 +77,7 @@ ItemDelegate {
|
||||
LogosCopyButton {
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 40
|
||||
onCopyText: model.isPublic
|
||||
? root.copyRequested(Base58.encode(model.accountId ?? ""))
|
||||
: root.copyPublicKeysRequested(model.accountId ?? "")
|
||||
onCopyText: root.copyRequested(Base58.encode(model.accountId ?? ""))
|
||||
visible: addressLabel.text
|
||||
icon.color: Theme.palette.textMuted
|
||||
}
|
||||
|
||||
@ -21,7 +21,6 @@ Rectangle {
|
||||
signal createPrivateAccountRequested()
|
||||
signal fetchBalancesRequested()
|
||||
signal copyRequested(string text)
|
||||
signal copyPublicKeysRequested(string accountIdHex)
|
||||
|
||||
radius: Theme.spacing.radiusXlarge
|
||||
color: Theme.palette.backgroundSecondary
|
||||
@ -63,7 +62,7 @@ Rectangle {
|
||||
// Sync progress
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.xsmall
|
||||
spacing: Theme.spacing.small
|
||||
visible: root.currentBlockHeight > 0 && root.lastSyncedBlock < root.currentBlockHeight
|
||||
|
||||
RowLayout {
|
||||
@ -127,10 +126,54 @@ Rectangle {
|
||||
spacing: Theme.spacing.small
|
||||
model: root.accountModel
|
||||
|
||||
delegate: AccountDelegate {
|
||||
// Each private account's "keysJson"/"sectionKey"/"isFirstInGroup" are plain
|
||||
// model roles (replicated like any other row data), so the group header for
|
||||
// the section a row starts is rendered inline here rather than via
|
||||
// ListView.section — section.delegate only gets the section's string value,
|
||||
// with no way back to that row's data once the model is a remote replica.
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
onCopyPublicKeysRequested: (id) => root.copyPublicKeysRequested(id)
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
// keysJson is only ever read inside the copy button's click handler below,
|
||||
// never bound to anything visible — so unlike accountId/isPublic/isFirstInGroup
|
||||
// (which are warmed up by their visible bindings), the Qt Remote Objects model
|
||||
// replica never requests this role from the source process until something
|
||||
// actually reads it. Without this binding, the first click reads a not-yet-
|
||||
// fetched (empty) value and the real data only arrives in time for the next
|
||||
// click. Referencing it here forces the role to be requested as soon as the
|
||||
// row is created.
|
||||
property string keysJsonWarm: model.keysJson ?? ""
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: model.isFirstInGroup ?? false
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: model.isPublic
|
||||
? qsTr("Public Accounts")
|
||||
: qsTr("Private")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
LogosCopyButton {
|
||||
Layout.preferredHeight: 32
|
||||
Layout.preferredWidth: 32
|
||||
visible: !model.isPublic
|
||||
icon.color: Theme.palette.textMuted
|
||||
onCopyText: root.copyRequested(model.keysJson ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
AccountDelegate {
|
||||
Layout.fillWidth: true
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
69
src/qml/views/BridgePanel.qml
Normal file
69
src/qml/views/BridgePanel.qml
Normal file
@ -0,0 +1,69 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// --- Public API: data in ---
|
||||
property var publicAccountModel: null
|
||||
property var claimableAccountModel: null
|
||||
property bool transferPending: false
|
||||
|
||||
// --- Public API: signals out ---
|
||||
signal bridgeWithdrawRequested(string fromAccountId, string bedrockAccountPkHex, string amount)
|
||||
signal vaultClaimRequested(string fromAccountId, bool isPublic, string amount)
|
||||
signal refreshClaimableDepositsRequested()
|
||||
signal copyRequested(string copyText)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
// Bridge section toggle
|
||||
TabBar {
|
||||
id: bridgeSectionBar
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
currentIndex: 0
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusSmall
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Withdraw")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Claim Deposit")
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
currentIndex: bridgeSectionBar.currentIndex
|
||||
|
||||
WithdrawPanel {
|
||||
publicAccountModel: root.publicAccountModel
|
||||
transferPending: root.transferPending
|
||||
|
||||
onBridgeWithdrawRequested: (fromId, bedrockAccountPkHex, amount) => root.bridgeWithdrawRequested(fromId, bedrockAccountPkHex, amount)
|
||||
onCopyRequested: (copyText) => root.copyRequested(copyText)
|
||||
}
|
||||
|
||||
ClaimDepositPanel {
|
||||
claimableAccountModel: root.claimableAccountModel
|
||||
claimPending: root.transferPending
|
||||
|
||||
onVaultClaimRequested: (fromId, isPublic, amount) => root.vaultClaimRequested(fromId, isPublic, amount)
|
||||
onRefreshRequested: root.refreshClaimableDepositsRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/qml/views/ClaimDepositPanel.qml
Normal file
98
src/qml/views/ClaimDepositPanel.qml
Normal file
@ -0,0 +1,98 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../Base58.js" as Base58
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// --- Public API: data in ---
|
||||
property var claimableAccountModel: null
|
||||
property bool claimPending: false
|
||||
|
||||
// --- Public API: signals out ---
|
||||
signal vaultClaimRequested(string fromAccountId, bool isPublic, string amount)
|
||||
signal refreshRequested()
|
||||
|
||||
onVisibleChanged: if (visible) root.refreshRequested()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Theme.spacing.xlarge
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
text: qsTr("No deposits pending claim")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
visible: !listView.visible
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: count > 0
|
||||
clip: true
|
||||
spacing: Theme.spacing.small
|
||||
model: root.claimableAccountModel
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: delegateRoot
|
||||
width: listView.width
|
||||
|
||||
leftPadding: Theme.spacing.medium
|
||||
rightPadding: Theme.spacing.medium
|
||||
topPadding: Theme.spacing.medium
|
||||
bottomPadding: Theme.spacing.medium
|
||||
|
||||
background: Rectangle {
|
||||
color: delegateRoot.hovered ? Theme.palette.backgroundMuted : Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
LogosText {
|
||||
text: model.name || (qsTr("Account ") + Base58.encode(model.accountId ?? "").slice(0, 4))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Claimable: %1").arg(model.vaultBalance ?? "0")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
text: qsTr("Claim")
|
||||
enabled: !root.claimPending
|
||||
onClicked: root.vaultClaimRequested(model.accountId, model.isPublic, model.vaultBalance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Refresh")
|
||||
enabled: !root.claimPending
|
||||
onClicked: root.refreshRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ Rectangle {
|
||||
property var accountModel: null
|
||||
property var publicAccountModel: null
|
||||
property var privateAccountModel: null
|
||||
property var claimableAccountModel: null
|
||||
property string transferResult: ""
|
||||
property string transferTxHash: ""
|
||||
property bool transferResultIsError: false
|
||||
@ -29,8 +30,10 @@ Rectangle {
|
||||
signal transferShieldedRequested(string fromAccountId, string toKeysJsonOrAddress, string amount)
|
||||
signal transferShieldedOwnedRequested(string fromAccountId, string toAccountId, string amount)
|
||||
signal transferDeshieldedRequested(string fromAccountId, string toAccountId, string amount)
|
||||
signal bridgeWithdrawRequested(string fromAccountId, string bedrockAccountPkHex, string amount)
|
||||
signal vaultClaimRequested(string fromAccountId, bool isPublic, string amount)
|
||||
signal refreshClaimableDepositsRequested()
|
||||
signal copyRequested(string copyText)
|
||||
signal copyPublicKeysRequested(string accountIdHex)
|
||||
|
||||
color: Theme.palette.background
|
||||
|
||||
@ -52,7 +55,6 @@ Rectangle {
|
||||
onCreatePrivateAccountRequested: root.createPrivateAccountRequested()
|
||||
onFetchBalancesRequested: root.fetchBalancesRequested()
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
onCopyPublicKeysRequested: (id) => root.copyPublicKeysRequested(id)
|
||||
}
|
||||
|
||||
TransferPanel {
|
||||
@ -61,6 +63,7 @@ Rectangle {
|
||||
Layout.fillHeight: true
|
||||
publicAccountModel: root.publicAccountModel
|
||||
privateAccountModel: root.privateAccountModel
|
||||
claimableAccountModel: root.claimableAccountModel
|
||||
transferResult: root.transferResult
|
||||
transferTxHash: root.transferTxHash
|
||||
transferResultIsError: root.transferResultIsError
|
||||
@ -72,6 +75,9 @@ Rectangle {
|
||||
onTransferShieldedRequested: (fromId, toKeysJsonOrAddress, amount) => root.transferShieldedRequested(fromId, toKeysJsonOrAddress, amount)
|
||||
onTransferShieldedOwnedRequested: (fromId, toAccountId, amount) => root.transferShieldedOwnedRequested(fromId, toAccountId, amount)
|
||||
onTransferDeshieldedRequested: (fromId, toAccountId, amount) => root.transferDeshieldedRequested(fromId, toAccountId, amount)
|
||||
onBridgeWithdrawRequested: (fromId, bedrockAccountPkHex, amount) => root.bridgeWithdrawRequested(fromId, bedrockAccountPkHex, amount)
|
||||
onVaultClaimRequested: (fromId, isPublic, amount) => root.vaultClaimRequested(fromId, isPublic, amount)
|
||||
onRefreshClaimableDepositsRequested: root.refreshClaimableDepositsRequested()
|
||||
onCopyRequested: (copyText) => root.copyRequested(copyText)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
import "../Base58.js" as Base58
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
@ -14,42 +13,24 @@ Rectangle {
|
||||
// --- Public API: data in ---
|
||||
property var publicAccountModel: null
|
||||
property var privateAccountModel: null
|
||||
property var claimableAccountModel: null
|
||||
property string transferResult: ""
|
||||
property string transferTxHash: ""
|
||||
property bool transferResultIsError: false
|
||||
property bool transferPending: false
|
||||
|
||||
// --- Public API: signals out (match backend: transfer_public, transfer_private, transfer_private_owned, transfer_shielded, transfer_shielded_owned) ---
|
||||
// --- Public API: signals out (match backend: transfer_public, transfer_private, transfer_private_owned, transfer_shielded, transfer_shielded_owned, transfer_deshielded, bridge_withdraw, vault_claim) ---
|
||||
signal transferPublicRequested(string fromAccountId, string toAddress, string amount)
|
||||
signal transferPrivateRequested(string fromAccountId, string toKeysJsonOrAddress, string amount)
|
||||
signal transferPrivateOwnedRequested(string fromAccountId, string toAccountId, string amount)
|
||||
signal transferShieldedRequested(string fromAccountId, string toKeysJsonOrAddress, string amount)
|
||||
signal transferShieldedOwnedRequested(string fromAccountId, string toAccountId, string amount)
|
||||
signal transferDeshieldedRequested(string fromAccountId, string toAccountId, string amount)
|
||||
signal bridgeWithdrawRequested(string fromAccountId, string bedrockAccountPkHex, string amount)
|
||||
signal vaultClaimRequested(string fromAccountId, bool isPublic, string amount)
|
||||
signal refreshClaimableDepositsRequested()
|
||||
signal copyRequested(string copyText)
|
||||
|
||||
readonly property int fromFilterCount: fromCombo.count
|
||||
readonly property int toFilterCount: toCombo.count
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
property bool useOwnedAccountForTo: false
|
||||
readonly property bool isPublicTab: transferTypeBar.currentIndex === 0
|
||||
readonly property bool isPrivateTab: transferTypeBar.currentIndex === 1
|
||||
readonly property bool isShieldedTab: transferTypeBar.currentIndex === 2
|
||||
readonly property bool isDeshieldedTab: transferTypeBar.currentIndex === 3
|
||||
readonly property bool showOwnedOption: true
|
||||
readonly property bool toAddressValid: useOwnedAccountForTo
|
||||
? (toFilterCount > 0 && toCombo.currentIndex >= 0)
|
||||
: (toField && toField.text.trim().length > 0)
|
||||
readonly property bool needsProof: isPrivateTab || isShieldedTab || isDeshieldedTab
|
||||
readonly property bool sendEnabled: !root.transferPending
|
||||
&& amountField && manualFromField
|
||||
&& amountField.text.length > 0 && d.toAddressValid
|
||||
&& ((fromFilterCount > 0 && fromCombo.currentIndex >= 0)
|
||||
|| (fromFilterCount === 0 && manualFromField.text.trim().length > 0))
|
||||
}
|
||||
|
||||
radius: Theme.spacing.radiusXlarge
|
||||
color: Theme.palette.backgroundSecondary
|
||||
|
||||
@ -58,17 +39,11 @@ Rectangle {
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Transfer")
|
||||
font.pixelSize: Theme.typography.titleText
|
||||
font.weight: Theme.typography.weightBold
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
// Transfer type toggle
|
||||
// Main section toggle
|
||||
TabBar {
|
||||
id: transferTypeBar
|
||||
Layout.preferredWidth: 400
|
||||
id: mainSectionBar
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
currentIndex: 0
|
||||
|
||||
background: Rectangle {
|
||||
@ -77,134 +52,42 @@ Rectangle {
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Public")
|
||||
text: qsTr("Transfer")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Private")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Shielded")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Deshielded")
|
||||
text: qsTr("Bridge")
|
||||
}
|
||||
}
|
||||
|
||||
// From: dropdown when accounts exist, or manual entry when list is empty
|
||||
ColumnLayout {
|
||||
StackLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
Layout.fillHeight: true
|
||||
currentIndex: mainSectionBar.currentIndex
|
||||
|
||||
LogosText {
|
||||
text: qsTr("From")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
TransferTypesPanel {
|
||||
publicAccountModel: root.publicAccountModel
|
||||
privateAccountModel: root.privateAccountModel
|
||||
transferPending: root.transferPending
|
||||
|
||||
onTransferPublicRequested: (fromId, toAddress, amount) => root.transferPublicRequested(fromId, toAddress, amount)
|
||||
onTransferPrivateRequested: (fromId, toKeysJsonOrAddress, amount) => root.transferPrivateRequested(fromId, toKeysJsonOrAddress, amount)
|
||||
onTransferPrivateOwnedRequested: (fromId, toAccountId, amount) => root.transferPrivateOwnedRequested(fromId, toAccountId, amount)
|
||||
onTransferShieldedRequested: (fromId, toKeysJsonOrAddress, amount) => root.transferShieldedRequested(fromId, toKeysJsonOrAddress, amount)
|
||||
onTransferShieldedOwnedRequested: (fromId, toAccountId, amount) => root.transferShieldedOwnedRequested(fromId, toAccountId, amount)
|
||||
onTransferDeshieldedRequested: (fromId, toAccountId, amount) => root.transferDeshieldedRequested(fromId, toAccountId, amount)
|
||||
onCopyRequested: (copyText) => root.copyRequested(copyText)
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: manualFromField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Paste or type from account ID")
|
||||
visible: fromFilterCount === 0
|
||||
}
|
||||
BridgePanel {
|
||||
publicAccountModel: root.publicAccountModel
|
||||
claimableAccountModel: root.claimableAccountModel
|
||||
transferPending: root.transferPending
|
||||
|
||||
AccountComboBox {
|
||||
id: fromCombo
|
||||
Layout.fillWidth: true
|
||||
model: (d.isPrivateTab || d.isDeshieldedTab) ? root.privateAccountModel : root.publicAccountModel
|
||||
visible: fromFilterCount > 0
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
}
|
||||
}
|
||||
|
||||
// To field
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("To")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosCheckbox {
|
||||
id: useOwnedToCheck
|
||||
visible: d.showOwnedOption
|
||||
checked: d.useOwnedAccountForTo
|
||||
onCheckedChanged: d.useOwnedAccountForTo = checked
|
||||
text: qsTr("Use owned account")
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: toField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: (d.isPublicTab || d.isDeshieldedTab) ? qsTr("Recipient account ID") : qsTr("Recipient public keys (JSON)")
|
||||
visible: !d.useOwnedAccountForTo
|
||||
}
|
||||
|
||||
AccountComboBox {
|
||||
id: toCombo
|
||||
Layout.fillWidth: true
|
||||
model: (d.isPublicTab || d.isDeshieldedTab) ? root.publicAccountModel : root.privateAccountModel
|
||||
visible: d.useOwnedAccountForTo && toFilterCount > 0
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Amount field
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Amount")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: amountField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "0.00"
|
||||
}
|
||||
}
|
||||
|
||||
// Send button
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Send")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
enabled: d.sendEnabled
|
||||
onClicked: {
|
||||
var fromId = fromFilterCount > 0 && fromCombo.currentIndex >= 0
|
||||
? (fromCombo.currentValue ?? "")
|
||||
: Base58.decode(manualFromField.text.trim())
|
||||
var rawTo = toField.text.trim()
|
||||
var toAddress = (d.useOwnedAccountForTo && toCombo.currentIndex >= 0)
|
||||
? (toCombo.currentValue ?? "")
|
||||
: (rawTo.startsWith("{") ? rawTo : Base58.decode(rawTo))
|
||||
var amount = amountField.text.trim()
|
||||
if (fromId.length > 0 && toAddress.length > 0 && amount.length > 0) {
|
||||
if (d.isPublicTab)
|
||||
root.transferPublicRequested(fromId, toAddress, amount)
|
||||
else if (d.isPrivateTab) {
|
||||
if (d.useOwnedAccountForTo)
|
||||
root.transferPrivateOwnedRequested(fromId, toAddress, amount)
|
||||
else
|
||||
root.transferPrivateRequested(fromId, toAddress, amount)
|
||||
} else if (d.isShieldedTab) {
|
||||
if (d.useOwnedAccountForTo)
|
||||
root.transferShieldedOwnedRequested(fromId, toAddress, amount)
|
||||
else
|
||||
root.transferShieldedRequested(fromId, toAddress, amount)
|
||||
} else if (d.isDeshieldedTab) {
|
||||
root.transferDeshieldedRequested(fromId, toAddress, amount)
|
||||
}
|
||||
}
|
||||
onBridgeWithdrawRequested: (fromId, bedrockAccountPkHex, amount) => root.bridgeWithdrawRequested(fromId, bedrockAccountPkHex, amount)
|
||||
onVaultClaimRequested: (fromId, isPublic, amount) => root.vaultClaimRequested(fromId, isPublic, amount)
|
||||
onRefreshClaimableDepositsRequested: root.refreshClaimableDepositsRequested()
|
||||
onCopyRequested: (copyText) => root.copyRequested(copyText)
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,9 +133,5 @@ Rectangle {
|
||||
visible: resultText.text
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
229
src/qml/views/TransferTypesPanel.qml
Normal file
229
src/qml/views/TransferTypesPanel.qml
Normal file
@ -0,0 +1,229 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
import "../Base58.js" as Base58
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// --- Public API: data in ---
|
||||
property var publicAccountModel: null
|
||||
property var privateAccountModel: null
|
||||
property bool transferPending: false
|
||||
|
||||
// --- Public API: signals out (match backend: transfer_public, transfer_private, transfer_private_owned, transfer_shielded, transfer_shielded_owned, transfer_deshielded) ---
|
||||
signal transferPublicRequested(string fromAccountId, string toAddress, string amount)
|
||||
signal transferPrivateRequested(string fromAccountId, string toKeysJsonOrAddress, string amount)
|
||||
signal transferPrivateOwnedRequested(string fromAccountId, string toAccountId, string amount)
|
||||
signal transferShieldedRequested(string fromAccountId, string toKeysJsonOrAddress, string amount)
|
||||
signal transferShieldedOwnedRequested(string fromAccountId, string toAccountId, string amount)
|
||||
signal transferDeshieldedRequested(string fromAccountId, string toAccountId, string amount)
|
||||
signal copyRequested(string copyText)
|
||||
|
||||
readonly property int fromFilterCount: fromCombo.count
|
||||
readonly property int toFilterCount: toCombo.count
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
property bool useOwnedAccountForTo: false
|
||||
readonly property bool isPublicTab: transferTypeBar.currentIndex === 0
|
||||
readonly property bool isPrivateTab: transferTypeBar.currentIndex === 1
|
||||
readonly property bool isShieldedTab: transferTypeBar.currentIndex === 2
|
||||
readonly property bool isDeshieldedTab: transferTypeBar.currentIndex === 3
|
||||
readonly property bool needsKeysJson: !useOwnedAccountForTo && (isPrivateTab || isShieldedTab)
|
||||
readonly property bool toAddressValid: useOwnedAccountForTo
|
||||
? (root.toFilterCount > 0 && toCombo.currentIndex >= 0)
|
||||
: (needsKeysJson
|
||||
? d.isValidKeysJson(toField && toField.text)
|
||||
: (toField && toField.text.trim().length > 0))
|
||||
readonly property bool needsProof: isPrivateTab || isShieldedTab || isDeshieldedTab
|
||||
readonly property bool sendEnabled: !root.transferPending
|
||||
&& amountField && manualFromField
|
||||
&& amountField.text.length > 0 && d.toAddressValid
|
||||
&& ((root.fromFilterCount > 0 && fromCombo.currentIndex >= 0)
|
||||
|| (root.fromFilterCount === 0 && manualFromField.text.trim().length > 0))
|
||||
|
||||
// Private/shielded transfers (when not sending to an owned account) require the
|
||||
// recipient's {nullifier_public_key, viewing_public_key} JSON — the same JSON the
|
||||
// section-header copy button produces — not a bare account ID.
|
||||
function isValidKeysJson(text) {
|
||||
var trimmed = (text || "").trim()
|
||||
if (trimmed.length === 0) return false
|
||||
var obj
|
||||
try {
|
||||
obj = JSON.parse(trimmed)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return !!obj && typeof obj === "object" && !Array.isArray(obj)
|
||||
&& typeof obj.nullifier_public_key === "string" && obj.nullifier_public_key.length > 0
|
||||
&& typeof obj.viewing_public_key === "string" && obj.viewing_public_key.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
// Transfer type toggle
|
||||
TabBar {
|
||||
id: transferTypeBar
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
currentIndex: 0
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusSmall
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Public")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Private")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Shielded")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Deshielded")
|
||||
}
|
||||
}
|
||||
|
||||
// From: dropdown when accounts exist, or manual entry when list is empty
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("From")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: manualFromField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Paste or type from account ID")
|
||||
visible: root.fromFilterCount === 0
|
||||
}
|
||||
|
||||
AccountComboBox {
|
||||
id: fromCombo
|
||||
Layout.fillWidth: true
|
||||
model: (d.isPrivateTab || d.isDeshieldedTab) ? root.privateAccountModel : root.publicAccountModel
|
||||
visible: root.fromFilterCount > 0
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
}
|
||||
}
|
||||
|
||||
// To field
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("To")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosCheckbox {
|
||||
id: useOwnedToCheck
|
||||
checked: d.useOwnedAccountForTo
|
||||
onCheckedChanged: d.useOwnedAccountForTo = checked
|
||||
text: qsTr("Use owned account")
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: toField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: (d.isPublicTab || d.isDeshieldedTab) ? qsTr("Recipient account ID") : qsTr("Recipient public keys (JSON)")
|
||||
visible: !d.useOwnedAccountForTo
|
||||
}
|
||||
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
visible: d.needsKeysJson && toField.text.trim().length > 0 && !d.toAddressValid
|
||||
text: qsTr("Enter the recipient's public keys as JSON with nullifier_public_key and viewing_public_key.")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.error
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
AccountComboBox {
|
||||
id: toCombo
|
||||
Layout.fillWidth: true
|
||||
model: (d.isPublicTab || d.isDeshieldedTab) ? root.publicAccountModel : root.privateAccountModel
|
||||
visible: d.useOwnedAccountForTo && root.toFilterCount > 0
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Amount field
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Amount")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: amountField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "0.00"
|
||||
}
|
||||
}
|
||||
|
||||
// Send button
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Send")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
enabled: d.sendEnabled
|
||||
onClicked: {
|
||||
var fromId = root.fromFilterCount > 0 && fromCombo.currentIndex >= 0
|
||||
? (fromCombo.currentValue ?? "")
|
||||
: Base58.decode(manualFromField.text.trim())
|
||||
var rawTo = toField.text.trim()
|
||||
var toAddress = (d.useOwnedAccountForTo && toCombo.currentIndex >= 0)
|
||||
? (toCombo.currentValue ?? "")
|
||||
: (d.needsKeysJson ? rawTo : Base58.decode(rawTo))
|
||||
var amount = amountField.text.trim()
|
||||
if (fromId.length > 0 && toAddress.length > 0 && amount.length > 0) {
|
||||
if (d.isPublicTab)
|
||||
root.transferPublicRequested(fromId, toAddress, amount)
|
||||
else if (d.isPrivateTab) {
|
||||
if (d.useOwnedAccountForTo)
|
||||
root.transferPrivateOwnedRequested(fromId, toAddress, amount)
|
||||
else
|
||||
root.transferPrivateRequested(fromId, toAddress, amount)
|
||||
} else if (d.isShieldedTab) {
|
||||
if (d.useOwnedAccountForTo)
|
||||
root.transferShieldedOwnedRequested(fromId, toAddress, amount)
|
||||
else
|
||||
root.transferShieldedRequested(fromId, toAddress, amount)
|
||||
} else if (d.isDeshieldedTab) {
|
||||
root.transferDeshieldedRequested(fromId, toAddress, amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/qml/views/WithdrawPanel.qml
Normal file
122
src/qml/views/WithdrawPanel.qml
Normal file
@ -0,0 +1,122 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
import "../Base58.js" as Base58
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// --- Public API: data in ---
|
||||
property var publicAccountModel: null
|
||||
property bool transferPending: false
|
||||
|
||||
// --- Public API: signals out (match backend: bridge_withdraw) ---
|
||||
signal bridgeWithdrawRequested(string fromAccountId, string bedrockAccountPkHex, string amount)
|
||||
signal copyRequested(string copyText)
|
||||
|
||||
readonly property int fromFilterCount: fromCombo.count
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
readonly property bool sendEnabled: !root.transferPending
|
||||
&& amountField && manualFromField && toField
|
||||
&& amountField.text.length > 0
|
||||
&& toField.text.trim().length > 0
|
||||
&& ((root.fromFilterCount > 0 && fromCombo.currentIndex >= 0)
|
||||
|| (root.fromFilterCount === 0 && manualFromField.text.trim().length > 0))
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
// From: dropdown when public accounts exist, or manual entry when list is empty.
|
||||
// Bridge withdrawals only support public sender accounts.
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("From")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: manualFromField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Paste or type from account ID")
|
||||
visible: root.fromFilterCount === 0
|
||||
}
|
||||
|
||||
AccountComboBox {
|
||||
id: fromCombo
|
||||
Layout.fillWidth: true
|
||||
model: root.publicAccountModel
|
||||
visible: root.fromFilterCount > 0
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Bedrock (L1) recipient public key
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Bedrock (L1) public key")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: toField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Recipient's Bedrock public key (hex)")
|
||||
}
|
||||
}
|
||||
|
||||
// Amount field
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Amount")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: amountField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "0.00"
|
||||
}
|
||||
}
|
||||
|
||||
// Withdraw button
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Withdraw")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
enabled: d.sendEnabled
|
||||
onClicked: {
|
||||
var fromId = root.fromFilterCount > 0 && fromCombo.currentIndex >= 0
|
||||
? (fromCombo.currentValue ?? "")
|
||||
: Base58.decode(manualFromField.text.trim())
|
||||
var toAddress = toField.text.trim()
|
||||
var amount = amountField.text.trim()
|
||||
if (fromId.length > 0 && toAddress.length > 0 && amount.length > 0)
|
||||
root.bridgeWithdrawRequested(fromId, toAddress, amount)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user