diff --git a/apps/shared/wallet/qml/TransactionConfirmationDialog.qml b/apps/shared/wallet/qml/TransactionConfirmationDialog.qml index 141a8db..189d348 100644 --- a/apps/shared/wallet/qml/TransactionConfirmationDialog.qml +++ b/apps/shared/wallet/qml/TransactionConfirmationDialog.qml @@ -12,9 +12,11 @@ Popup { property var snapshot: ({}) property Component summary: null property bool confirmationPending: false + property bool confirmEnabled: true signal canceled signal confirmed(var snapshot) + signal summaryEdited(var snapshot) modal: true dim: true @@ -43,6 +45,10 @@ Popup { cancelButton.forceActiveFocus() } + function updateSnapshot(nextSnapshot) { + root.snapshot = root.cloneSnapshot(nextSnapshot) + } + function cancel() { if (root.busy) return @@ -52,7 +58,7 @@ Popup { } function confirm() { - if (root.busy) + if (root.busy || !root.confirmEnabled) return root.confirmationPending = true root.confirmed(root.snapshot) @@ -62,6 +68,16 @@ Popup { } } + Connections { + target: summaryLoader.item + ignoreUnknownSignals: true + + function onSnapshotEdited(snapshot) { + root.updateSnapshot(snapshot) + root.summaryEdited(root.snapshot) + } + } + onBusyChanged: { if (!root.busy && root.confirmationPending) { root.confirmationPending = false @@ -145,7 +161,7 @@ Popup { Layout.fillWidth: true implicitHeight: 44 text: root.busy ? qsTr("Submitting...") : root.confirmText - enabled: !root.busy + enabled: !root.busy && root.confirmEnabled Accessible.name: text onClicked: root.confirm() diff --git a/apps/shared/wallet/qml/WalletControl.qml b/apps/shared/wallet/qml/WalletControl.qml index 28d32a1..687de84 100644 --- a/apps/shared/wallet/qml/WalletControl.qml +++ b/apps/shared/wallet/qml/WalletControl.qml @@ -16,6 +16,11 @@ Item { property bool busy: false readonly property bool connected: root.wallet !== null && root.wallet.isWalletOpen + readonly property string syncStatus: root.wallet + ? String(root.wallet.walletSyncStatus || "closed") + : "closed" + readonly property bool syncing: root.syncStatus === "opening" + || root.syncStatus === "syncing" readonly property bool compactLayout: root.compact || root.viewportWidth < 680 readonly property string selectedAddress: root.accountAt(root.selectedIndex, "address") readonly property string selectedName: root.accountAt(root.selectedIndex, "name") @@ -129,10 +134,13 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter visible: !root.connected - enabled: root.wallet !== null && !root.busy + enabled: root.wallet !== null && !root.busy && !root.syncing implicitHeight: 40 implicitWidth: root.compactLayout ? 40 : 108 - text: root.compactLayout ? "" : root.busy ? qsTr("Connecting...") : qsTr("Connect") + text: root.compactLayout ? "" + : root.syncStatus === "opening" ? qsTr("Opening...") + : root.syncStatus === "syncing" ? qsTr("Syncing...") + : root.busy ? qsTr("Connecting...") : qsTr("Connect") display: root.compactLayout ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon icon.source: Qt.resolvedUrl("icons/account.svg") icon.color: "#ffffff" @@ -198,7 +206,7 @@ Item { Layout.preferredWidth: 8 Layout.preferredHeight: 8 radius: 4 - color: "#22c55e" + color: root.syncing ? "#f59e0b" : "#22c55e" } Label { diff --git a/apps/shared/wallet/src/LogosWalletProvider.cpp b/apps/shared/wallet/src/LogosWalletProvider.cpp index 3e56952..62cdbf4 100644 --- a/apps/shared/wallet/src/LogosWalletProvider.cpp +++ b/apps/shared/wallet/src/LogosWalletProvider.cpp @@ -1,10 +1,13 @@ #include "LogosWalletProvider.h" +#include + #include #include #include #include #include +#include #include #include @@ -73,6 +76,39 @@ WalletCreation failedCreation(WalletFailure failure) creation.snapshot.failure = failure; return creation; } + +WalletAccountRead parsePublicAccount(const QString& accountId, const QString& payload) +{ + WalletAccountRead read; + read.accountId = accountId; + if (!isHex(accountId, 64)) + return read; + + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(payload.toUtf8(), &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) + return read; + + const QJsonObject account = document.object(); + const QString owner = account.value(QStringLiteral("program_owner")).toString(); + const QString balance = account.value(QStringLiteral("balance")).toString(); + const QString nonce = account.value(QStringLiteral("nonce")).toString(); + const QString data = account.value(QStringLiteral("data")).toString(); + if (!isHex(owner, 64) + || !isHex(balance, 32) + || !isHex(nonce, 32) + || data.size() % 2 != 0 + || !isHex(data, data.size())) { + return read; + } + + read.status = QStringLiteral("ok"); + read.programOwner = owner; + read.balanceHex = balance; + read.nonceHex = nonce; + read.dataHex = data; + return read; +} } struct LogosWalletProvider::Impl { @@ -130,6 +166,76 @@ WalletSession LogosWalletProvider::connect(const WalletPaths& paths) return session; } +void LogosWalletProvider::connectAsync(const WalletPaths& paths, SessionCallback callback) +{ + clearSnapshot(); + const quint64 generation = ++m_generation; + if (!m_impl->logos) { + QTimer::singleShot(0, [callback = std::move(callback)]() mutable { + callback(failedSession(WalletFailure::WalletUnavailable)); + }); + return; + } + + auto finishOpen = [this, generation, callback = std::move(callback)]( + bool adopted, WalletFailure failure) mutable { + if (generation != m_generation) + return; + if (failure != WalletFailure::None) { + callback(failedSession(failure)); + return; + } + m_connected = true; + loadSnapshotAsync(generation, + [this, generation, adopted, callback = std::move(callback)]( + WalletSnapshot snapshot) mutable { + if (generation != m_generation) + return; + WalletSession session; + session.adopted = adopted; + session.failure = snapshot.failure; + session.snapshot = std::move(snapshot); + callback(std::move(session)); + }); + }; + + auto openStored = [this, generation, paths, finishOpen]() mutable { + if (generation != m_generation) + return; + if (!QFileInfo::exists(paths.storage)) { + finishOpen(false, WalletFailure::WalletMissing); + return; + } + m_impl->logos->logos_execution_zone.openAsync( + paths.config, paths.storage, + [this, generation, finishOpen](int result) mutable { + if (generation != m_generation) + return; + finishOpen(false, result == WALLET_FFI_SUCCESS + ? WalletFailure::None : WalletFailure::OpenFailed); + }); + }; + + m_impl->logos->logos_execution_zone.get_sequencer_addrAsync( + [this, generation, finishOpen, openStored](QString address) mutable { + if (generation != m_generation) + return; + if (!address.isEmpty()) { + finishOpen(true, WalletFailure::None); + return; + } + m_impl->logos->logos_execution_zone.list_accountsAsync( + [this, generation, finishOpen, openStored](QVariantList accounts) mutable { + if (generation != m_generation) + return; + if (!accounts.isEmpty()) + finishOpen(true, WalletFailure::None); + else + openStored(); + }); + }); +} + WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths, const QString& password) { @@ -180,6 +286,26 @@ WalletSnapshot LogosWalletProvider::snapshot(bool forceRefresh) return result; } +void LogosWalletProvider::snapshotAsync(bool forceRefresh, SnapshotCallback callback) +{ + if (m_snapshotReady && !forceRefresh) { + const WalletSnapshot snapshot = m_snapshot; + QTimer::singleShot(0, [callback = std::move(callback), snapshot]() mutable { + callback(snapshot); + }); + return; + } + if (!m_connected) { + WalletSnapshot snapshot; + snapshot.failure = WalletFailure::WalletUnavailable; + QTimer::singleShot(0, [callback = std::move(callback), snapshot]() mutable { + callback(snapshot); + }); + return; + } + loadSnapshotAsync(++m_generation, std::move(callback)); +} + void LogosWalletProvider::clearSnapshot() { m_snapshot = {}; @@ -208,45 +334,47 @@ WalletAccountCreation LogosWalletProvider::createAccount(bool isPublic) if (isPublic) creation.publicAccount = readPublicAccount(creation.accountId); - - clearSnapshot(); - creation.snapshot = snapshot(true); + if (m_snapshotReady) { + WalletAccount account; + account.address = creation.accountId; + account.isPublic = isPublic; + if (isPublic && creation.publicAccount.ok()) { + account.balance = littleEndianU128ToDecimal(creation.publicAccount.balanceHex); + auto read = std::find_if( + m_snapshot.publicAccountReads.begin(), + m_snapshot.publicAccountReads.end(), + [&creation](const WalletAccountRead& existing) { + return existing.accountId == creation.accountId; + }); + if (read == m_snapshot.publicAccountReads.end()) + m_snapshot.publicAccountReads.append(creation.publicAccount); + else + *read = creation.publicAccount; + } else { + account.balance = m_impl->logos->logos_execution_zone.get_balance( + creation.accountId, isPublic); + } + auto existing = std::find_if( + m_snapshot.accounts.begin(), m_snapshot.accounts.end(), + [&creation](const WalletAccount& candidate) { + return candidate.address == creation.accountId; + }); + if (existing == m_snapshot.accounts.end()) + m_snapshot.accounts.append(account); + else + *existing = account; + creation.snapshot = m_snapshot; + } return creation; } WalletAccountRead LogosWalletProvider::readPublicAccount(const QString& accountId) const { - WalletAccountRead read; - read.accountId = accountId; if (!m_impl->logos || !isHex(accountId, 64)) - return read; - - QJsonParseError parseError; - const QJsonDocument document = QJsonDocument::fromJson( - m_impl->logos->logos_execution_zone.get_account_public(accountId).toUtf8(), - &parseError); - if (parseError.error != QJsonParseError::NoError || !document.isObject()) - return read; - - const QJsonObject account = document.object(); - const QString owner = account.value(QStringLiteral("program_owner")).toString(); - const QString balance = account.value(QStringLiteral("balance")).toString(); - const QString nonce = account.value(QStringLiteral("nonce")).toString(); - const QString data = account.value(QStringLiteral("data")).toString(); - if (!isHex(owner, 64) - || !isHex(balance, 32) - || !isHex(nonce, 32) - || data.size() % 2 != 0 - || !isHex(data, data.size())) { - return read; - } - - read.status = QStringLiteral("ok"); - read.programOwner = owner; - read.balanceHex = balance; - read.nonceHex = nonce; - read.dataHex = data; - return read; + return WalletAccountRead { accountId }; + return parsePublicAccount( + accountId, + m_impl->logos->logos_execution_zone.get_account_public(accountId)); } WalletSubmission LogosWalletProvider::submitPublicTransaction( @@ -314,6 +442,7 @@ WalletSubmission LogosWalletProvider::submitPublicTransaction( void LogosWalletProvider::disconnect() { + ++m_generation; if (m_connected) save(); clearSnapshot(); @@ -375,6 +504,161 @@ WalletSnapshot LogosWalletProvider::loadSnapshot() return result; } +void LogosWalletProvider::loadSnapshotAsync(quint64 generation, SnapshotCallback callback) +{ + if (!m_impl->logos || generation != m_generation) + return; + + m_impl->logos->logos_execution_zone.get_current_block_heightAsync( + [this, generation, callback = std::move(callback)](int currentHeight) mutable { + if (generation != m_generation) + return; + + auto afterSync = [this, generation, currentHeight, + callback = std::move(callback)](int syncResult) mutable { + if (generation != m_generation) + return; + if (syncResult != WALLET_FFI_SUCCESS) { + WalletSnapshot failed; + failed.failure = WalletFailure::ReadFailed; + callback(std::move(failed)); + return; + } + + m_impl->logos->logos_execution_zone.get_last_synced_blockAsync( + [this, generation, currentHeight, + callback = std::move(callback)](int lastSynced) mutable { + if (generation != m_generation) + return; + m_impl->logos->logos_execution_zone.get_sequencer_addrAsync( + [this, generation, currentHeight, lastSynced, + callback = std::move(callback)](QString address) mutable { + if (generation != m_generation) + return; + m_impl->logos->logos_execution_zone.list_accountsAsync( + [this, generation, currentHeight, lastSynced, + address = std::move(address), + callback = std::move(callback)]( + QVariantList entries) mutable { + if (generation != m_generation) + return; + + struct SnapshotState { + WalletSnapshot snapshot; + QVector publicReads; + QVector publicFlags; + qsizetype remaining = 0; + SnapshotCallback callback; + }; + auto state = std::make_shared(); + state->snapshot.currentBlockHeight = static_cast( + qMax(0, currentHeight)); + state->snapshot.lastSyncedBlock = static_cast( + qMax(0, lastSynced)); + state->snapshot.sequencerAddress = std::move(address); + state->snapshot.accounts.resize(entries.size()); + state->publicReads.resize(entries.size()); + state->publicFlags.resize(entries.size()); + state->remaining = entries.size(); + state->callback = std::move(callback); + + for (qsizetype index = 0; index < entries.size(); ++index) { + const QVariantMap entry = entries.at(index).toMap(); + const QString accountId = entry + .value(QStringLiteral("account_id")).toString(); + if (entry.isEmpty() || !isHex(accountId, 64)) { + state->snapshot.failure = WalletFailure::ReadFailed; + state->callback(std::move(state->snapshot)); + return; + } + state->snapshot.accounts[index] = WalletAccount { + accountId, + {}, + entry.value(QStringLiteral("is_public"), true).toBool(), + }; + state->publicFlags[index] = + state->snapshot.accounts.at(index).isPublic; + } + + auto finishOne = std::make_shared>(); + *finishOne = [this, generation, state, finishOne]() mutable { + if (generation != m_generation || --state->remaining > 0) + return; + for (qsizetype index = 0; + index < state->publicReads.size(); ++index) { + if (state->publicFlags.at(index)) + state->snapshot.publicAccountReads.append( + state->publicReads.at(index)); + } + m_impl->logos->logos_execution_zone.saveAsync( + [this, generation, state](int result) mutable { + if (generation != m_generation) + return; + if (result != WALLET_FFI_SUCCESS) + state->snapshot.failure = WalletFailure::SaveFailed; + if (state->snapshot.ok()) { + m_snapshot = state->snapshot; + m_snapshotReady = true; + } + state->callback(std::move(state->snapshot)); + }); + }; + + if (entries.isEmpty()) { + state->remaining = 1; + (*finishOne)(); + return; + } + + for (qsizetype index = 0; index < entries.size(); ++index) { + const WalletAccount account = state->snapshot.accounts.at(index); + if (!account.isPublic) { + m_impl->logos->logos_execution_zone.get_balanceAsync( + account.address, false, + [state, finishOne, index](QString balance) { + state->snapshot.accounts[index].balance = + std::move(balance); + (*finishOne)(); + }); + continue; + } + + m_impl->logos->logos_execution_zone.get_account_publicAsync( + account.address, + [this, state, finishOne, index, + accountId = account.address](QString payload) { + const WalletAccountRead read = + parsePublicAccount(accountId, payload); + state->publicReads[index] = read; + if (read.ok()) { + state->snapshot.accounts[index].balance = + littleEndianU128ToDecimal(read.balanceHex); + (*finishOne)(); + return; + } + m_impl->logos->logos_execution_zone.get_balanceAsync( + accountId, true, + [state, finishOne, index](QString balance) { + state->snapshot.accounts[index].balance = + std::move(balance); + (*finishOne)(); + }); + }); + } + }); + }); + }); + }; + + if (currentHeight > 0) { + m_impl->logos->logos_execution_zone.sync_to_blockAsync( + currentHeight, std::move(afterSync)); + } else { + afterSync(WALLET_FFI_SUCCESS); + } + }); +} + bool LogosWalletProvider::save() const { return m_impl->logos diff --git a/apps/shared/wallet/src/LogosWalletProvider.h b/apps/shared/wallet/src/LogosWalletProvider.h index d046290..b037240 100644 --- a/apps/shared/wallet/src/LogosWalletProvider.h +++ b/apps/shared/wallet/src/LogosWalletProvider.h @@ -14,9 +14,11 @@ public: ~LogosWalletProvider() override; WalletSession connect(const WalletPaths& paths) override; + void connectAsync(const WalletPaths& paths, SessionCallback callback) override; WalletCreation createWallet(const WalletPaths& paths, const QString& password) override; WalletSnapshot snapshot(bool forceRefresh = false) override; + void snapshotAsync(bool forceRefresh, SnapshotCallback callback) override; void clearSnapshot() override; WalletAccountCreation createAccount(bool isPublic) override; WalletAccountRead readPublicAccount(const QString& accountId) const override; @@ -27,6 +29,7 @@ public: private: bool sharedWalletIsOpen() const; WalletSnapshot loadSnapshot(); + void loadSnapshotAsync(quint64 generation, SnapshotCallback callback); bool save() const; struct Impl; @@ -34,4 +37,5 @@ private: WalletSnapshot m_snapshot; bool m_snapshotReady = false; bool m_connected = false; + quint64 m_generation = 0; }; diff --git a/apps/shared/wallet/src/WalletController.cpp b/apps/shared/wallet/src/WalletController.cpp index 5cffdfd..bdf2439 100644 --- a/apps/shared/wallet/src/WalletController.cpp +++ b/apps/shared/wallet/src/WalletController.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -25,6 +28,17 @@ QString toLocalPath(const QString& path) return QUrl::fromUserInput(path).toLocalFile(); return path; } + +QString configuredSequencer(const QString& path) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) + return {}; + const QJsonDocument document = QJsonDocument::fromJson(file.readAll()); + if (!document.isObject()) + return {}; + return document.object().value(QStringLiteral("sequencer_addr")).toString(); +} } WalletController::WalletController(WalletProvider& wallet, @@ -38,7 +52,10 @@ WalletController::WalletController(WalletProvider& wallet, m_reachabilityTimer(new QTimer(this)) { m_state.walletHome = defaultWalletHome(); + m_state.configPath = defaultConfigPath(); + m_state.storagePath = defaultStoragePath(); m_state.walletExists = QFileInfo::exists(defaultStoragePath()); + m_state.sequencerAddress = configuredSequencer(defaultConfigPath()); m_reachabilityTimer->setInterval(10000); connect(m_reachabilityTimer, &QTimer::timeout, @@ -83,20 +100,60 @@ void WalletController::openOnStartup() const QString config = defaultConfigPath(); const QString storage = defaultStoragePath(); - const WalletSession session = m_wallet.connect({ config, storage }); - if (session.failure == WalletFailure::WalletMissing) - return; - if (!session.ok()) { - qWarning() << "WalletController: wallet connection failed" - << walletFailureCode(session.failure); - return; + beginOpen(config, storage); +} + +bool WalletController::beginOpen(const QString& config, const QString& storage) +{ + if (m_state.syncStatus == QStringLiteral("opening") + || m_state.syncStatus == QStringLiteral("syncing")) { + return false; } + const quint64 generation = ++m_operationGeneration; m_state.configPath = config; m_state.storagePath = storage; - m_state.walletExists = QFileInfo::exists(storage) || session.adopted; - m_state.isWalletOpen = true; - applySnapshot(session.snapshot); + m_state.syncStatus = QStringLiteral("opening"); + m_state.syncError.clear(); + const QString endpoint = configuredSequencer(config); + if (!endpoint.isEmpty()) + m_state.sequencerAddress = endpoint; + emit stateChanged(); + + QTimer::singleShot(0, this, [this, generation]() { + if (generation == m_operationGeneration + && m_state.syncStatus == QStringLiteral("opening")) { + m_state.syncStatus = QStringLiteral("syncing"); + emit stateChanged(); + } + }); + m_wallet.connectAsync({ config, storage }, + [this, generation, config, storage](WalletSession session) { + if (generation != m_operationGeneration) + return; + if (session.failure == WalletFailure::WalletMissing) { + m_state.syncStatus = QStringLiteral("closed"); + m_state.walletExists = false; + emit stateChanged(); + return; + } + if (!session.ok()) { + qWarning() << "WalletController: wallet connection failed" + << walletFailureCode(session.failure); + m_state.syncStatus = QStringLiteral("error"); + m_state.syncError = walletFailureCode(session.failure); + emit stateChanged(); + return; + } + + m_state.configPath = config; + m_state.storagePath = storage; + m_state.walletExists = QFileInfo::exists(storage) || session.adopted; + m_state.isWalletOpen = true; + m_state.syncStatus = QStringLiteral("ready"); + applySnapshot(session.snapshot); + }); + return true; } QString WalletController::createDefaultWallet(const QString& password) @@ -130,6 +187,7 @@ QString WalletController::createWallet(const QString& configPath, } m_state.isWalletOpen = true; + m_state.syncStatus = QStringLiteral("ready"); applySnapshot(creation.snapshot); return creation.mnemonic; } @@ -140,26 +198,17 @@ bool WalletController::open() ? defaultConfigPath() : m_state.configPath; const QString storage = m_state.storagePath.isEmpty() ? defaultStoragePath() : m_state.storagePath; - const WalletSession session = m_wallet.connect({ config, storage }); - if (!session.ok()) { - qWarning() << "WalletController: wallet open failed" - << walletFailureCode(session.failure); - return false; - } - - m_state.configPath = config; - m_state.storagePath = storage; - m_state.walletExists = true; - m_state.isWalletOpen = true; QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false); - applySnapshot(session.snapshot); - return true; + return beginOpen(config, storage); } void WalletController::disconnect() { + ++m_operationGeneration; m_wallet.disconnect(); m_state.isWalletOpen = false; + m_state.syncStatus = QStringLiteral("closed"); + m_state.syncError.clear(); m_accountModel->replaceAccounts({}); QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, true); emit stateChanged(); @@ -184,13 +233,26 @@ QString WalletController::createAccount(bool isPublic) void WalletController::refresh() { - const WalletSnapshot next = m_wallet.snapshot(true); - if (next.ok()) { - applySnapshot(next); - } else { - qWarning() << "WalletController: wallet refresh failed" - << walletFailureCode(next.failure); - } + if (!m_state.isWalletOpen || m_state.syncStatus == QStringLiteral("syncing")) + return; + const quint64 generation = ++m_operationGeneration; + m_state.syncStatus = QStringLiteral("syncing"); + m_state.syncError.clear(); + emit stateChanged(); + m_wallet.snapshotAsync(true, [this, generation](WalletSnapshot next) { + if (generation != m_operationGeneration) + return; + if (next.ok()) { + m_state.syncStatus = QStringLiteral("ready"); + applySnapshot(next); + } else { + qWarning() << "WalletController: wallet refresh failed" + << walletFailureCode(next.failure); + m_state.syncStatus = QStringLiteral("error"); + m_state.syncError = walletFailureCode(next.failure); + emit stateChanged(); + } + }); } QString WalletController::balance(const QString& accountId, bool isPublic) @@ -208,7 +270,8 @@ void WalletController::applySnapshot(const WalletSnapshot& snapshot) m_accountModel->replaceAccounts(snapshot.accounts); m_state.lastSyncedBlock = static_cast(snapshot.lastSyncedBlock); m_state.currentBlockHeight = static_cast(snapshot.currentBlockHeight); - m_state.sequencerAddress = snapshot.sequencerAddress; + if (!snapshot.sequencerAddress.isEmpty()) + m_state.sequencerAddress = snapshot.sequencerAddress; emit stateChanged(); checkReachability(); } diff --git a/apps/shared/wallet/src/WalletController.h b/apps/shared/wallet/src/WalletController.h index c27f9ce..70caa79 100644 --- a/apps/shared/wallet/src/WalletController.h +++ b/apps/shared/wallet/src/WalletController.h @@ -19,6 +19,13 @@ struct WalletUiState { int currentBlockHeight = 0; QString sequencerAddress; bool sequencerReachable = true; + QString syncStatus = QStringLiteral("closed"); + QString syncError; + + bool canSubmit() const + { + return isWalletOpen && syncStatus == QStringLiteral("ready"); + } }; class WalletController final : public QObject { @@ -54,6 +61,7 @@ private: QString defaultStoragePath() const; void openOnStartup(); + bool beginOpen(const QString& config, const QString& storage); void applySnapshot(const WalletSnapshot& snapshot); void checkReachability(); @@ -64,4 +72,5 @@ private: QNetworkAccessManager* m_network; QTimer* m_reachabilityTimer; bool m_started = false; + quint64 m_operationGeneration = 0; }; diff --git a/apps/shared/wallet/src/WalletProvider.h b/apps/shared/wallet/src/WalletProvider.h index a7814ca..198f598 100644 --- a/apps/shared/wallet/src/WalletProvider.h +++ b/apps/shared/wallet/src/WalletProvider.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -92,12 +93,17 @@ struct WalletSubmission { class WalletProvider { public: + using SessionCallback = std::function; + using SnapshotCallback = std::function; + virtual ~WalletProvider() = default; virtual WalletSession connect(const WalletPaths& paths) = 0; + virtual void connectAsync(const WalletPaths& paths, SessionCallback callback) = 0; virtual WalletCreation createWallet(const WalletPaths& paths, const QString& password) = 0; virtual WalletSnapshot snapshot(bool forceRefresh = false) = 0; + virtual void snapshotAsync(bool forceRefresh, SnapshotCallback callback) = 0; virtual void clearSnapshot() = 0; virtual WalletAccountCreation createAccount(bool isPublic) = 0; virtual WalletAccountRead readPublicAccount(const QString& accountId) const = 0; diff --git a/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp b/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp index 9701d6b..32ef3c0 100644 --- a/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp +++ b/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp @@ -39,6 +39,7 @@ QVariantMap accountEntry(const QString& id, bool isPublic) { QStringLiteral("is_public"), isPublic }, }; } + } class LogosWalletProviderTest : public QObject { @@ -52,12 +53,13 @@ private slots: void fallsBackToBalanceWhenPublicReadFails(); void createsAndPersistsAccounts(); void preservesCreatedAccountWhenPublicReadFails(); - void preservesCreatedAccountWhenSnapshotRefreshFails(); + void createdAccountDoesNotRescanWallet(); void dispatchesExactGenericTransaction(); void rejectsInvalidSubmissionResponses(); void exposesStableAccountModelRoles(); void fakeProviderImplementsConsumerContract(); void controllerOwnsUiWalletFlow(); + void controllerOpenDoesNotWaitForWalletSync(); void controllerStopsReachabilityChecksAfterDisconnect(); }; @@ -257,7 +259,7 @@ void LogosWalletProviderTest::preservesCreatedAccountWhenPublicReadFails() QCOMPARE(creation.snapshot.accounts.at(0).balance, QStringLiteral("7")); } -void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails() +void LogosWalletProviderTest::createdAccountDoesNotRescanWallet() { LogosModules modules; modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer"); @@ -268,11 +270,13 @@ void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails() modules.logos_execution_zone.currentBlockHeight = 1; modules.logos_execution_zone.syncResult = 1; + const int syncCalls = modules.logos_execution_zone.syncCalls; const WalletAccountCreation creation = provider.createAccount(true); QVERIFY(creation.ok()); QCOMPARE(creation.accountId, ACCOUNT_A); - QCOMPARE(creation.snapshot.failure, WalletFailure::ReadFailed); + QVERIFY(creation.snapshot.ok()); + QCOMPARE(modules.logos_execution_zone.syncCalls, syncCalls); } void LogosWalletProviderTest::dispatchesExactGenericTransaction() @@ -419,6 +423,33 @@ void LogosWalletProviderTest::controllerOwnsUiWalletFlow() settings.clear(); } +void LogosWalletProviderTest::controllerOpenDoesNotWaitForWalletSync() +{ + const QString settingsApplication = QStringLiteral("WalletAsyncOpenTest"); + QSettings settings(QStringLiteral("Logos"), settingsApplication); + settings.clear(); + + FakeWalletProvider provider; + provider.deferAsync = true; + provider.connectResult.snapshot.accounts = { + { ACCOUNT_A, QStringLiteral("5"), true }, + }; + WalletController controller(provider, settingsApplication); + + QVERIFY(controller.open()); + QCOMPARE(provider.connectCalls, 1); + QVERIFY(!controller.state().isWalletOpen); + QCOMPARE(controller.state().syncStatus, QStringLiteral("opening")); + QCOMPARE(controller.accountModel()->count(), 0); + + provider.finishConnect(); + QVERIFY(controller.state().isWalletOpen); + QVERIFY(controller.state().canSubmit()); + QCOMPARE(controller.state().syncStatus, QStringLiteral("ready")); + QCOMPARE(controller.accountModel()->count(), 1); + settings.clear(); +} + void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect() { const QString settingsApplication = QStringLiteral("WalletReachabilityTest"); diff --git a/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h b/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h index c1927f4..13ca5d7 100644 --- a/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h +++ b/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h @@ -6,6 +6,8 @@ #include #include +#include + class LogosAPI; class FakeExecutionZone { @@ -48,6 +50,13 @@ public: return openResult; } + void openAsync(const QString& config, + const QString& storage, + std::function callback) + { + callback(open(config, storage)); + } + QString create_new(const QString& config, const QString& storage, const QString& password) @@ -64,36 +73,69 @@ public: return saveResult; } + void saveAsync(std::function callback) { callback(save()); } + QString create_account_public() { return publicAccountId; } QString create_account_private() { return privateAccountId; } int get_last_synced_block() const { return lastSyncedBlock; } int get_current_block_height() const { return currentBlockHeight; } + void get_last_synced_blockAsync(std::function callback) + { + callback(get_last_synced_block()); + } + void get_current_block_heightAsync(std::function callback) + { + callback(get_current_block_height()); + } int sync_to_block(quint64) { ++syncCalls; return syncResult; } + void sync_to_blockAsync(int blockId, std::function callback) + { + callback(sync_to_block(static_cast(blockId))); + } QString get_sequencer_addr() const { return sequencerAddress; } + void get_sequencer_addrAsync(std::function callback) + { + callback(get_sequencer_addr()); + } QVariantList list_accounts() { ++listCalls; return accounts; } + void list_accountsAsync(std::function callback) + { + callback(list_accounts()); + } QString get_account_public(const QString& accountId) { ++publicReadCalls; return publicAccounts.value(accountId); } + void get_account_publicAsync(const QString& accountId, + std::function callback) + { + callback(get_account_public(accountId)); + } QString get_balance(const QString& accountId, bool) const { return balances.value(accountId); } + void get_balanceAsync(const QString& accountId, + bool isPublic, + std::function callback) + { + callback(get_balance(accountId, isPublic)); + } QString send_generic_public_transaction( const QStringList& accountIds, diff --git a/apps/shared/wallet/tests/support/FakeWalletProvider.h b/apps/shared/wallet/tests/support/FakeWalletProvider.h index d5dea0a..d525f3e 100644 --- a/apps/shared/wallet/tests/support/FakeWalletProvider.h +++ b/apps/shared/wallet/tests/support/FakeWalletProvider.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "WalletProvider.h" class FakeWalletProvider final : public WalletProvider { @@ -23,6 +25,9 @@ public: bool lastAccountWasPublic = false; WalletPaths lastPaths; WalletTransaction lastTransaction; + bool deferAsync = false; + SessionCallback pendingConnectCallback; + SnapshotCallback pendingSnapshotCallback; WalletSession connect(const WalletPaths& paths) override { @@ -31,6 +36,16 @@ public: return connectResult; } + void connectAsync(const WalletPaths& paths, SessionCallback callback) override + { + ++connectCalls; + lastPaths = paths; + if (deferAsync) + pendingConnectCallback = std::move(callback); + else + callback(connectResult); + } + WalletCreation createWallet(const WalletPaths& paths, const QString&) override { @@ -46,6 +61,16 @@ public: return snapshotResult; } + void snapshotAsync(bool forceRefresh, SnapshotCallback callback) override + { + ++snapshotCalls; + lastForceRefresh = forceRefresh; + if (deferAsync) + pendingSnapshotCallback = std::move(callback); + else + callback(snapshotResult); + } + void clearSnapshot() override { ++clearCalls; } WalletAccountCreation createAccount(bool isPublic) override @@ -72,4 +97,18 @@ public: } void disconnect() override { ++disconnectCalls; } + + void finishConnect() + { + SessionCallback callback = std::move(pendingConnectCallback); + if (callback) + callback(connectResult); + } + + void finishSnapshot() + { + SnapshotCallback callback = std::move(pendingSnapshotCallback); + if (callback) + callback(snapshotResult); + } };