mirror of
https://github.com/logos-blockchain/logos-execution-zone-wallet-ui.git
synced 2026-08-01 23:53:14 +00:00
Merge pull request #18 from logos-blockchain/schouhy/wallet-improvements
feat: wallet improvements
This commit is contained in:
commit
bd36ed9002
@ -31,7 +31,7 @@ bool LEZAccountFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& s
|
||||
int LEZAccountFilterModel::rowForAddress(const QString& address) const
|
||||
{
|
||||
for (int i = 0; i < rowCount(); ++i) {
|
||||
if (data(index(i, 0), LEZWalletAccountModel::AddressRole).toString() == address)
|
||||
if (data(index(i, 0), LEZWalletAccountModel::AccountIdRole).toString() == address)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
|
||||
@ -21,7 +21,7 @@ QVariant LEZWalletAccountModel::data(const QModelIndex& index, int role) const
|
||||
const LEZWalletAccountEntry& e = m_entries.at(index.row());
|
||||
switch (role) {
|
||||
case NameRole: return e.name;
|
||||
case AddressRole: return e.address;
|
||||
case AccountIdRole: return e.accountId;
|
||||
case BalanceRole: return e.balance;
|
||||
case IsPublicRole: return e.isPublic;
|
||||
default: return QVariant();
|
||||
@ -32,7 +32,7 @@ QHash<int, QByteArray> LEZWalletAccountModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{ NameRole, "name" },
|
||||
{ AddressRole, "address" },
|
||||
{ AccountIdRole, "accountId" },
|
||||
{ BalanceRole, "balance" },
|
||||
{ IsPublicRole, "isPublic" }
|
||||
};
|
||||
@ -43,19 +43,18 @@ void LEZWalletAccountModel::replaceFromJsonArray(const QJsonArray& arr)
|
||||
beginResetModel();
|
||||
int oldCount = m_entries.size();
|
||||
m_entries.clear();
|
||||
int idx = 0;
|
||||
for (const QJsonValue& v : arr) {
|
||||
LEZWalletAccountEntry e;
|
||||
e.name = QStringLiteral("Account %1").arg(++idx);
|
||||
e.balance = QString();
|
||||
if (v.isObject()) {
|
||||
const QJsonObject obj = v.toObject();
|
||||
e.address = obj.value(QStringLiteral("account_id")).toString();
|
||||
e.accountId = obj.value(QStringLiteral("account_id")).toString();
|
||||
e.isPublic = obj.value(QStringLiteral("is_public")).toBool(true);
|
||||
} else {
|
||||
e.address = v.toString();
|
||||
e.accountId = v.toString();
|
||||
e.isPublic = true;
|
||||
}
|
||||
e.name = QString();
|
||||
m_entries.append(e);
|
||||
}
|
||||
endResetModel();
|
||||
@ -63,10 +62,10 @@ void LEZWalletAccountModel::replaceFromJsonArray(const QJsonArray& arr)
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void LEZWalletAccountModel::setBalanceByAddress(const QString& address, const QString& balance)
|
||||
void LEZWalletAccountModel::setBalanceByAccountId(const QString& accountId, const QString& balance)
|
||||
{
|
||||
for (int i = 0; i < m_entries.size(); ++i) {
|
||||
if (m_entries.at(i).address == address) {
|
||||
if (m_entries.at(i).accountId == accountId) {
|
||||
if (m_entries.at(i).balance != balance) {
|
||||
m_entries[i].balance = balance;
|
||||
QModelIndex idx = index(i, 0);
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
struct LEZWalletAccountEntry {
|
||||
QString name;
|
||||
QString address;
|
||||
QString accountId;
|
||||
QString balance;
|
||||
bool isPublic = true;
|
||||
};
|
||||
@ -17,7 +17,7 @@ class LEZWalletAccountModel : public QAbstractListModel {
|
||||
public:
|
||||
enum Role {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
AddressRole,
|
||||
AccountIdRole,
|
||||
BalanceRole,
|
||||
IsPublicRole
|
||||
};
|
||||
@ -30,7 +30,7 @@ public:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void replaceFromJsonArray(const QJsonArray& arr);
|
||||
void setBalanceByAddress(const QString& address, const QString& balance);
|
||||
void setBalanceByAccountId(const QString& accountId, const QString& balance);
|
||||
int count() const { return m_entries.size(); }
|
||||
|
||||
signals:
|
||||
|
||||
@ -4,13 +4,19 @@
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "logos_api.h"
|
||||
#include "logos_api_client.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
namespace {
|
||||
@ -18,7 +24,12 @@ namespace {
|
||||
const char SETTINGS_APP[] = "ExecutionZoneWalletUI";
|
||||
const char CONFIG_PATH_KEY[] = "configPath";
|
||||
const char STORAGE_PATH_KEY[] = "storagePath";
|
||||
const char LEZ_MODULE[] = "logos_execution_zone";
|
||||
const int WALLET_FFI_SUCCESS = 0;
|
||||
// Proof generation time is unbounded on commodity hardware.
|
||||
// Timeout(-1) means "wait indefinitely", matching Qt's own convention
|
||||
// for infinite waits (e.g. QRemoteObjectPendingCall::waitForFinished(-1)).
|
||||
const Timeout NO_TIMEOUT{-1};
|
||||
|
||||
// Convert a decimal amount string to 32-char hex (16 bytes little-endian)
|
||||
// for transfer_public/transfer_private/transfer_private_owned.
|
||||
@ -36,9 +47,12 @@ namespace {
|
||||
|
||||
// Normalise file:// URLs and OS paths to a plain local path.
|
||||
QString toLocalPath(const QString& path) {
|
||||
if (path.startsWith("file://") || path.contains("/"))
|
||||
return QUrl::fromUserInput(path).toLocalFile();
|
||||
return path;
|
||||
QString p = path;
|
||||
if (p.startsWith(QLatin1Char('~')))
|
||||
p = QDir::homePath() + p.mid(1);
|
||||
if (p.startsWith("file://") || p.contains("/"))
|
||||
return QUrl::fromUserInput(p).toLocalFile();
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,10 +60,13 @@ LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
: LEZWalletBackendSimpleSource(parent),
|
||||
m_accountModel(new LEZWalletAccountModel(this)),
|
||||
m_filteredAccountModel(new LEZAccountFilterModel(this)),
|
||||
m_privateAccountModel(new LEZAccountFilterModel(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);
|
||||
|
||||
// Initialise PROP defaults via the generated setters.
|
||||
setIsWalletOpen(false);
|
||||
@ -111,9 +128,14 @@ void LEZWalletBackend::openIfPathsConfigured()
|
||||
if (err == WALLET_FFI_SUCCESS) {
|
||||
qDebug() << "LEZWalletBackend: wallet opened successfully";
|
||||
setIsWalletOpen(true);
|
||||
refreshAccounts();
|
||||
refreshBlockHeights();
|
||||
QJsonArray arr = m_logos->logos_execution_zone.list_accounts();
|
||||
m_accountModel->replaceFromJsonArray(arr);
|
||||
fetchAndUpdateBlockHeights();
|
||||
startChunkedSync();
|
||||
refreshSequencerAddr();
|
||||
} else {
|
||||
qWarning() << "LEZWalletBackend: failed to open wallet, error" << err
|
||||
<< "config:" << configPath() << "storage:" << storagePath();
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,21 +143,66 @@ void LEZWalletBackend::refreshAccounts()
|
||||
{
|
||||
QJsonArray arr = m_logos->logos_execution_zone.list_accounts();
|
||||
m_accountModel->replaceFromJsonArray(arr);
|
||||
refreshBalances();
|
||||
fetchAndUpdateBlockHeights();
|
||||
if (!m_syncing)
|
||||
startChunkedSync();
|
||||
}
|
||||
|
||||
void LEZWalletBackend::refreshBalances()
|
||||
{
|
||||
refreshBlockHeights();
|
||||
syncToBlock(currentBlockHeight());
|
||||
fetchAndUpdateBlockHeights();
|
||||
if (!m_syncing)
|
||||
startChunkedSync();
|
||||
}
|
||||
|
||||
void LEZWalletBackend::startChunkedSync()
|
||||
{
|
||||
m_syncTarget = static_cast<quint64>(currentBlockHeight());
|
||||
if (m_syncTarget == 0 || static_cast<quint64>(lastSyncedBlock()) >= m_syncTarget) {
|
||||
updateBalances();
|
||||
return;
|
||||
}
|
||||
m_syncing = true;
|
||||
syncNextChunk();
|
||||
}
|
||||
|
||||
void LEZWalletBackend::syncNextChunk()
|
||||
{
|
||||
const quint64 synced = static_cast<quint64>(lastSyncedBlock());
|
||||
if (synced >= m_syncTarget) {
|
||||
m_syncing = false;
|
||||
fetchAndUpdateBlockHeights();
|
||||
updateBalances();
|
||||
return;
|
||||
}
|
||||
const quint64 next = qMin(synced + SYNC_CHUNK_SIZE, m_syncTarget);
|
||||
m_logos->logos_execution_zone.sync_to_block(next);
|
||||
// Only read lastSyncedBlock between chunks — avoids a sequencer network
|
||||
// call (get_current_block_height) on every iteration.
|
||||
const int lastVal = m_logos->logos_execution_zone.get_last_synced_block();
|
||||
if (lastSyncedBlock() != lastVal)
|
||||
setLastSyncedBlock(lastVal);
|
||||
QTimer::singleShot(0, this, &LEZWalletBackend::syncNextChunk);
|
||||
}
|
||||
|
||||
void LEZWalletBackend::updateBalances()
|
||||
{
|
||||
if (!m_accountModel) return;
|
||||
bool anyFailed = false;
|
||||
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::AddressRole).toString();
|
||||
const QString addr = m_accountModel->data(idx, LEZWalletAccountModel::AccountIdRole).toString();
|
||||
const bool isPub = m_accountModel->data(idx, LEZWalletAccountModel::IsPublicRole).toBool();
|
||||
m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub));
|
||||
const QString bal = getBalance(addr, isPub);
|
||||
if (!bal.isEmpty())
|
||||
m_accountModel->setBalanceByAccountId(addr, bal);
|
||||
else
|
||||
anyFailed = true;
|
||||
}
|
||||
saveWallet();
|
||||
if (anyFailed)
|
||||
QTimer::singleShot(3000, this, &LEZWalletBackend::updateBalances);
|
||||
else
|
||||
saveWallet();
|
||||
}
|
||||
|
||||
void LEZWalletBackend::fetchAndUpdateBlockHeights()
|
||||
@ -148,16 +215,6 @@ void LEZWalletBackend::fetchAndUpdateBlockHeights()
|
||||
setCurrentBlockHeight(currentVal);
|
||||
}
|
||||
|
||||
void LEZWalletBackend::refreshBlockHeights()
|
||||
{
|
||||
fetchAndUpdateBlockHeights();
|
||||
if (currentBlockHeight() > 0
|
||||
&& lastSyncedBlock() < currentBlockHeight()
|
||||
&& syncToBlock(currentBlockHeight()))
|
||||
{
|
||||
fetchAndUpdateBlockHeights();
|
||||
}
|
||||
}
|
||||
|
||||
void LEZWalletBackend::refreshSequencerAddr()
|
||||
{
|
||||
@ -216,7 +273,6 @@ QString LEZWalletBackend::transferPrivate(QString fromHex, QString toHex, QStrin
|
||||
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
||||
|
||||
QString keysPayload = toHex.trimmed();
|
||||
// If "To" is not JSON (e.g. user pasted account id hex), resolve to keys.
|
||||
if (!keysPayload.startsWith(QLatin1Char('{'))) {
|
||||
qDebug() << "LEZWalletBackend::transferPrivate: resolving keys via get_private_account_keys";
|
||||
const QString resolved = getPrivateAccountKeys(keysPayload);
|
||||
@ -224,27 +280,97 @@ QString LEZWalletBackend::transferPrivate(QString fromHex, QString toHex, QStrin
|
||||
keysPayload = resolved;
|
||||
}
|
||||
|
||||
return m_logos->logos_execution_zone.transfer_private(fromHex, keysPayload, amountHex);
|
||||
return m_logosAPI->getClient(LEZ_MODULE)->invokeRemoteMethod(
|
||||
LEZ_MODULE, "transfer_private",
|
||||
QVariantList{fromHex.trimmed(), keysPayload, amountHex},
|
||||
NO_TIMEOUT).toString();
|
||||
}
|
||||
|
||||
QString LEZWalletBackend::transferPrivateOwned(QString fromHex, QString toHex, QString amountStr)
|
||||
{
|
||||
const QString amountHex = amountToLe16Hex(amountStr);
|
||||
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
||||
return m_logos->logos_execution_zone.transfer_private_owned(fromHex, toHex.trimmed(), amountHex);
|
||||
return m_logosAPI->getClient(LEZ_MODULE)->invokeRemoteMethod(
|
||||
LEZ_MODULE, "transfer_private_owned",
|
||||
QVariantList{fromHex.trimmed(), toHex.trimmed(), amountHex},
|
||||
NO_TIMEOUT).toString();
|
||||
}
|
||||
|
||||
bool LEZWalletBackend::createNew(QString configPath, QString storagePath, QString password)
|
||||
QString LEZWalletBackend::transferShielded(QString fromHex, QString toKeysJson, QString amountStr)
|
||||
{
|
||||
const QString localPath = toLocalPath(configPath);
|
||||
int err = m_logos->logos_execution_zone.create_new(localPath, storagePath, password);
|
||||
const QString amountHex = amountToLe16Hex(amountStr);
|
||||
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
||||
|
||||
QString keysPayload = toKeysJson.trimmed();
|
||||
if (!keysPayload.startsWith(QLatin1Char('{'))) {
|
||||
qDebug() << "LEZWalletBackend::transferShielded: resolving keys via get_private_account_keys";
|
||||
const QString resolved = getPrivateAccountKeys(keysPayload);
|
||||
if (!resolved.isEmpty())
|
||||
keysPayload = resolved;
|
||||
}
|
||||
|
||||
return m_logosAPI->getClient(LEZ_MODULE)->invokeRemoteMethod(
|
||||
LEZ_MODULE, "transfer_shielded",
|
||||
QVariantList{fromHex.trimmed(), keysPayload, amountHex},
|
||||
NO_TIMEOUT).toString();
|
||||
}
|
||||
|
||||
QString LEZWalletBackend::transferShieldedOwned(QString fromHex, QString toHex, QString amountStr)
|
||||
{
|
||||
const QString amountHex = amountToLe16Hex(amountStr);
|
||||
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
||||
return m_logosAPI->getClient(LEZ_MODULE)->invokeRemoteMethod(
|
||||
LEZ_MODULE, "transfer_shielded_owned",
|
||||
QVariantList{fromHex.trimmed(), toHex.trimmed(), amountHex},
|
||||
NO_TIMEOUT).toString();
|
||||
}
|
||||
|
||||
QString LEZWalletBackend::transferDeshielded(QString fromHex, QString toHex, QString amountStr)
|
||||
{
|
||||
const QString amountHex = amountToLe16Hex(amountStr);
|
||||
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
||||
return m_logosAPI->getClient(LEZ_MODULE)->invokeRemoteMethod(
|
||||
LEZ_MODULE, "transfer_deshielded",
|
||||
QVariantList{fromHex.trimmed(), toHex.trimmed(), amountHex},
|
||||
NO_TIMEOUT).toString();
|
||||
}
|
||||
|
||||
void LEZWalletBackend::applySequencerAddrToConfig(const QString& configPath, const QString& sequencerAddr)
|
||||
{
|
||||
QJsonObject obj;
|
||||
QFile file(configPath);
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
obj = QJsonDocument::fromJson(file.readAll()).object();
|
||||
file.close();
|
||||
} else {
|
||||
// Defaults matching WalletConfig::default() in the wallet crate.
|
||||
obj[QStringLiteral("seq_poll_timeout")] = QStringLiteral("30s");
|
||||
obj[QStringLiteral("seq_tx_poll_max_blocks")] = 15;
|
||||
obj[QStringLiteral("seq_poll_max_retries")] = 10;
|
||||
obj[QStringLiteral("seq_block_poll_max_amount")] = 100;
|
||||
}
|
||||
obj[QStringLiteral("sequencer_addr")] = sequencerAddr;
|
||||
|
||||
QDir().mkpath(QFileInfo(configPath).absolutePath());
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||
file.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
|
||||
}
|
||||
|
||||
bool LEZWalletBackend::createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr)
|
||||
{
|
||||
const QString localConfigPath = toLocalPath(configPath);
|
||||
const QString localStoragePath = toLocalPath(storagePath);
|
||||
|
||||
if (!sequencerAddr.isEmpty())
|
||||
applySequencerAddrToConfig(localConfigPath, sequencerAddr);
|
||||
|
||||
int err = m_logos->logos_execution_zone.create_new(localConfigPath, localStoragePath, password);
|
||||
if (err != WALLET_FFI_SUCCESS) return false;
|
||||
|
||||
persistConfigPath(localPath);
|
||||
persistStoragePath(storagePath);
|
||||
persistConfigPath(localConfigPath);
|
||||
persistStoragePath(localStoragePath);
|
||||
setIsWalletOpen(true);
|
||||
refreshAccounts();
|
||||
refreshBlockHeights();
|
||||
refreshSequencerAddr();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ class LEZWalletBackend : public LEZWalletBackendSimpleSource {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(LEZWalletAccountModel* accountModel READ accountModel CONSTANT)
|
||||
Q_PROPERTY(LEZAccountFilterModel* filteredAccountModel READ filteredAccountModel CONSTANT)
|
||||
Q_PROPERTY(LEZAccountFilterModel* privateAccountModel READ privateAccountModel CONSTANT)
|
||||
|
||||
public:
|
||||
explicit LEZWalletBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
||||
@ -26,6 +27,7 @@ public:
|
||||
|
||||
LEZWalletAccountModel* accountModel() const { return m_accountModel; }
|
||||
LEZAccountFilterModel* filteredAccountModel() const { return m_filteredAccountModel; }
|
||||
LEZAccountFilterModel* privateAccountModel() const { return m_privateAccountModel; }
|
||||
|
||||
public slots:
|
||||
// Overrides of the pure-virtual slots generated from the .rep.
|
||||
@ -40,20 +42,34 @@ public slots:
|
||||
QString transferPublic(QString fromHex, QString toHex, QString amountStr) override;
|
||||
QString transferPrivate(QString fromHex, QString toHex, QString amountStr) override;
|
||||
QString transferPrivateOwned(QString fromHex, QString toHex, QString amountStr) override;
|
||||
bool createNew(QString configPath, QString storagePath, QString password) override;
|
||||
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;
|
||||
void copyToClipboard(QString text) override;
|
||||
|
||||
private slots:
|
||||
void syncNextChunk();
|
||||
|
||||
private:
|
||||
void persistConfigPath(const QString& path);
|
||||
void persistStoragePath(const QString& path);
|
||||
void refreshBlockHeights();
|
||||
void applySequencerAddrToConfig(const QString& configPath, const QString& sequencerAddr);
|
||||
void fetchAndUpdateBlockHeights();
|
||||
void startChunkedSync();
|
||||
|
||||
void updateBalances();
|
||||
void refreshSequencerAddr();
|
||||
void saveWallet();
|
||||
void fetchAndUpdateBlockHeights();
|
||||
void openIfPathsConfigured();
|
||||
|
||||
bool m_syncing = false;
|
||||
quint64 m_syncTarget = 0;
|
||||
static constexpr quint64 SYNC_CHUNK_SIZE = 100;
|
||||
|
||||
LEZWalletAccountModel* m_accountModel;
|
||||
LEZAccountFilterModel* m_filteredAccountModel;
|
||||
LEZAccountFilterModel* m_privateAccountModel;
|
||||
|
||||
LogosAPI* m_logosAPI;
|
||||
LogosModules* m_logos;
|
||||
|
||||
@ -19,8 +19,11 @@ class LEZWalletBackend
|
||||
SLOT(QString transferPublic(QString fromHex, QString toHex, QString amountStr))
|
||||
SLOT(QString transferPrivate(QString fromHex, QString toHex, QString amountStr))
|
||||
SLOT(QString transferPrivateOwned(QString fromHex, QString toHex, QString amountStr))
|
||||
SLOT(QString transferShielded(QString fromHex, QString toKeysJson, QString amountStr))
|
||||
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))
|
||||
SLOT(bool createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr))
|
||||
|
||||
SLOT(void copyToClipboard(QString text))
|
||||
}
|
||||
|
||||
71
src/qml/Base58.js
Normal file
71
src/qml/Base58.js
Normal file
@ -0,0 +1,71 @@
|
||||
.pragma library
|
||||
|
||||
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
|
||||
// Encode a hex string to base58.
|
||||
function encode(hexStr) {
|
||||
if (!hexStr || hexStr.length === 0) return ""
|
||||
const clean = hexStr.toLowerCase().replace(/^0x/, "")
|
||||
if (clean.length === 0 || clean.length % 2 !== 0) return hexStr
|
||||
|
||||
const bytes = []
|
||||
for (let i = 0; i < clean.length; i += 2)
|
||||
bytes.push(parseInt(clean.substr(i, 2), 16))
|
||||
|
||||
let leadingZeros = 0
|
||||
while (leadingZeros < bytes.length && bytes[leadingZeros] === 0)
|
||||
leadingZeros++
|
||||
|
||||
// digits[i] holds base-58 digits, least-significant first.
|
||||
// For each input byte, multiply existing digits by 256 and add the byte.
|
||||
const digits = []
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
let carry = bytes[i]
|
||||
for (let j = 0; j < digits.length; j++) {
|
||||
carry += digits[j] * 256
|
||||
digits[j] = carry % 58
|
||||
carry = Math.floor(carry / 58)
|
||||
}
|
||||
while (carry > 0) {
|
||||
digits.push(carry % 58)
|
||||
carry = Math.floor(carry / 58)
|
||||
}
|
||||
}
|
||||
|
||||
let result = "1".repeat(leadingZeros)
|
||||
for (let i = digits.length - 1; i >= 0; i--)
|
||||
result += ALPHABET[digits[i]]
|
||||
return result
|
||||
}
|
||||
|
||||
// Decode a base58 string to hex.
|
||||
function decode(b58Str) {
|
||||
if (!b58Str || b58Str.length === 0) return ""
|
||||
|
||||
let leadingZeros = 0
|
||||
while (leadingZeros < b58Str.length && b58Str[leadingZeros] === "1")
|
||||
leadingZeros++
|
||||
|
||||
// digits[i] holds base-256 (byte) digits, least-significant first.
|
||||
// For each input char, multiply existing digits by 58 and add the char value.
|
||||
const digits = []
|
||||
for (let i = 0; i < b58Str.length; i++) {
|
||||
const idx = ALPHABET.indexOf(b58Str[i])
|
||||
if (idx < 0) return ""
|
||||
let carry = idx
|
||||
for (let j = 0; j < digits.length; j++) {
|
||||
carry += digits[j] * 58
|
||||
digits[j] = carry % 256
|
||||
carry = Math.floor(carry / 256)
|
||||
}
|
||||
while (carry > 0) {
|
||||
digits.push(carry % 256)
|
||||
carry = Math.floor(carry / 256)
|
||||
}
|
||||
}
|
||||
|
||||
let hex = "00".repeat(leadingZeros)
|
||||
for (let i = digits.length - 1; i >= 0; i--)
|
||||
hex += (digits[i] < 16 ? "0" : "") + digits[i].toString(16)
|
||||
return hex
|
||||
}
|
||||
@ -11,6 +11,8 @@ Rectangle {
|
||||
|
||||
readonly property var backend: logos.module("lez_wallet_ui")
|
||||
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")
|
||||
property bool ready: false
|
||||
|
||||
Connections {
|
||||
@ -59,7 +61,6 @@ Rectangle {
|
||||
}
|
||||
|
||||
// Parse a transfer result JSON string and write to dashboardView.
|
||||
// Used by all three transfer handlers below.
|
||||
function applyTransferResult(dashboardView, raw) {
|
||||
var msg = raw || ""
|
||||
var isError = false
|
||||
@ -76,6 +77,7 @@ Rectangle {
|
||||
}
|
||||
dashboardView.transferResult = msg
|
||||
dashboardView.transferResultIsError = isError
|
||||
dashboardView.transferTxHash = (obj && obj.tx_hash) ? obj.tx_hash : ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,9 +116,9 @@ Rectangle {
|
||||
OnboardingView {
|
||||
storePath: backend ? backend.storagePath : ""
|
||||
configPath: backend ? backend.configPath : ""
|
||||
onCreateWallet: function(configPath, storagePath, password) {
|
||||
onCreateWallet: function(configPath, storagePath, password, sequencerUrl) {
|
||||
if (!backend) return
|
||||
logos.watch(backend.createNew(configPath, storagePath, password),
|
||||
logos.watch(backend.createNew(configPath, storagePath, password, sequencerUrl),
|
||||
function(ok) {
|
||||
if (!ok)
|
||||
createError = qsTr("Failed to create wallet. Check paths and try again.")
|
||||
@ -134,6 +136,10 @@ Rectangle {
|
||||
DashboardView {
|
||||
id: dashboardView
|
||||
accountModel: root.accountModel
|
||||
publicAccountModel: root.publicAccountModel
|
||||
privateAccountModel: root.privateAccountModel
|
||||
lastSyncedBlock: backend ? backend.lastSyncedBlock : 0
|
||||
currentBlockHeight: backend ? backend.currentBlockHeight : 0
|
||||
|
||||
onCreatePublicAccountRequested: {
|
||||
if (!backend) { console.warn("backend is null"); return }
|
||||
@ -160,24 +166,67 @@ Rectangle {
|
||||
function(error) {
|
||||
dashboardView.transferResult = qsTr("Error: %1").arg(error)
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onTransferPrivateRequested: (fromId, toKeysJsonOrAddress, amount) => {
|
||||
if (!backend) return
|
||||
dashboardView.transferPending = true
|
||||
logos.watch(backend.transferPrivate(fromId, toKeysJsonOrAddress, amount),
|
||||
function(raw) { ffiErrors.applyTransferResult(dashboardView, raw) },
|
||||
function(raw) { dashboardView.transferPending = false; ffiErrors.applyTransferResult(dashboardView, raw) },
|
||||
function(error) {
|
||||
dashboardView.transferPending = false
|
||||
dashboardView.transferResult = qsTr("Error: %1").arg(error)
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onTransferPrivateOwnedRequested: (fromId, toAccountId, amount) => {
|
||||
if (!backend) return
|
||||
dashboardView.transferPending = true
|
||||
logos.watch(backend.transferPrivateOwned(fromId, toAccountId, amount),
|
||||
function(raw) { ffiErrors.applyTransferResult(dashboardView, raw) },
|
||||
function(raw) { dashboardView.transferPending = false; ffiErrors.applyTransferResult(dashboardView, raw) },
|
||||
function(error) {
|
||||
dashboardView.transferPending = false
|
||||
dashboardView.transferResult = qsTr("Error: %1").arg(error)
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onTransferShieldedRequested: (fromId, toKeysJsonOrAddress, amount) => {
|
||||
if (!backend) return
|
||||
dashboardView.transferPending = true
|
||||
logos.watch(backend.transferShielded(fromId, toKeysJsonOrAddress, amount),
|
||||
function(raw) { dashboardView.transferPending = false; ffiErrors.applyTransferResult(dashboardView, raw) },
|
||||
function(error) {
|
||||
dashboardView.transferPending = false
|
||||
dashboardView.transferResult = qsTr("Error: %1").arg(error)
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onTransferShieldedOwnedRequested: (fromId, toAccountId, amount) => {
|
||||
if (!backend) return
|
||||
dashboardView.transferPending = true
|
||||
logos.watch(backend.transferShieldedOwned(fromId, toAccountId, amount),
|
||||
function(raw) { dashboardView.transferPending = false; ffiErrors.applyTransferResult(dashboardView, raw) },
|
||||
function(error) {
|
||||
dashboardView.transferPending = false
|
||||
dashboardView.transferResult = qsTr("Error: %1").arg(error)
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onTransferDeshieldedRequested: (fromId, toAccountId, amount) => {
|
||||
if (!backend) return
|
||||
dashboardView.transferPending = true
|
||||
logos.watch(backend.transferDeshielded(fromId, toAccountId, amount),
|
||||
function(raw) { dashboardView.transferPending = false; ffiErrors.applyTransferResult(dashboardView, raw) },
|
||||
function(error) {
|
||||
dashboardView.transferPending = false
|
||||
dashboardView.transferResult = qsTr("Error: %1").arg(error)
|
||||
dashboardView.transferResultIsError = true
|
||||
dashboardView.transferTxHash = ""
|
||||
})
|
||||
}
|
||||
onCopyRequested: (copyText) => {
|
||||
@ -185,6 +234,16 @@ Rectangle {
|
||||
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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
import "../Base58.js" as Base58
|
||||
|
||||
ComboBox {
|
||||
id: root
|
||||
@ -11,12 +12,13 @@ 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
|
||||
implicitHeight: 40
|
||||
textRole: "name"
|
||||
valueRole: "address"
|
||||
valueRole: "accountId"
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.spacing.radiusSmall
|
||||
@ -45,7 +47,7 @@ ComboBox {
|
||||
selectByMouse: true
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.text
|
||||
text: root.displayText
|
||||
text: root.currentValue ? ("Account " + Base58.encode(root.currentValue).substring(0, 4)) : root.displayText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
clip: true
|
||||
}
|
||||
@ -60,6 +62,7 @@ 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 {
|
||||
|
||||
@ -4,6 +4,7 @@ import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
import "../Base58.js" as Base58
|
||||
|
||||
ItemDelegate {
|
||||
id: root
|
||||
@ -13,6 +14,7 @@ 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
|
||||
@ -33,7 +35,7 @@ ItemDelegate {
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: model.name ?? ""
|
||||
text: model.name || ("Account " + Base58.encode(model.accountId ?? "").slice(0, 4))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
@ -68,7 +70,7 @@ ItemDelegate {
|
||||
id: addressLabel
|
||||
Layout.fillWidth: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: model.address ?? ""
|
||||
text: Base58.encode(model.accountId ?? "")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textMuted
|
||||
elide: Text.ElideMiddle
|
||||
@ -76,7 +78,9 @@ ItemDelegate {
|
||||
LogosCopyButton {
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 40
|
||||
onCopyText: root.copyRequested(model.address)
|
||||
onCopyText: model.isPublic
|
||||
? root.copyRequested(Base58.encode(model.accountId ?? ""))
|
||||
: root.copyPublicKeysRequested(model.accountId ?? "")
|
||||
visible: addressLabel.text
|
||||
icon.color: Theme.palette.textMuted
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ Popup {
|
||||
|
||||
LogosText {
|
||||
text: tabBar.currentIndex === 0
|
||||
? qsTr("Address visible. Balance on-chain.")
|
||||
? qsTr("Account ID visible. Balance on-chain.")
|
||||
: qsTr("Private balance and activity.")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
|
||||
@ -13,12 +13,15 @@ Rectangle {
|
||||
|
||||
// --- Public API: data in ---
|
||||
property var accountModel: null
|
||||
property int lastSyncedBlock: 0
|
||||
property int currentBlockHeight: 0
|
||||
|
||||
// --- Public API: signals out ---
|
||||
signal createPublicAccountRequested()
|
||||
signal createPrivateAccountRequested()
|
||||
signal fetchBalancesRequested()
|
||||
signal copyRequested(string text)
|
||||
signal copyPublicKeysRequested(string accountIdHex)
|
||||
|
||||
radius: Theme.spacing.radiusXlarge
|
||||
color: Theme.palette.backgroundSecondary
|
||||
@ -57,6 +60,50 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync progress
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.xsmall
|
||||
visible: root.currentBlockHeight > 0 && root.lastSyncedBlock < root.currentBlockHeight
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
LogosText {
|
||||
text: qsTr("Syncing")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosText {
|
||||
text: root.lastSyncedBlock + " / " + root.currentBlockHeight
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
ProgressBar {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: root.currentBlockHeight
|
||||
value: root.lastSyncedBlock
|
||||
|
||||
contentItem: Item {
|
||||
Rectangle {
|
||||
width: parent.width * (root.currentBlockHeight > 0
|
||||
? root.lastSyncedBlock / root.currentBlockHeight : 0)
|
||||
height: parent.height
|
||||
radius: height / 2
|
||||
color: Theme.palette.overlayOrange
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitHeight: 6
|
||||
radius: height / 2
|
||||
color: Theme.palette.backgroundElevated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state (when no real model and we don't show showcase)
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
@ -83,6 +130,7 @@ Rectangle {
|
||||
delegate: AccountDelegate {
|
||||
width: listView.width
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
onCopyPublicKeysRequested: (id) => root.copyPublicKeysRequested(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,8 +10,14 @@ Rectangle {
|
||||
|
||||
// --- Public API: input properties (set by parent / MainView) ---
|
||||
property var accountModel: null
|
||||
property var publicAccountModel: null
|
||||
property var privateAccountModel: null
|
||||
property string transferResult: ""
|
||||
property string transferTxHash: ""
|
||||
property bool transferResultIsError: false
|
||||
property bool transferPending: false
|
||||
property int lastSyncedBlock: 0
|
||||
property int currentBlockHeight: 0
|
||||
|
||||
// --- Public API: output signals (parent connects and calls backend) ---
|
||||
signal createPublicAccountRequested()
|
||||
@ -20,7 +26,11 @@ Rectangle {
|
||||
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)
|
||||
signal copyPublicKeysRequested(string accountIdHex)
|
||||
|
||||
color: Theme.palette.background
|
||||
|
||||
@ -35,24 +45,33 @@ Rectangle {
|
||||
Layout.fillHeight: true
|
||||
|
||||
accountModel: root.accountModel
|
||||
lastSyncedBlock: root.lastSyncedBlock
|
||||
currentBlockHeight: root.currentBlockHeight
|
||||
|
||||
onCreatePublicAccountRequested: root.createPublicAccountRequested()
|
||||
onCreatePrivateAccountRequested: root.createPrivateAccountRequested()
|
||||
onFetchBalancesRequested: root.fetchBalancesRequested()
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
onCopyPublicKeysRequested: (id) => root.copyPublicKeysRequested(id)
|
||||
}
|
||||
|
||||
TransferPanel {
|
||||
id: transferPanel
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
fromAccountModel: root.accountModel
|
||||
publicAccountModel: root.publicAccountModel
|
||||
privateAccountModel: root.privateAccountModel
|
||||
transferResult: root.transferResult
|
||||
transferTxHash: root.transferTxHash
|
||||
transferResultIsError: root.transferResultIsError
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,10 @@ Control {
|
||||
property string storePath: ""
|
||||
property string createError: ""
|
||||
|
||||
signal createWallet(string configPath, string storagePath, string password)
|
||||
signal createWallet(string configPath, string storagePath, string password, string sequencerUrl)
|
||||
|
||||
readonly property string testnetUrl: "https://testnet.lez.logos.co"
|
||||
readonly property string localhostUrl: "http://127.0.0.1:3040"
|
||||
|
||||
|
||||
QtObject {
|
||||
@ -90,6 +93,38 @@ Control {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Theme.spacing.medium
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: qsTr("Network")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.weight: Theme.typography.weightMedium
|
||||
color: Theme.palette.text
|
||||
}
|
||||
LogosTextField {
|
||||
id: sequencerUrlField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Sequencer URL")
|
||||
text: root.testnetUrl
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosButton {
|
||||
text: qsTr("Testnet")
|
||||
opacity: sequencerUrlField.text === root.testnetUrl ? 1.0 : 0.4
|
||||
onClicked: sequencerUrlField.text = root.testnetUrl
|
||||
}
|
||||
LogosButton {
|
||||
text: qsTr("Localhost")
|
||||
opacity: sequencerUrlField.text === root.localhostUrl ? 1.0 : 0.4
|
||||
onClicked: sequencerUrlField.text = root.localhostUrl
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Security")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
@ -131,7 +166,7 @@ Control {
|
||||
root.createError = qsTr("Passwords do not match.")
|
||||
} else {
|
||||
root.createError = ""
|
||||
root.createWallet(configPathField.text, storagePathField.text, passwordField.text)
|
||||
root.createWallet(configPathField.text, storagePathField.text, passwordField.text, sequencerUrlField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,31 +6,45 @@ import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
import "../Base58.js" as Base58
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// --- Public API: data in ---
|
||||
property var fromAccountModel: null
|
||||
property var publicAccountModel: null
|
||||
property var privateAccountModel: 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) ---
|
||||
// --- Public API: signals out (match backend: transfer_public, transfer_private, transfer_private_owned, transfer_shielded, transfer_shielded_owned) ---
|
||||
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 toAddressValid: isPrivateTab && useOwnedAccountForTo
|
||||
? (fromFilterCount > 0 && toCombo.currentIndex >= 0)
|
||||
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 sendEnabled: amountField && manualFromField
|
||||
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))
|
||||
@ -54,7 +68,7 @@ Rectangle {
|
||||
// Transfer type toggle
|
||||
TabBar {
|
||||
id: transferTypeBar
|
||||
Layout.preferredWidth: 200
|
||||
Layout.preferredWidth: 400
|
||||
currentIndex: 0
|
||||
|
||||
background: Rectangle {
|
||||
@ -69,6 +83,14 @@ Rectangle {
|
||||
LogosTabButton {
|
||||
text: qsTr("Private")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Shielded")
|
||||
}
|
||||
|
||||
LogosTabButton {
|
||||
text: qsTr("Deshielded")
|
||||
}
|
||||
}
|
||||
|
||||
// From: dropdown when accounts exist, or manual entry when list is empty
|
||||
@ -85,14 +107,14 @@ Rectangle {
|
||||
LogosTextField {
|
||||
id: manualFromField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Paste or type from address")
|
||||
placeholderText: qsTr("Paste or type from account ID")
|
||||
visible: fromFilterCount === 0
|
||||
}
|
||||
|
||||
AccountComboBox {
|
||||
id: fromCombo
|
||||
Layout.fillWidth: true
|
||||
model: fromAccountModel
|
||||
model: (d.isPrivateTab || d.isDeshieldedTab) ? root.privateAccountModel : root.publicAccountModel
|
||||
visible: fromFilterCount > 0
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
}
|
||||
@ -109,28 +131,26 @@ Rectangle {
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
LogosCheckbox {
|
||||
id: useOwnedToCheck
|
||||
visible: d.isPrivateTab
|
||||
visible: d.showOwnedOption
|
||||
checked: d.useOwnedAccountForTo
|
||||
onCheckedChanged: d.useOwnedAccountForTo = checked
|
||||
text: qsTr("Use owned account")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
palette.text: Theme.palette.text
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: toField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Recipient public key")
|
||||
visible: !d.isPrivateTab || !d.useOwnedAccountForTo
|
||||
placeholderText: (d.isPublicTab || d.isDeshieldedTab) ? qsTr("Recipient account ID") : qsTr("Recipient public keys (JSON)")
|
||||
visible: !d.useOwnedAccountForTo
|
||||
}
|
||||
|
||||
AccountComboBox {
|
||||
id: toCombo
|
||||
Layout.fillWidth: true
|
||||
model: fromAccountModel
|
||||
visible: d.isPrivateTab && d.useOwnedAccountForTo && fromFilterCount > 0
|
||||
model: (d.isPublicTab || d.isDeshieldedTab) ? root.publicAccountModel : root.privateAccountModel
|
||||
visible: d.useOwnedAccountForTo && toFilterCount > 0
|
||||
onCopyRequested: (text) => root.copyRequested(text)
|
||||
}
|
||||
}
|
||||
@ -162,25 +182,56 @@ Rectangle {
|
||||
onClicked: {
|
||||
var fromId = fromFilterCount > 0 && fromCombo.currentIndex >= 0
|
||||
? (fromCombo.currentValue ?? "")
|
||||
: manualFromField.text.trim()
|
||||
var toAddress = d.useOwnedAccountForTo && toCombo.currentIndex >= 0
|
||||
: Base58.decode(manualFromField.text.trim())
|
||||
var rawTo = toField.text.trim()
|
||||
var toAddress = (d.useOwnedAccountForTo && toCombo.currentIndex >= 0)
|
||||
? (toCombo.currentValue ?? "")
|
||||
: toField.text.trim()
|
||||
: (rawTo.startsWith("{") ? rawTo : Base58.decode(rawTo))
|
||||
var amount = amountField.text.trim()
|
||||
if (fromId.length > 0 && toAddress.length > 0 && amount.length > 0) {
|
||||
if (transferTypeBar.currentIndex === 0)
|
||||
if (d.isPublicTab)
|
||||
root.transferPublicRequested(fromId, toAddress, amount)
|
||||
else if (d.useOwnedAccountForTo)
|
||||
root.transferPrivateOwnedRequested(fromId, toAddress, amount)
|
||||
else
|
||||
root.transferPrivateRequested(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proof pending indicator
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.transferPending
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosSpinner {
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
running: root.transferPending
|
||||
}
|
||||
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Generating proof, please wait…")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
|
||||
// Result label
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.transferPending
|
||||
LogosText {
|
||||
id: resultText
|
||||
Layout.fillWidth: true
|
||||
@ -195,7 +246,7 @@ Rectangle {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.preferredHeight: 40
|
||||
Layout.preferredWidth: 40
|
||||
onCopyText: root.copyRequested(root.transferResult)
|
||||
onCopyText: root.copyRequested(root.transferTxHash || root.transferResult)
|
||||
visible: resultText.text
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user