2026-02-20 14:20:39 +01:00
|
|
|
#include "LEZWalletBackend.h"
|
2026-04-17 00:37:17 +02:00
|
|
|
|
2026-02-22 02:03:13 +05:30
|
|
|
#include <QAbstractItemModel>
|
2026-02-23 18:09:06 +01:00
|
|
|
#include <QClipboard>
|
2026-04-17 00:37:17 +02:00
|
|
|
#include <QCoreApplication>
|
2026-02-20 14:20:39 +01:00
|
|
|
#include <QDebug>
|
2026-02-23 18:09:06 +01:00
|
|
|
#include <QGuiApplication>
|
2026-02-22 02:03:13 +05:30
|
|
|
#include <QJsonArray>
|
2026-02-20 14:20:39 +01:00
|
|
|
#include <QSettings>
|
2026-04-17 00:37:17 +02:00
|
|
|
#include <QTimer>
|
2026-02-20 14:20:39 +01:00
|
|
|
#include <QUrl>
|
2026-06-05 19:08:50 +03:00
|
|
|
#include <QList>
|
|
|
|
|
#include <QString>
|
2026-02-20 14:20:39 +01:00
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
#include "logos_api.h"
|
2026-04-20 16:41:20 +02:00
|
|
|
#include "logos_sdk.h"
|
2026-04-17 00:37:17 +02:00
|
|
|
|
2026-02-20 14:20:39 +01:00
|
|
|
namespace {
|
|
|
|
|
const char SETTINGS_ORG[] = "Logos";
|
|
|
|
|
const char SETTINGS_APP[] = "ExecutionZoneWalletUI";
|
|
|
|
|
const char CONFIG_PATH_KEY[] = "configPath";
|
|
|
|
|
const char STORAGE_PATH_KEY[] = "storagePath";
|
|
|
|
|
const int WALLET_FFI_SUCCESS = 0;
|
2026-02-23 18:09:06 +01:00
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
// Convert a decimal amount string to 32-char hex (16 bytes little-endian)
|
|
|
|
|
// for transfer_public/transfer_private/transfer_private_owned.
|
2026-02-23 18:09:06 +01:00
|
|
|
QString amountToLe16Hex(const QString& amountStr) {
|
|
|
|
|
const QString trimmed = amountStr.trimmed();
|
|
|
|
|
if (trimmed.isEmpty()) return QString();
|
|
|
|
|
bool parseOk = false;
|
|
|
|
|
const quint64 value = trimmed.toULongLong(&parseOk);
|
|
|
|
|
if (!parseOk) return QString();
|
|
|
|
|
uint8_t bytes[16] = {0};
|
|
|
|
|
for (int i = 0; i < 8; ++i)
|
|
|
|
|
bytes[i] = static_cast<uint8_t>((value >> (i * 8)) & 0xff);
|
|
|
|
|
return QByteArray(reinterpret_cast<const char*>(bytes), 16).toHex();
|
|
|
|
|
}
|
2026-04-17 00:37:17 +02:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-05 19:08:50 +03:00
|
|
|
static bool hexToU128(const QString& hex, uint8_t (*output)[16]) {
|
|
|
|
|
QByteArray buffer;
|
|
|
|
|
if (!hexToBytes(hex, buffer, 16))
|
|
|
|
|
return false;
|
|
|
|
|
memcpy(output, buffer.constData(), 16);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 14:20:39 +01:00
|
|
|
LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent)
|
2026-04-17 00:37:17 +02:00
|
|
|
: LEZWalletBackendSimpleSource(parent),
|
2026-02-22 02:03:13 +05:30
|
|
|
m_accountModel(new LEZWalletAccountModel(this)),
|
|
|
|
|
m_filteredAccountModel(new LEZAccountFilterModel(this)),
|
2026-04-20 15:17:40 +02:00
|
|
|
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("lez_wallet_ui", this)),
|
2026-04-20 16:41:20 +02:00
|
|
|
m_logos(new LogosModules(m_logosAPI))
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-02-22 02:03:13 +05:30
|
|
|
m_filteredAccountModel->setSourceModel(m_accountModel);
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
// Initialise PROP defaults via the generated setters.
|
|
|
|
|
setIsWalletOpen(false);
|
|
|
|
|
setLastSyncedBlock(0);
|
|
|
|
|
setCurrentBlockHeight(0);
|
|
|
|
|
|
|
|
|
|
// Load persisted config/storage paths.
|
2026-02-20 14:20:39 +01:00
|
|
|
QSettings s(SETTINGS_ORG, SETTINGS_APP);
|
2026-04-17 00:37:17 +02:00
|
|
|
setConfigPath(s.value(CONFIG_PATH_KEY).toString());
|
|
|
|
|
setStoragePath(s.value(STORAGE_PATH_KEY).toString());
|
2026-02-20 14:20:39 +01:00
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
// ui-host runs our constructor inside initLogos(), synchronously, BEFORE
|
|
|
|
|
// it enables remoting and emits READY. Any blocking RPC here (open,
|
|
|
|
|
// list_accounts, block-height queries, sequencer lookup) would stall
|
|
|
|
|
// ui-host startup past ViewModuleHost's 30s ready watchdog and get the
|
|
|
|
|
// child SIGTERM'd. Defer the whole open+refresh chain to the first
|
|
|
|
|
// event-loop tick so ui-host finishes wiring itself up first.
|
|
|
|
|
QTimer::singleShot(0, this, [this]() { openIfPathsConfigured(); });
|
|
|
|
|
|
|
|
|
|
// Save wallet when app quits; host may not call destructors so this is best-effort.
|
|
|
|
|
connect(qApp, &QCoreApplication::aboutToQuit, this,
|
|
|
|
|
[this]() { saveWallet(); }, Qt::DirectConnection);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LEZWalletBackend::~LEZWalletBackend()
|
|
|
|
|
{
|
|
|
|
|
saveWallet();
|
2026-04-20 16:41:20 +02:00
|
|
|
delete m_logos;
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LEZWalletBackend::saveWallet()
|
|
|
|
|
{
|
2026-04-20 16:41:20 +02:00
|
|
|
if (isWalletOpen()) {
|
2026-05-21 17:08:02 +02:00
|
|
|
m_logos->logos_execution_zone.save();
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
void LEZWalletBackend::persistConfigPath(const QString& path)
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-04-17 00:37:17 +02:00
|
|
|
const QString localPath = toLocalPath(path);
|
|
|
|
|
setConfigPath(localPath);
|
|
|
|
|
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(CONFIG_PATH_KEY, localPath);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
void LEZWalletBackend::persistStoragePath(const QString& path)
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-04-17 00:37:17 +02:00
|
|
|
const QString localPath = toLocalPath(path);
|
|
|
|
|
setStoragePath(localPath);
|
|
|
|
|
QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(STORAGE_PATH_KEY, localPath);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
void LEZWalletBackend::openIfPathsConfigured()
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-04-17 00:37:17 +02:00
|
|
|
if (configPath().isEmpty() || storagePath().isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
qDebug() << "LEZWalletBackend: opening wallet with config" << configPath()
|
|
|
|
|
<< "storage" << storagePath();
|
2026-05-21 17:08:02 +02:00
|
|
|
int err = m_logos->logos_execution_zone.open(configPath(), storagePath());
|
2026-04-17 00:37:17 +02:00
|
|
|
if (err == WALLET_FFI_SUCCESS) {
|
|
|
|
|
qDebug() << "LEZWalletBackend: wallet opened successfully";
|
|
|
|
|
setIsWalletOpen(true);
|
|
|
|
|
refreshAccounts();
|
|
|
|
|
refreshBlockHeights();
|
|
|
|
|
refreshSequencerAddr();
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LEZWalletBackend::refreshAccounts()
|
|
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
QJsonArray arr = m_logos->logos_execution_zone.list_accounts();
|
2026-02-22 02:03:13 +05:30
|
|
|
m_accountModel->replaceFromJsonArray(arr);
|
2026-02-23 18:09:06 +01:00
|
|
|
refreshBalances();
|
2026-02-22 02:03:13 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LEZWalletBackend::refreshBalances()
|
|
|
|
|
{
|
2026-03-06 00:19:53 +00:00
|
|
|
refreshBlockHeights();
|
2026-04-17 00:37:17 +02:00
|
|
|
syncToBlock(currentBlockHeight());
|
2026-04-20 16:41:20 +02:00
|
|
|
if (!m_accountModel) return;
|
2026-02-22 02:03:13 +05:30
|
|
|
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 bool isPub = m_accountModel->data(idx, LEZWalletAccountModel::IsPublicRole).toBool();
|
|
|
|
|
m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub));
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
2026-03-07 16:17:57 +00:00
|
|
|
saveWallet();
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 18:09:06 +01:00
|
|
|
void LEZWalletBackend::fetchAndUpdateBlockHeights()
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
const int lastVal = m_logos->logos_execution_zone.get_last_synced_block();
|
|
|
|
|
const int currentVal = m_logos->logos_execution_zone.get_current_block_height();
|
2026-04-17 00:37:17 +02:00
|
|
|
if (lastSyncedBlock() != lastVal)
|
|
|
|
|
setLastSyncedBlock(lastVal);
|
|
|
|
|
if (currentBlockHeight() != currentVal)
|
|
|
|
|
setCurrentBlockHeight(currentVal);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 18:09:06 +01:00
|
|
|
void LEZWalletBackend::refreshBlockHeights()
|
|
|
|
|
{
|
|
|
|
|
fetchAndUpdateBlockHeights();
|
2026-04-17 00:37:17 +02:00
|
|
|
if (currentBlockHeight() > 0
|
|
|
|
|
&& lastSyncedBlock() < currentBlockHeight()
|
|
|
|
|
&& syncToBlock(currentBlockHeight()))
|
|
|
|
|
{
|
2026-02-23 18:09:06 +01:00
|
|
|
fetchAndUpdateBlockHeights();
|
2026-04-17 00:37:17 +02:00
|
|
|
}
|
2026-02-23 18:09:06 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-20 14:20:39 +01:00
|
|
|
void LEZWalletBackend::refreshSequencerAddr()
|
|
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
const QString addr = m_logos->logos_execution_zone.get_sequencer_addr();
|
2026-04-17 00:37:17 +02:00
|
|
|
if (sequencerAddr() != addr)
|
|
|
|
|
setSequencerAddr(addr);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString LEZWalletBackend::createAccountPublic()
|
|
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
QString result = m_logos->logos_execution_zone.create_account_public();
|
2026-04-20 16:41:20 +02:00
|
|
|
if (!result.isEmpty())
|
2026-02-20 14:20:39 +01:00
|
|
|
refreshAccounts();
|
2026-04-20 16:41:20 +02:00
|
|
|
return result;
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString LEZWalletBackend::createAccountPrivate()
|
|
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
QString result = m_logos->logos_execution_zone.create_account_private();
|
2026-04-20 16:41:20 +02:00
|
|
|
if (!result.isEmpty())
|
2026-02-20 14:20:39 +01:00
|
|
|
refreshAccounts();
|
2026-04-20 16:41:20 +02:00
|
|
|
return result;
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
QString LEZWalletBackend::getBalance(QString accountIdHex, bool isPublic)
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
return m_logos->logos_execution_zone.get_balance(accountIdHex, isPublic);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
QString LEZWalletBackend::getPublicAccountKey(QString accountIdHex)
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
return m_logos->logos_execution_zone.get_public_account_key(accountIdHex);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
QString LEZWalletBackend::getPrivateAccountKeys(QString accountIdHex)
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
return m_logos->logos_execution_zone.get_private_account_keys(accountIdHex);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool LEZWalletBackend::syncToBlock(quint64 blockId)
|
|
|
|
|
{
|
2026-05-21 17:08:02 +02:00
|
|
|
int err = m_logos->logos_execution_zone.sync_to_block(blockId);
|
2026-02-23 18:09:06 +01:00
|
|
|
return err == WALLET_FFI_SUCCESS;
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
QString LEZWalletBackend::transferPublic(QString fromHex, QString toHex, QString amountStr)
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-06-05 19:08:50 +03:00
|
|
|
QList<QString> account_ids = { fromHex, toHex };
|
|
|
|
|
QList<bool> signing_requirements = { true, false };
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
const QString amountHex = amountToLe16Hex(amountStr);
|
2026-02-23 18:09:06 +01:00
|
|
|
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
2026-06-05 19:08:50 +03:00
|
|
|
|
|
|
|
|
uint8_t amount[16];
|
|
|
|
|
if (!hexToU128(amount_le16_hex, &amount)) {
|
|
|
|
|
return QStringLiteral("Error: Invalid amount.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<uint8_t> input_data_raw;
|
|
|
|
|
input_data_raw.reserve(17);
|
|
|
|
|
input_data_raw.append(0);
|
|
|
|
|
for (int i = 0; i < 16; i++) {
|
|
|
|
|
input_data_raw.append(amount[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<uint32_t> input_data = m_logos->logos_execution_zone.serialization_helper(&input_data_raw);
|
|
|
|
|
QList<uint8_t> elf = m_logos->logos_execution_zone.authenticated_transfer_elf();
|
|
|
|
|
|
|
|
|
|
QList<QList<uint8_t>> program_dependencies;
|
|
|
|
|
|
|
|
|
|
return m_logos->logos_execution_zone.send_generic_public_transaction(&account_ids, &signing_requirements, &input_data, &elf, &program_dependencies);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
QString LEZWalletBackend::transferPrivate(QString fromHex, QString toHex, QString amountStr)
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-04-17 00:37:17 +02:00
|
|
|
const QString amountHex = amountToLe16Hex(amountStr);
|
2026-02-23 18:09:06 +01:00
|
|
|
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
|
|
|
|
|
|
|
|
|
QString keysPayload = toHex.trimmed();
|
2026-04-17 00:37:17 +02:00
|
|
|
// If "To" is not JSON (e.g. user pasted account id hex), resolve to keys.
|
2026-02-23 18:09:06 +01:00
|
|
|
if (!keysPayload.startsWith(QLatin1Char('{'))) {
|
2026-04-20 16:41:20 +02:00
|
|
|
qDebug() << "LEZWalletBackend::transferPrivate: resolving keys via get_private_account_keys";
|
2026-02-23 18:09:06 +01:00
|
|
|
const QString resolved = getPrivateAccountKeys(keysPayload);
|
|
|
|
|
if (!resolved.isEmpty())
|
|
|
|
|
keysPayload = resolved;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 17:08:02 +02:00
|
|
|
return m_logos->logos_execution_zone.transfer_private(fromHex, keysPayload, amountHex);
|
2026-02-20 14:20:39 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
QString LEZWalletBackend::transferPrivateOwned(QString fromHex, QString toHex, QString amountStr)
|
2026-03-07 16:17:57 +00:00
|
|
|
{
|
2026-06-05 19:08:50 +03:00
|
|
|
QList<QString> account_ids = { fromHex, toHex };
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
const QString amountHex = amountToLe16Hex(amountStr);
|
2026-03-07 16:17:57 +00:00
|
|
|
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
|
2026-06-05 19:08:50 +03:00
|
|
|
|
|
|
|
|
uint8_t amount[16];
|
|
|
|
|
if (!hexToU128(amount_le16_hex, &amount)) {
|
|
|
|
|
return QStringLiteral("Error: Invalid amount.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<uint8_t> input_data_raw;
|
|
|
|
|
input_data_raw.reserve(17);
|
|
|
|
|
input_data_raw.append(0);
|
|
|
|
|
for (int i = 0; i < 16; i++) {
|
|
|
|
|
input_data_raw.append(amount[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<uint32_t> input_data = m_logos->logos_execution_zone.serialization_helper(&input_data_raw);
|
|
|
|
|
QList<uint8_t> elf = m_logos->logos_execution_zone.authenticated_transfer_elf();
|
|
|
|
|
|
|
|
|
|
QList<QList<uint8_t>> program_dependencies;
|
|
|
|
|
|
|
|
|
|
return m_logos->logos_execution_zone.send_generic_private_transaction(&account_ids, &input_data, &elf, &program_dependencies);
|
2026-03-07 16:17:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
bool LEZWalletBackend::createNew(QString configPath, QString storagePath, QString password)
|
2026-02-20 14:20:39 +01:00
|
|
|
{
|
2026-04-17 00:37:17 +02:00
|
|
|
const QString localPath = toLocalPath(configPath);
|
2026-05-21 17:08:02 +02:00
|
|
|
int err = m_logos->logos_execution_zone.create_new(localPath, storagePath, password);
|
2026-02-20 14:20:39 +01:00
|
|
|
if (err != WALLET_FFI_SUCCESS) return false;
|
|
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
persistConfigPath(localPath);
|
|
|
|
|
persistStoragePath(storagePath);
|
|
|
|
|
setIsWalletOpen(true);
|
2026-02-20 14:20:39 +01:00
|
|
|
refreshAccounts();
|
|
|
|
|
refreshBlockHeights();
|
|
|
|
|
refreshSequencerAddr();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-02-22 02:03:13 +05:30
|
|
|
|
2026-04-17 00:37:17 +02:00
|
|
|
void LEZWalletBackend::copyToClipboard(QString text)
|
2026-02-23 18:09:06 +01:00
|
|
|
{
|
|
|
|
|
if (QGuiApplication::clipboard())
|
|
|
|
|
QGuiApplication::clipboard()->setText(text);
|
|
|
|
|
}
|