#include "LEZWalletBackend.h" #include #include #include #include #include #include #include #include #include #include #include #include "logos_api.h" #include "logos_sdk.h" 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; // Convert a decimal amount string to 32-char hex (16 bytes little-endian) // for transfer_public/transfer_private/transfer_private_owned. 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((value >> (i * 8)) & 0xff); return QByteArray(reinterpret_cast(bytes), 16).toHex(); } // 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; } } static bool hexToU128(const QString& hex, uint8_t (*output)[16]) { if (hex.isEmpty() || hex.length() != 32) return false; for (int i = 0; i < 16; i++) { bool ok = false; uint8_t byte = static_cast(hex.mid(i * 2, 2).toUInt(&ok, 16)); if (!ok) return false; (*output)[i] = byte; } return true; } LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent) : LEZWalletBackendSimpleSource(parent), m_accountModel(new LEZWalletAccountModel(this)), m_filteredAccountModel(new LEZAccountFilterModel(this)), m_logosAPI(logosAPI ? logosAPI : new LogosAPI("lez_wallet_ui", this)), m_logos(new LogosModules(m_logosAPI)) { m_filteredAccountModel->setSourceModel(m_accountModel); // Initialise PROP defaults via the generated setters. setIsWalletOpen(false); setLastSyncedBlock(0); setCurrentBlockHeight(0); // Load persisted config/storage paths. QSettings s(SETTINGS_ORG, SETTINGS_APP); setConfigPath(s.value(CONFIG_PATH_KEY).toString()); setStoragePath(s.value(STORAGE_PATH_KEY).toString()); // 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); } LEZWalletBackend::~LEZWalletBackend() { saveWallet(); delete m_logos; } void LEZWalletBackend::saveWallet() { if (isWalletOpen()) { m_logos->logos_execution_zone.save(); } } void LEZWalletBackend::persistConfigPath(const QString& path) { const QString localPath = toLocalPath(path); setConfigPath(localPath); QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(CONFIG_PATH_KEY, localPath); } void LEZWalletBackend::persistStoragePath(const QString& path) { const QString localPath = toLocalPath(path); setStoragePath(localPath); QSettings(SETTINGS_ORG, SETTINGS_APP).setValue(STORAGE_PATH_KEY, localPath); } void LEZWalletBackend::openIfPathsConfigured() { if (configPath().isEmpty() || storagePath().isEmpty()) 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); refreshAccounts(); refreshBlockHeights(); refreshSequencerAddr(); } } void LEZWalletBackend::refreshAccounts() { QJsonArray arr = m_logos->logos_execution_zone.list_accounts(); m_accountModel->replaceFromJsonArray(arr); refreshBalances(); } void LEZWalletBackend::refreshBalances() { refreshBlockHeights(); syncToBlock(currentBlockHeight()); 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::AddressRole).toString(); const bool isPub = m_accountModel->data(idx, LEZWalletAccountModel::IsPublicRole).toBool(); m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub)); } saveWallet(); } void LEZWalletBackend::fetchAndUpdateBlockHeights() { const int lastVal = m_logos->logos_execution_zone.get_last_synced_block(); const int currentVal = m_logos->logos_execution_zone.get_current_block_height(); if (lastSyncedBlock() != lastVal) setLastSyncedBlock(lastVal); if (currentBlockHeight() != currentVal) setCurrentBlockHeight(currentVal); } void LEZWalletBackend::refreshBlockHeights() { fetchAndUpdateBlockHeights(); if (currentBlockHeight() > 0 && lastSyncedBlock() < currentBlockHeight() && syncToBlock(currentBlockHeight())) { fetchAndUpdateBlockHeights(); } } void LEZWalletBackend::refreshSequencerAddr() { const QString addr = m_logos->logos_execution_zone.get_sequencer_addr(); if (sequencerAddr() != addr) setSequencerAddr(addr); } QString LEZWalletBackend::createAccountPublic() { QString result = m_logos->logos_execution_zone.create_account_public(); if (!result.isEmpty()) refreshAccounts(); return result; } QString LEZWalletBackend::createAccountPrivate() { QString result = m_logos->logos_execution_zone.create_account_private(); if (!result.isEmpty()) refreshAccounts(); return result; } QString LEZWalletBackend::getBalance(QString accountIdHex, bool isPublic) { return m_logos->logos_execution_zone.get_balance(accountIdHex, isPublic); } QString LEZWalletBackend::getPublicAccountKey(QString accountIdHex) { return m_logos->logos_execution_zone.get_public_account_key(accountIdHex); } QString LEZWalletBackend::getPrivateAccountKeys(QString accountIdHex) { return m_logos->logos_execution_zone.get_private_account_keys(accountIdHex); } bool LEZWalletBackend::syncToBlock(quint64 blockId) { int err = m_logos->logos_execution_zone.sync_to_block(blockId); return err == WALLET_FFI_SUCCESS; } QString LEZWalletBackend::transferPublic(QString fromHex, QString toHex, QString amountStr) { QStringList account_ids = { fromHex, toHex }; QStringList signing_requirements = { "true", "false" }; QStringList elf_json = m_logos->logos_execution_zone.authenticated_transfer_elf(); // No dependencies QStringList program_dependencies; // Instruction encoding, the hardest part // RISC0 consumes not bytes, but uint32_t words // construction of which is dependant on Rust serialization // In this exact case instruction is: // // Instruction::Transfer { // amount: u128 // } // // which encodes to [0, word_1, word_2, word_3, word_4] // where 0 - is Transfer enum identifier and words is from le representation of u128. const QString amountHex = amountToLe16Hex(amountStr); if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount."); uint8_t amount[16]; if (!hexToU128(amountHex, &amount)) { return QStringLiteral("Error: Invalid amount."); } QList amount_words; amount_words.reserve(5); amount_words.append(0); for (int i = 0; i < 4; ++i) { uint32_t word = static_cast(amount[i * 4]) | static_cast(amount[i * 4 + 1]) << 8 | static_cast(amount[i * 4 + 2]) << 16 | static_cast(amount[i * 4 + 3]) << 24; amount_words.append(word); } QStringList amount_list; amount_list.reserve(amount_words.size()); for (const uint32_t word : amount_words) { amount_list.append(QString::number(word)); } const QStringList& account_ids_ref = account_ids; const QStringList& signing_requirements_ref = signing_requirements; const QStringList& input_data_ref = amount_list; const QStringList& elf_json_ref = elf_json; const QStringList& program_dependencies_ref = program_dependencies; return m_logos->logos_execution_zone.send_generic_public_transaction(account_ids_ref, signing_requirements_ref, input_data_ref, elf_json_ref, program_dependencies_ref); } QString LEZWalletBackend::transferPrivate(QString fromHex, QString toHex, QString amountStr) { const QString amountHex = amountToLe16Hex(amountStr); 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); if (!resolved.isEmpty()) keysPayload = resolved; } return m_logos->logos_execution_zone.transfer_private(fromHex, keysPayload, amountHex); } 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); } bool LEZWalletBackend::createNew(QString configPath, QString storagePath, QString password) { const QString localPath = toLocalPath(configPath); int err = m_logos->logos_execution_zone.create_new(localPath, storagePath, password); if (err != WALLET_FFI_SUCCESS) return false; persistConfigPath(localPath); persistStoragePath(storagePath); setIsWalletOpen(true); refreshAccounts(); refreshBlockHeights(); refreshSequencerAddr(); return true; } void LEZWalletBackend::copyToClipboard(QString text) { if (QGuiApplication::clipboard()) QGuiApplication::clipboard()->setText(text); }