diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index e6207cf..13223de 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -191,6 +191,7 @@ void AmmUiBackend::syncWalletState() { const WalletUiState& state = m_walletController->state(); const bool walletWasOpen = isWalletOpen(); + const bool walletCouldSubmit = walletCanSubmit(); const bool wasReachable = sequencerReachable(); const QString previousAddress = sequencerAddr(); @@ -212,6 +213,8 @@ void AmmUiBackend::syncWalletState() m_sequencer->configure(state.configPath); const bool addressChanged = previousAddress != state.sequencerAddress; + if ((walletCouldSubmit && !state.canSubmit()) || addressChanged) + m_newPosition->cancelSubmit(); if (addressChanged) { m_identityRetryTimer->stop(); m_pendingTransactions.clear(); diff --git a/apps/amm/src/NewPositionRuntime.cpp b/apps/amm/src/NewPositionRuntime.cpp index 1e18f61..07f039b 100644 --- a/apps/amm/src/NewPositionRuntime.cpp +++ b/apps/amm/src/NewPositionRuntime.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -171,6 +172,7 @@ NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet, void NewPositionRuntime::clearWalletAccounts() { ++m_walletGeneration; + cancelSubmit(); m_walletPublicAccountIds.clear(); m_wallet->clearSnapshot(); } @@ -182,11 +184,44 @@ void NewPositionRuntime::setWalletAccounts(const QVector& account if (account.isPublic) publicAccountIds.append(account.address); } - if (publicAccountIds != m_walletPublicAccountIds) + if (publicAccountIds != m_walletPublicAccountIds) { ++m_walletGeneration; + cancelSubmit(); + } m_walletPublicAccountIds = std::move(publicAccountIds); } +void NewPositionRuntime::cancelSubmit() +{ + if (!m_submitInFlight) + return; + ++m_submitGeneration; + m_submitInFlight = false; + ResultCallback callback = std::move(m_submitCallback); + m_submitCallback = {}; + if (callback) + callback(publicError(QStringLiteral("wallet_unavailable")).toVariantMap()); +} + +void NewPositionRuntime::finishSubmit(quint64 submitGeneration, QVariantMap result) +{ + if (!m_submitInFlight || submitGeneration != m_submitGeneration) + return; + m_submitInFlight = false; + ResultCallback callback = std::move(m_submitCallback); + m_submitCallback = {}; + if (callback) + callback(std::move(result)); +} + +bool NewPositionRuntime::submitIsCurrent(quint64 submitGeneration, + quint64 walletGeneration) const +{ + return m_submitInFlight + && submitGeneration == m_submitGeneration + && walletGeneration == m_walletGeneration; +} + QJsonArray NewPositionRuntime::walletAccountReads(bool walletOpen, bool refresh) const { if (!walletOpen) @@ -280,14 +315,20 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request, } const QString configId = configResult.value.value( QStringLiteral("configId")).toString(); + QPointer guard(this); m_sequencer->readAccounts({ configId }, refreshPublicData, - [this, request, network, walletOpen, refreshPublicData, + [guard, request, network, walletOpen, refreshPublicData, callback = std::move(callback)](QVector configReads) mutable { + if (!guard) + return; const QJsonObject config = accountReadJson(configReads.value(0)); - m_sequencer->readAccounts(m_walletPublicAccountIds, refreshPublicData, - [this, request, network, walletOpen, refreshPublicData, config, + guard->m_sequencer->readAccounts( + guard->m_walletPublicAccountIds, refreshPublicData, + [guard, request, network, walletOpen, refreshPublicData, config, callback = std::move(callback)]( QVector walletReads) mutable { + if (!guard) + return; const QJsonObject hints = QJsonObject::fromVariantMap(request); QJsonArray configured; for (const QString& id : network.tokenIds) @@ -297,7 +338,7 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request, const QJsonArray resolved = variantStringArray( hints.value(QStringLiteral("resolvedTokenIds")).toVariant()); const QJsonArray walletAccounts = accountReadsJson(walletReads); - const AmmClientResult tokenResult = m_client->tokenIds(QJsonObject { + const AmmClientResult tokenResult = guard->m_client->tokenIds(QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId }, { QStringLiteral("config"), config }, { QStringLiteral("walletAccounts"), walletAccounts }, @@ -322,12 +363,14 @@ void NewPositionRuntime::contextAsync(const QVariantMap& request, .value(QStringLiteral("tokenIds")).toArray()) { definitionIds.append(id.toString()); } - m_sequencer->readAccounts(definitionIds, refreshPublicData, - [this, network, walletOpen, config, walletAccounts, + guard->m_sequencer->readAccounts(definitionIds, refreshPublicData, + [guard, network, walletOpen, config, walletAccounts, configured, recent, resolved, callback = std::move(callback)]( QVector definitions) mutable { - const AmmClientResult result = m_client->context(QJsonObject { + if (!guard) + return; + const AmmClientResult result = guard->m_client->context(QJsonObject { { QStringLiteral("networkId"), network.id }, { QStringLiteral("networkFingerprint"), network.fingerprint }, { QStringLiteral("ammProgramId"), network.ammProgramId }, @@ -354,7 +397,7 @@ void NewPositionRuntime::buildQuoteInputAsync( const ActiveNetworkSnapshot& network, bool walletOpen, bool forceRefresh, - std::function callback) const + std::function callback) { if (network.status != QStringLiteral("ready")) { callback({}, publicError(network.status)); @@ -372,12 +415,15 @@ void NewPositionRuntime::buildQuoteInputAsync( } const QString configId = configResult.value.value( QStringLiteral("configId")).toString(); + QPointer guard(this); m_sequencer->readAccounts({ configId }, forceRefresh, - [this, request, network, walletOpen, forceRefresh, + [guard, request, network, walletOpen, forceRefresh, callback = std::move(callback)](QVector configReads) mutable { + if (!guard) + return; const QJsonObject config = accountReadJson(configReads.value(0)); const QJsonObject requestObject = QJsonObject::fromVariantMap(request); - const AmmClientResult pairResult = m_client->pairIds(QJsonObject { + const AmmClientResult pairResult = guard->m_client->pairIds(QJsonObject { { QStringLiteral("ammProgramId"), network.ammProgramId }, { QStringLiteral("config"), config }, { QStringLiteral("tokenAId"), @@ -407,15 +453,20 @@ void NewPositionRuntime::buildQuoteInputAsync( pair.value(QStringLiteral("currentTickId")).toString(), pair.value(QStringLiteral("clockId")).toString(), }; - m_sequencer->readAccounts(fixedIds, forceRefresh, - [this, requestObject, network, walletOpen, config, pair, + guard->m_sequencer->readAccounts(fixedIds, forceRefresh, + [guard, requestObject, network, walletOpen, config, pair, callback = std::move(callback)]( QVector fixedReads) mutable { - m_sequencer->readAccounts(m_walletPublicAccountIds, false, - [this, requestObject, network, walletOpen, config, pair, + if (!guard) + return; + guard->m_sequencer->readAccounts( + guard->m_walletPublicAccountIds, false, + [guard, requestObject, network, walletOpen, config, pair, fixedReads = std::move(fixedReads), callback = std::move(callback)]( QVector walletReads) mutable { + if (!guard) + return; QStringList selectedIds; for (const QString& key : { QStringLiteral("holdingAId"), @@ -426,8 +477,7 @@ void NewPositionRuntime::buildQuoteInputAsync( if (!id.isEmpty() && !selectedIds.contains(id)) selectedIds.append(id); } - auto finish = [this, requestObject, network, walletOpen, - config, pair, + auto finish = [requestObject, network, walletOpen, config, fixedReads = std::move(fixedReads), walletReads = std::move(walletReads), callback = std::move(callback)]( @@ -464,7 +514,7 @@ void NewPositionRuntime::buildQuoteInputAsync( if (selectedIds.isEmpty()) finish({}); else - m_sequencer->readAccounts( + guard->m_sequencer->readAccounts( selectedIds, true, std::move(finish)); }); }); @@ -477,14 +527,17 @@ void NewPositionRuntime::quoteAsync(const QVariantMap& request, bool forceRefresh, ResultCallback callback) { + QPointer guard(this); buildQuoteInputAsync(request, network, walletOpen, forceRefresh, - [this, callback = std::move(callback)]( + [guard, callback = std::move(callback)]( QJsonObject input, QJsonObject error) mutable { + if (!guard) + return; if (!error.isEmpty()) { callback(error.toVariantMap()); return; } - const AmmClientResult result = m_client->quote(input); + const AmmClientResult result = guard->m_client->quote(input); callback((result.ok ? result.value : publicError(QStringLiteral("backend_error"))) .toVariantMap()); @@ -506,114 +559,154 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request, return; } m_submitInFlight = true; + const quint64 submitGeneration = ++m_submitGeneration; const quint64 walletGeneration = m_walletGeneration; - auto finish = [this, callback = std::move(callback)](QVariantMap result) mutable { - m_submitInFlight = false; - callback(std::move(result)); - }; + m_submitCallback = std::move(callback); + QPointer guard(this); buildQuoteInputAsync(request, network, true, true, - [this, quoteHash, walletGeneration, finish = std::move(finish)]( + [guard, quoteHash, submitGeneration, walletGeneration]( QJsonObject input, QJsonObject error) mutable { - if (walletGeneration != m_walletGeneration) { - finish(publicError(QStringLiteral("wallet_unavailable")).toVariantMap()); + if (!guard + || !guard->submitIsCurrent(submitGeneration, walletGeneration)) return; - } if (!error.isEmpty()) { - finish(error.toVariantMap()); + guard->finishSubmit(submitGeneration, error.toVariantMap()); return; } - const AmmClientResult quoteResult = m_client->quote(input); + const AmmClientResult quoteResult = guard->m_client->quote(input); if (!quoteResult.ok) { - finish(publicError(QStringLiteral("backend_error")).toVariantMap()); + guard->finishSubmit( + submitGeneration, + publicError(QStringLiteral("backend_error")).toVariantMap()); return; } const QJsonObject quote = quoteResult.value; if (quote.value(QStringLiteral("quoteHash")).toString() != quoteHash) { QJsonObject result = publicError(QStringLiteral("quote_changed")); result.insert(QStringLiteral("quote"), quote); - finish(result.toVariantMap()); + guard->finishSubmit(submitGeneration, result.toVariantMap()); return; } if (!quote.value(QStringLiteral("canSubmit")).toBool(false)) { QJsonObject result = publicError(QStringLiteral("quote_not_submittable")); result.insert(QStringLiteral("quote"), quote); - finish(result.toVariantMap()); + guard->finishSubmit(submitGeneration, result.toVariantMap()); return; } - QJsonValue freshLp; if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) { - if (walletGeneration != m_walletGeneration) { - finish(publicError(QStringLiteral("wallet_unavailable")).toVariantMap()); - return; - } - const WalletAccountCreation creation = m_wallet->createAccount(true); - if (!creation.ok() || !creation.publicAccount.ok()) { - finish(publicError(QStringLiteral("wallet_submission_failed")).toVariantMap()); - return; - } - freshLp = accountReadJson(creation.publicAccount); - if (!m_walletPublicAccountIds.contains(creation.accountId)) - m_walletPublicAccountIds.append(creation.accountId); + guard->m_wallet->createAccountAsync( + true, + [guard, input = std::move(input), quoteHash, + submitGeneration, walletGeneration]( + WalletAccountCreation creation) mutable { + if (!guard + || !guard->submitIsCurrent( + submitGeneration, walletGeneration)) + return; + if (!creation.ok() || !creation.publicAccount.ok()) { + const QString code = creation.failure + == WalletFailure::WalletUnavailable + ? QStringLiteral("wallet_unavailable") + : QStringLiteral("wallet_submission_failed"); + guard->finishSubmit( + submitGeneration, publicError(code).toVariantMap()); + return; + } + if (!guard->m_walletPublicAccountIds.contains(creation.accountId)) + guard->m_walletPublicAccountIds.append(creation.accountId); + guard->submitPlanAsync( + std::move(input), quoteHash, + accountReadJson(creation.publicAccount), + submitGeneration, walletGeneration); + }); + return; } + guard->submitPlanAsync( + std::move(input), quoteHash, {}, + submitGeneration, walletGeneration); + }); +} - QJsonObject planInput = input; - planInput.insert(QStringLiteral("quoteHash"), quoteHash); - planInput.insert(QStringLiteral("nowMs"), QDateTime::currentMSecsSinceEpoch()); - if (!freshLp.isUndefined()) - planInput.insert(QStringLiteral("freshLp"), freshLp); - const AmmClientResult planResult = m_client->plan(planInput); - if (!planResult.ok) { - finish(publicError(QStringLiteral("backend_error")).toVariantMap()); - return; - } - const QJsonObject plan = planResult.value; - if (plan.value(QStringLiteral("status")).toString() - != QStringLiteral("ready")) { - const QString code = plan.value(QStringLiteral("code")).toString(); - finish(publicError(code.isEmpty() - ? QStringLiteral("wallet_submission_failed") : code).toVariantMap()); - return; - } +void NewPositionRuntime::submitPlanAsync(QJsonObject input, + const QString& quoteHash, + QJsonValue freshLp, + quint64 submitGeneration, + quint64 walletGeneration) +{ + if (!submitIsCurrent(submitGeneration, walletGeneration)) + return; - const QStringList accountIds = jsonStringList( - plan.value(QStringLiteral("accountIds")).toArray()); - const QVector signingRequirements = jsonBoolList( - plan.value(QStringLiteral("signingRequirements")).toArray()); - const QVector instruction = jsonUIntList( - plan.value(QStringLiteral("instruction")).toArray()); - bool deadlineValid = false; - const qulonglong deadline = plan.value(QStringLiteral("deadlineMs")) - .toString().toULongLong(&deadlineValid); - if (!deadlineValid - || static_cast(QDateTime::currentMSecsSinceEpoch()) >= deadline) { - finish(publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap()); + input.insert(QStringLiteral("quoteHash"), quoteHash); + input.insert(QStringLiteral("nowMs"), QDateTime::currentMSecsSinceEpoch()); + if (!freshLp.isUndefined() && !freshLp.isNull()) + input.insert(QStringLiteral("freshLp"), std::move(freshLp)); + const AmmClientResult planResult = m_client->plan(input); + if (!planResult.ok) { + finishSubmit( + submitGeneration, + publicError(QStringLiteral("backend_error")).toVariantMap()); + return; + } + const QJsonObject plan = planResult.value; + if (plan.value(QStringLiteral("status")).toString() + != QStringLiteral("ready")) { + const QString code = plan.value(QStringLiteral("code")).toString(); + finishSubmit( + submitGeneration, + publicError(code.isEmpty() + ? QStringLiteral("wallet_submission_failed") : code).toVariantMap()); + return; + } + + bool deadlineValid = false; + const qulonglong deadline = plan.value(QStringLiteral("deadlineMs")) + .toString().toULongLong(&deadlineValid); + if (!deadlineValid + || static_cast(QDateTime::currentMSecsSinceEpoch()) >= deadline) { + finishSubmit( + submitGeneration, + publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap()); + return; + } + if (!submitIsCurrent(submitGeneration, walletGeneration)) + return; + + WalletTransaction transaction { + plan.value(QStringLiteral("programId")).toString(), + jsonStringList(plan.value(QStringLiteral("accountIds")).toArray()), + jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray()), + jsonUIntList(plan.value(QStringLiteral("instruction")).toArray()), + }; + QPointer guard(this); + m_wallet->submitPublicTransactionAsync( + transaction, + [guard, submitGeneration, walletGeneration, deadlineMs = + plan.value(QStringLiteral("deadlineMs")), affectedAccountIds = + plan.value(QStringLiteral("affectedAccountIds"))]( + WalletSubmission submission) mutable { + if (!guard + || !guard->submitIsCurrent(submitGeneration, walletGeneration)) return; - } - if (walletGeneration != m_walletGeneration) { - finish(publicError(QStringLiteral("wallet_unavailable")).toVariantMap()); - return; - } - const WalletSubmission submission = m_wallet->submitPublicTransaction({ - plan.value(QStringLiteral("programId")).toString(), - accountIds, - signingRequirements, - instruction, - }); const QString transactionId = submission.accepted() ? base58TransactionId(submission.nativeHash) : QString(); if (transactionId.isEmpty()) { - finish(publicError(QStringLiteral("wallet_submission_failed")).toVariantMap()); + const QString code = submission.failure + == WalletFailure::WalletUnavailable + ? QStringLiteral("wallet_unavailable") + : QStringLiteral("wallet_submission_failed"); + guard->finishSubmit( + submitGeneration, publicError(code).toVariantMap()); return; } - finish(QJsonObject { + guard->finishSubmit(submitGeneration, QJsonObject { { QStringLiteral("schema"), QString::fromLatin1(SCHEMA) }, { QStringLiteral("status"), QStringLiteral("submitted") }, { QStringLiteral("transactionId"), transactionId }, { QStringLiteral("nativeTransactionHash"), submission.nativeHash }, - { QStringLiteral("deadlineMs"), plan.value(QStringLiteral("deadlineMs")) }, + { QStringLiteral("deadlineMs"), deadlineMs }, { QStringLiteral("affectedAccountIds"), - plan.value(QStringLiteral("affectedAccountIds")) }, + affectedAccountIds }, }.toVariantMap()); }); } diff --git a/apps/amm/src/NewPositionRuntime.h b/apps/amm/src/NewPositionRuntime.h index f09289a..8f1d700 100644 --- a/apps/amm/src/NewPositionRuntime.h +++ b/apps/amm/src/NewPositionRuntime.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -15,7 +16,7 @@ class WalletProvider; class SequencerClient; struct WalletAccount; -class NewPositionRuntime { +class NewPositionRuntime : public QObject { public: using ResultCallback = std::function; @@ -41,6 +42,7 @@ public: const ActiveNetworkSnapshot& network, bool walletCanSubmit, ResultCallback callback); + void cancelSubmit(); QVariantMap context(const QVariantMap& request, const ActiveNetworkSnapshot& network, @@ -65,7 +67,15 @@ private: const ActiveNetworkSnapshot& network, bool walletOpen, bool forceRefresh, - std::function callback) const; + std::function callback); + void submitPlanAsync(QJsonObject input, + const QString& quoteHash, + QJsonValue freshLp, + quint64 submitGeneration, + quint64 walletGeneration); + void finishSubmit(quint64 submitGeneration, QVariantMap result); + bool submitIsCurrent(quint64 submitGeneration, + quint64 walletGeneration) const; WalletProvider* m_wallet; AmmClient* m_client; @@ -73,4 +83,6 @@ private: QStringList m_walletPublicAccountIds; bool m_submitInFlight = false; quint64 m_walletGeneration = 0; + quint64 m_submitGeneration = 0; + ResultCallback m_submitCallback; }; diff --git a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp index 584d17c..c404779 100644 --- a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp +++ b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp @@ -15,6 +15,8 @@ #include #include +#include +#include #include namespace { @@ -143,6 +145,15 @@ namespace { return first && second; } + bool waitForCondition(const std::function& condition) + { + QElapsedTimer timer; + timer.start(); + while (!condition() && timer.elapsed() < 3000) + QCoreApplication::processEvents(QEventLoop::AllEvents, 10); + return condition(); + } + class FakeWallet final : public WalletProvider { public: WalletSession connect(const WalletPaths&) override @@ -182,6 +193,17 @@ namespace { return creation; } + void createAccountAsync(bool isPublic, AccountCreationCallback callback) override + { + WalletAccountCreation creation = createAccount(isPublic); + if (deferAccountCreation) { + pendingAccountCreation = std::move(callback); + pendingCreation = std::move(creation); + } else { + callback(std::move(creation)); + } + } + WalletAccountRead readPublicAccount(const QString& accountId) const override { WalletAccountRead read; @@ -197,6 +219,32 @@ namespace { return { WalletFailure::None, transactionHash }; } + void submitPublicTransactionAsync( + const WalletTransaction& transaction, SubmissionCallback callback) override + { + WalletSubmission submission = submitPublicTransaction(transaction); + if (deferSubmission) { + pendingSubmissionCallback = std::move(callback); + pendingSubmission = std::move(submission); + } else { + callback(std::move(submission)); + } + } + + void finishAccountCreation() + { + AccountCreationCallback callback = std::move(pendingAccountCreation); + if (callback) + callback(std::move(pendingCreation)); + } + + void finishSubmission() + { + SubmissionCallback callback = std::move(pendingSubmissionCallback); + if (callback) + callback(std::move(pendingSubmission)); + } + void disconnect() override {} QString transactionHash = QStringLiteral( @@ -205,6 +253,12 @@ namespace { int createdAccounts = 0; int submissions = 0; WalletTransaction submitted; + bool deferAccountCreation = false; + bool deferSubmission = false; + AccountCreationCallback pendingAccountCreation; + SubmissionCallback pendingSubmissionCallback; + WalletAccountCreation pendingCreation; + WalletSubmission pendingSubmission; }; class FakeAmmClient final : public AmmClient { @@ -625,5 +679,113 @@ int main(int argc, char** argv) "wallet change should cancel submit before side effects")) return 1; + LocalRpcServer asyncSubmitServer; + if (!expect(asyncSubmitServer.listen(), "async-submit sequencer should listen")) + return 1; + if (!expect(sequencerConfig.resize(0) && sequencerConfig.seek(0), + "async-submit config should rewind")) + return 1; + sequencerConfig.write(QJsonDocument(QJsonObject { + { QStringLiteral("sequencer_addr"), asyncSubmitServer.endpoint() }, + }).toJson(QJsonDocument::Compact)); + sequencerConfig.flush(); + if (!expect(sequencer.configure(sequencerConfig.fileName()), + "async-submit sequencer should configure")) + return 1; + + FakeWallet asyncWallet; + asyncWallet.deferAccountCreation = true; + asyncWallet.deferSubmission = true; + FakeAmmClient asyncClient; + NewPositionRuntime asyncRuntime(&asyncWallet, &asyncClient, &sequencer); + int asyncCallbackCount = 0; + QVariantMap asyncResult; + asyncRuntime.submitAsync( + request, QStringLiteral("sha256:expected"), readyNetwork(), true, + [&](QVariantMap result) { + ++asyncCallbackCount; + asyncResult = std::move(result); + }); + if (!expect(waitForCondition([&]() { return asyncWallet.createdAccounts == 1; }), + "submit should asynchronously request a fresh account")) + return 1; + if (!expect(asyncCallbackCount == 0 && asyncWallet.submissions == 0, + "account creation should not block or finish submission")) + return 1; + + QVariantMap concurrentResult; + asyncRuntime.submitAsync( + request, QStringLiteral("sha256:expected"), readyNetwork(), true, + [&](QVariantMap result) { concurrentResult = std::move(result); }); + if (!expect(concurrentResult.value(QStringLiteral("code")).toString() + == QStringLiteral("submit_in_progress"), + "concurrent submit should fail while async mutation is pending")) + return 1; + + asyncWallet.finishAccountCreation(); + if (!expect(waitForCondition([&]() { return asyncWallet.submissions == 1; }), + "fresh account completion should dispatch transaction")) + return 1; + if (!expect(asyncCallbackCount == 0, + "transaction submission should remain asynchronous")) + return 1; + asyncWallet.finishSubmission(); + if (!expect(asyncCallbackCount == 1 + && asyncResult.value(QStringLiteral("status")).toString() + == QStringLiteral("submitted"), + "async wallet completion should finish exactly once")) + return 1; + + FakeWallet cancelledMutationWallet; + cancelledMutationWallet.deferAccountCreation = true; + FakeAmmClient cancelledMutationClient; + NewPositionRuntime cancelledMutationRuntime( + &cancelledMutationWallet, &cancelledMutationClient, &sequencer); + int cancelledMutationCallbacks = 0; + QVariantMap cancelledMutationResult; + cancelledMutationRuntime.submitAsync( + request, QStringLiteral("sha256:expected"), readyNetwork(), true, + [&](QVariantMap result) { + ++cancelledMutationCallbacks; + cancelledMutationResult = std::move(result); + }); + if (!expect(waitForCondition( + [&]() { return cancelledMutationWallet.createdAccounts == 1; }), + "cancellable submit should reach account creation")) + return 1; + cancelledMutationRuntime.clearWalletAccounts(); + if (!expect(cancelledMutationCallbacks == 1 + && cancelledMutationResult.value(QStringLiteral("code")).toString() + == QStringLiteral("wallet_unavailable"), + "wallet change should immediately cancel pending mutation")) + return 1; + cancelledMutationWallet.finishAccountCreation(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 10); + if (!expect(cancelledMutationCallbacks == 1 + && cancelledMutationWallet.submissions == 0, + "stale account completion should have no transaction side effect")) + return 1; + + FakeWallet destroyedRuntimeWallet; + destroyedRuntimeWallet.deferSubmission = true; + FakeAmmClient destroyedRuntimeClient; + destroyedRuntimeClient.requiresFreshLp = false; + int destroyedRuntimeCallbacks = 0; + auto destroyedRuntime = std::make_unique( + &destroyedRuntimeWallet, &destroyedRuntimeClient, &sequencer); + destroyedRuntime->submitAsync( + request, QStringLiteral("sha256:expected"), readyNetwork(), true, + [&](QVariantMap) { ++destroyedRuntimeCallbacks; }); + if (!expect(waitForCondition( + [&]() { return destroyedRuntimeWallet.submissions == 1; }), + "lifetime test should reach async submission")) + return 1; + destroyedRuntime.reset(); + destroyedRuntimeWallet.finishSubmission(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 10); + if (!expect(destroyedRuntimeCallbacks == 0, + "destroyed runtime should ignore late wallet completion")) + return 1; + return 0; } diff --git a/apps/shared/wallet/src/LogosWalletProvider.cpp b/apps/shared/wallet/src/LogosWalletProvider.cpp index fb47ada..eeb7547 100644 --- a/apps/shared/wallet/src/LogosWalletProvider.cpp +++ b/apps/shared/wallet/src/LogosWalletProvider.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,58 @@ WalletAccountRead parsePublicAccount(const QString& accountId, const QString& pa read.dataHex = data; return read; } + +bool encodeTransaction(const WalletTransaction& transaction, + QVariantList* signingRequirements, + QVariantList* instruction) +{ + if (!isHex(transaction.programId, 64) + || transaction.accountIds.size() != transaction.signingRequirements.size()) { + return false; + } + for (const QString& accountId : transaction.accountIds) { + if (!isHex(accountId, 64)) + return false; + } + + signingRequirements->reserve(transaction.signingRequirements.size()); + for (bool required : transaction.signingRequirements) + signingRequirements->append(required); + + instruction->reserve(transaction.instruction.size()); + for (quint32 word : transaction.instruction) + instruction->append(word); + return true; +} + +WalletSubmission parseSubmission(const QString& response) +{ + WalletSubmission submission; + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(response.toUtf8(), &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) { + submission.failure = WalletFailure::SubmissionFailed; + return submission; + } + + const QJsonObject result = document.object(); + const QJsonValue success = result.value(QStringLiteral("success")); + const QJsonValue error = result.value(QStringLiteral("error")); + const QString hash = result.value(QStringLiteral("tx_hash")).toString(); + const bool emptyError = error.isUndefined() + || error.isNull() + || (error.isString() && error.toString().isEmpty()); + if (!success.isBool() + || !success.toBool() + || !emptyError + || !isHex(hash, 64, false)) { + submission.failure = WalletFailure::SubmissionFailed; + return submission; + } + + submission.nativeHash = hash.toLower(); + return submission; +} } struct LogosWalletProvider::Impl { @@ -138,12 +191,16 @@ LogosWalletProvider::LogosWalletProvider(LogosModules* logos) LogosWalletProvider::~LogosWalletProvider() { + ++m_generation; + ++m_sessionGeneration; if (m_connected) save(); } WalletSession LogosWalletProvider::connect(const WalletPaths& paths) { + ++m_generation; + ++m_sessionGeneration; clearSnapshot(); if (!m_impl->logos) return failedSession(WalletFailure::WalletUnavailable); @@ -168,6 +225,7 @@ WalletSession LogosWalletProvider::connect(const WalletPaths& paths) void LogosWalletProvider::connectAsync(const WalletPaths& paths, SessionCallback callback) { + ++m_sessionGeneration; clearSnapshot(); const quint64 generation = ++m_generation; if (!m_impl->logos) { @@ -239,6 +297,8 @@ void LogosWalletProvider::connectAsync(const WalletPaths& paths, SessionCallback WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths, const QString& password) { + ++m_generation; + ++m_sessionGeneration; clearSnapshot(); if (!m_impl->logos) return failedCreation(WalletFailure::WalletUnavailable); @@ -368,6 +428,167 @@ WalletAccountCreation LogosWalletProvider::createAccount(bool isPublic) return creation; } +void LogosWalletProvider::createAccountAsync(bool isPublic, + AccountCreationCallback callback) +{ + QPointer guard(this); + if (!m_connected || !m_impl->logos) { + QTimer::singleShot(0, [guard, callback = std::move(callback)]() mutable { + if (!guard) + return; + WalletAccountCreation creation; + creation.failure = WalletFailure::WalletUnavailable; + callback(std::move(creation)); + }); + return; + } + + const quint64 sessionGeneration = m_sessionGeneration; + auto finish = [guard, sessionGeneration, isPublic, + callback = std::move(callback)]( + WalletAccountCreation creation, QString fallbackBalance) mutable { + if (!guard) + return; + if (sessionGeneration != guard->m_sessionGeneration) { + WalletAccountCreation failed; + failed.failure = WalletFailure::WalletUnavailable; + callback(std::move(failed)); + return; + } + + if (guard->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( + guard->m_snapshot.publicAccountReads.begin(), + guard->m_snapshot.publicAccountReads.end(), + [&creation](const WalletAccountRead& existing) { + return existing.accountId == creation.accountId; + }); + if (read == guard->m_snapshot.publicAccountReads.end()) + guard->m_snapshot.publicAccountReads.append(creation.publicAccount); + else + *read = creation.publicAccount; + } else { + account.balance = std::move(fallbackBalance); + } + auto existing = std::find_if( + guard->m_snapshot.accounts.begin(), + guard->m_snapshot.accounts.end(), + [&creation](const WalletAccount& candidate) { + return candidate.address == creation.accountId; + }); + if (existing == guard->m_snapshot.accounts.end()) + guard->m_snapshot.accounts.append(account); + else + *existing = account; + creation.snapshot = guard->m_snapshot; + } + callback(std::move(creation)); + }; + + auto created = [guard, sessionGeneration, isPublic, + finish = std::move(finish)](QString accountId) mutable { + if (!guard) + return; + if (sessionGeneration != guard->m_sessionGeneration) { + WalletAccountCreation failed; + failed.failure = WalletFailure::WalletUnavailable; + finish(std::move(failed), {}); + return; + } + + WalletAccountCreation creation; + creation.accountId = std::move(accountId); + if (!isHex(creation.accountId, 64)) { + creation.failure = WalletFailure::CreateFailed; + finish(std::move(creation), {}); + return; + } + + guard->m_impl->logos->logos_execution_zone.saveAsync( + [guard, sessionGeneration, isPublic, creation = std::move(creation), + finish = std::move(finish)](int result) mutable { + if (!guard) + return; + if (sessionGeneration != guard->m_sessionGeneration) { + WalletAccountCreation failed; + failed.failure = WalletFailure::WalletUnavailable; + finish(std::move(failed), {}); + return; + } + if (result != WALLET_FFI_SUCCESS) { + creation.failure = WalletFailure::SaveFailed; + finish(std::move(creation), {}); + return; + } + + if (!isPublic) { + guard->m_impl->logos->logos_execution_zone.get_balanceAsync( + creation.accountId, false, + [guard, sessionGeneration, creation = std::move(creation), + finish = std::move(finish)](QString balance) mutable { + if (!guard) + return; + if (sessionGeneration != guard->m_sessionGeneration) { + WalletAccountCreation failed; + failed.failure = WalletFailure::WalletUnavailable; + finish(std::move(failed), {}); + return; + } + finish(std::move(creation), std::move(balance)); + }); + return; + } + + const QString accountId = creation.accountId; + guard->m_impl->logos->logos_execution_zone.get_account_publicAsync( + accountId, + [guard, sessionGeneration, creation = std::move(creation), + finish = std::move(finish)](QString payload) mutable { + if (!guard) + return; + if (sessionGeneration != guard->m_sessionGeneration) { + WalletAccountCreation failed; + failed.failure = WalletFailure::WalletUnavailable; + finish(std::move(failed), {}); + return; + } + creation.publicAccount = parsePublicAccount( + creation.accountId, payload); + if (creation.publicAccount.ok()) { + finish(std::move(creation), {}); + return; + } + guard->m_impl->logos->logos_execution_zone.get_balanceAsync( + creation.accountId, true, + [guard, sessionGeneration, + creation = std::move(creation), + finish = std::move(finish)](QString balance) mutable { + if (!guard) + return; + if (sessionGeneration != guard->m_sessionGeneration) { + WalletAccountCreation failed; + failed.failure = WalletFailure::WalletUnavailable; + finish(std::move(failed), {}); + return; + } + finish(std::move(creation), std::move(balance)); + }); + }); + }); + }; + + if (isPublic) + m_impl->logos->logos_execution_zone.create_account_publicAsync(std::move(created)); + else + m_impl->logos->logos_execution_zone.create_account_privateAsync(std::move(created)); +} + WalletAccountRead LogosWalletProvider::readPublicAccount(const QString& accountId) const { if (!m_impl->logos || !isHex(accountId, 64)) @@ -385,27 +606,12 @@ WalletSubmission LogosWalletProvider::submitPublicTransaction( submission.failure = WalletFailure::WalletUnavailable; return submission; } - if (!isHex(transaction.programId, 64) - || transaction.accountIds.size() != transaction.signingRequirements.size()) { + QVariantList signingRequirements; + QVariantList instruction; + if (!encodeTransaction(transaction, &signingRequirements, &instruction)) { submission.failure = WalletFailure::InvalidRequest; return submission; } - for (const QString& accountId : transaction.accountIds) { - if (!isHex(accountId, 64)) { - submission.failure = WalletFailure::InvalidRequest; - return submission; - } - } - - QVariantList signingRequirements; - signingRequirements.reserve(transaction.signingRequirements.size()); - for (bool required : transaction.signingRequirements) - signingRequirements.append(required); - - QVariantList instruction; - instruction.reserve(transaction.instruction.size()); - for (quint32 word : transaction.instruction) - instruction.append(word); const QString response = m_impl->logos->logos_execution_zone.send_generic_public_transaction( @@ -413,36 +619,60 @@ WalletSubmission LogosWalletProvider::submitPublicTransaction( signingRequirements, QVariant::fromValue(instruction), transaction.programId); + return parseSubmission(response); +} - QJsonParseError parseError; - const QJsonDocument document = QJsonDocument::fromJson(response.toUtf8(), &parseError); - if (parseError.error != QJsonParseError::NoError || !document.isObject()) { - submission.failure = WalletFailure::SubmissionFailed; - return submission; +void LogosWalletProvider::submitPublicTransactionAsync( + const WalletTransaction& transaction, SubmissionCallback callback) +{ + QPointer guard(this); + WalletSubmission submission; + if (!m_connected || !m_impl->logos) { + submission.failure = WalletFailure::WalletUnavailable; + QTimer::singleShot(0, [guard, callback = std::move(callback), + submission = std::move(submission)]() mutable { + if (guard) + callback(std::move(submission)); + }); + return; } - const QJsonObject result = document.object(); - const QJsonValue success = result.value(QStringLiteral("success")); - const QJsonValue error = result.value(QStringLiteral("error")); - const QString hash = result.value(QStringLiteral("tx_hash")).toString(); - const bool emptyError = error.isUndefined() - || error.isNull() - || (error.isString() && error.toString().isEmpty()); - if (!success.isBool() - || !success.toBool() - || !emptyError - || !isHex(hash, 64, false)) { - submission.failure = WalletFailure::SubmissionFailed; - return submission; + QVariantList signingRequirements; + QVariantList instruction; + if (!encodeTransaction(transaction, &signingRequirements, &instruction)) { + submission.failure = WalletFailure::InvalidRequest; + QTimer::singleShot(0, [guard, callback = std::move(callback), + submission = std::move(submission)]() mutable { + if (guard) + callback(std::move(submission)); + }); + return; } - submission.nativeHash = hash.toLower(); - return submission; + const quint64 sessionGeneration = m_sessionGeneration; + m_impl->logos->logos_execution_zone.send_generic_public_transactionAsync( + transaction.accountIds, + signingRequirements, + QVariant::fromValue(instruction), + transaction.programId, + [guard, sessionGeneration, callback = std::move(callback)]( + QString response) mutable { + if (!guard) + return; + if (sessionGeneration != guard->m_sessionGeneration) { + WalletSubmission failed; + failed.failure = WalletFailure::WalletUnavailable; + callback(std::move(failed)); + return; + } + callback(parseSubmission(response)); + }); } void LogosWalletProvider::disconnect() { ++m_generation; + ++m_sessionGeneration; if (m_connected) save(); clearSnapshot(); diff --git a/apps/shared/wallet/src/LogosWalletProvider.h b/apps/shared/wallet/src/LogosWalletProvider.h index b037240..db91702 100644 --- a/apps/shared/wallet/src/LogosWalletProvider.h +++ b/apps/shared/wallet/src/LogosWalletProvider.h @@ -2,12 +2,14 @@ #include +#include + #include "WalletProvider.h" class LogosAPI; struct LogosModules; -class LogosWalletProvider final : public WalletProvider { +class LogosWalletProvider final : public QObject, public WalletProvider { public: explicit LogosWalletProvider(LogosAPI* api); explicit LogosWalletProvider(LogosModules* logos); @@ -21,9 +23,12 @@ public: void snapshotAsync(bool forceRefresh, SnapshotCallback callback) override; void clearSnapshot() override; WalletAccountCreation createAccount(bool isPublic) override; + void createAccountAsync(bool isPublic, AccountCreationCallback callback) override; WalletAccountRead readPublicAccount(const QString& accountId) const override; WalletSubmission submitPublicTransaction( const WalletTransaction& transaction) override; + void submitPublicTransactionAsync( + const WalletTransaction& transaction, SubmissionCallback callback) override; void disconnect() override; private: @@ -38,4 +43,5 @@ private: bool m_snapshotReady = false; bool m_connected = false; quint64 m_generation = 0; + quint64 m_sessionGeneration = 0; }; diff --git a/apps/shared/wallet/src/WalletProvider.h b/apps/shared/wallet/src/WalletProvider.h index 198f598..3ecbe67 100644 --- a/apps/shared/wallet/src/WalletProvider.h +++ b/apps/shared/wallet/src/WalletProvider.h @@ -95,6 +95,8 @@ class WalletProvider { public: using SessionCallback = std::function; using SnapshotCallback = std::function; + using AccountCreationCallback = std::function; + using SubmissionCallback = std::function; virtual ~WalletProvider() = default; @@ -106,8 +108,11 @@ public: virtual void snapshotAsync(bool forceRefresh, SnapshotCallback callback) = 0; virtual void clearSnapshot() = 0; virtual WalletAccountCreation createAccount(bool isPublic) = 0; + virtual void createAccountAsync(bool isPublic, AccountCreationCallback callback) = 0; virtual WalletAccountRead readPublicAccount(const QString& accountId) const = 0; virtual WalletSubmission submitPublicTransaction( const WalletTransaction& transaction) = 0; + virtual void submitPublicTransactionAsync( + const WalletTransaction& transaction, SubmissionCallback callback) = 0; virtual void disconnect() = 0; }; diff --git a/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp b/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp index 6517953..94db985 100644 --- a/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp +++ b/apps/shared/wallet/tests/cpp/LogosWalletProviderTest.cpp @@ -61,6 +61,9 @@ private slots: void createdAccountDoesNotRescanWallet(); void dispatchesExactGenericTransaction(); void rejectsInvalidSubmissionResponses(); + void walletMutationsUseAsyncSdk(); + void staleAsyncMutationCannotCrossSession(); + void destroyedProviderIgnoresLateMutation(); void exposesStableAccountModelRoles(); void fakeProviderImplementsConsumerContract(); void controllerOwnsUiWalletFlow(); @@ -349,6 +352,108 @@ void LogosWalletProviderTest::rejectsInvalidSubmissionResponses() WalletFailure::InvalidRequest); } +void LogosWalletProviderTest::walletMutationsUseAsyncSdk() +{ + LogosModules modules; + modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer"); + modules.logos_execution_zone.publicAccountId = ACCOUNT_A; + modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson()); + modules.logos_execution_zone.transactionResponse = QStringLiteral( + R"({"success":true,"tx_hash":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"})"); + LogosWalletProvider provider(&modules); + QVERIFY(provider.connect({}).ok()); + + modules.logos_execution_zone.deferPublicAccountCreation = true; + bool creationFinished = false; + WalletAccountCreation creation; + const int listCalls = modules.logos_execution_zone.listCalls; + const int syncCalls = modules.logos_execution_zone.syncCalls; + provider.createAccountAsync(true, [&](WalletAccountCreation result) { + creation = std::move(result); + creationFinished = true; + }); + QVERIFY(!creationFinished); + QVERIFY(modules.logos_execution_zone.pendingPublicAccountCreation); + modules.logos_execution_zone.finishPublicAccountCreation(); + QVERIFY(creationFinished); + QVERIFY(creation.ok()); + QVERIFY(creation.publicAccount.ok()); + QCOMPARE(creation.accountId, ACCOUNT_A); + QCOMPARE(creation.snapshot.accounts.size(), 1); + QCOMPARE(modules.logos_execution_zone.listCalls, listCalls); + QCOMPARE(modules.logos_execution_zone.syncCalls, syncCalls); + + WalletTransaction transaction { + PROGRAM_ID, + { ACCOUNT_A, ACCOUNT_B }, + { true, false }, + { 7, 0, 4294967295U }, + }; + modules.logos_execution_zone.deferSubmission = true; + bool submissionFinished = false; + WalletSubmission submission; + provider.submitPublicTransactionAsync( + transaction, [&](WalletSubmission result) { + submission = std::move(result); + submissionFinished = true; + }); + QVERIFY(!submissionFinished); + QCOMPARE(modules.logos_execution_zone.submittedProgramId, PROGRAM_ID); + QCOMPARE(modules.logos_execution_zone.submittedAccountIds, transaction.accountIds); + QCOMPARE(modules.logos_execution_zone.submittedSigningRequirements, + QVariantList({ true, false })); + QCOMPARE(modules.logos_execution_zone.submittedInstruction.toList(), + QVariantList({ 7U, 0U, 4294967295U })); + modules.logos_execution_zone.finishSubmission(); + QVERIFY(submissionFinished); + QVERIFY(submission.accepted()); + QCOMPARE(submission.nativeHash, QString(64, QLatin1Char('a'))); +} + +void LogosWalletProviderTest::staleAsyncMutationCannotCrossSession() +{ + LogosModules modules; + modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer"); + modules.logos_execution_zone.publicAccountId = ACCOUNT_A; + modules.logos_execution_zone.publicAccounts.insert(ACCOUNT_A, publicAccountJson()); + modules.logos_execution_zone.deferPublicAccountCreation = true; + LogosWalletProvider provider(&modules); + QVERIFY(provider.connect({}).ok()); + + int callbackCount = 0; + WalletAccountCreation creation; + provider.createAccountAsync(true, [&](WalletAccountCreation result) { + ++callbackCount; + creation = std::move(result); + }); + provider.disconnect(); + modules.logos_execution_zone.finishPublicAccountCreation(); + + QCOMPARE(callbackCount, 1); + QCOMPARE(creation.failure, WalletFailure::WalletUnavailable); + QCOMPARE(modules.logos_execution_zone.publicReadCalls, 0); +} + +void LogosWalletProviderTest::destroyedProviderIgnoresLateMutation() +{ + LogosModules modules; + modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer"); + modules.logos_execution_zone.transactionResponse = QStringLiteral( + R"({"success":true,"tx_hash":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})"); + modules.logos_execution_zone.deferSubmission = true; + int callbackCount = 0; + { + LogosWalletProvider provider(&modules); + QVERIFY(provider.connect({}).ok()); + provider.submitPublicTransactionAsync( + { PROGRAM_ID, { ACCOUNT_A }, { true }, { 1 } }, + [&](WalletSubmission) { ++callbackCount; }); + } + + modules.logos_execution_zone.finishSubmission(); + QCOMPARE(callbackCount, 0); +} + void LogosWalletProviderTest::exposesStableAccountModelRoles() { WalletAccountModel model; diff --git a/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h b/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h index 13ca5d7..87cf30f 100644 --- a/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h +++ b/apps/shared/wallet/tests/cpp/fixtures/logos_sdk.h @@ -7,6 +7,7 @@ #include #include +#include class LogosAPI; @@ -32,6 +33,10 @@ public: int listCalls = 0; int publicReadCalls = 0; int submitCalls = 0; + bool deferPublicAccountCreation = false; + bool deferSubmission = false; + std::function pendingPublicAccountCreation; + std::function pendingSubmission; QString openedConfig; QString openedStorage; QString createdConfig; @@ -77,6 +82,17 @@ public: QString create_account_public() { return publicAccountId; } QString create_account_private() { return privateAccountId; } + void create_account_publicAsync(std::function callback) + { + if (deferPublicAccountCreation) + pendingPublicAccountCreation = std::move(callback); + else + callback(create_account_public()); + } + void create_account_privateAsync(std::function callback) + { + callback(create_account_private()); + } int get_last_synced_block() const { return lastSyncedBlock; } int get_current_block_height() const { return currentBlockHeight; } @@ -150,6 +166,38 @@ public: submittedProgramId = programId; return transactionResponse; } + + void send_generic_public_transactionAsync( + const QStringList& accountIds, + const QVariantList& signingRequirements, + const QVariant& instruction, + const QString& programId, + std::function callback) + { + ++submitCalls; + submittedAccountIds = accountIds; + submittedSigningRequirements = signingRequirements; + submittedInstruction = instruction; + submittedProgramId = programId; + if (deferSubmission) + pendingSubmission = std::move(callback); + else + callback(transactionResponse); + } + + void finishPublicAccountCreation() + { + auto callback = std::move(pendingPublicAccountCreation); + if (callback) + callback(publicAccountId); + } + + void finishSubmission() + { + auto callback = std::move(pendingSubmission); + if (callback) + callback(transactionResponse); + } }; struct LogosModules { diff --git a/apps/shared/wallet/tests/support/FakeWalletProvider.h b/apps/shared/wallet/tests/support/FakeWalletProvider.h index d525f3e..0c1327a 100644 --- a/apps/shared/wallet/tests/support/FakeWalletProvider.h +++ b/apps/shared/wallet/tests/support/FakeWalletProvider.h @@ -80,6 +80,11 @@ public: return createAccountResult; } + void createAccountAsync(bool isPublic, AccountCreationCallback callback) override + { + callback(createAccount(isPublic)); + } + WalletAccountRead readPublicAccount(const QString& accountId) const override { ++readCalls; @@ -96,6 +101,12 @@ public: return submissionResult; } + void submitPublicTransactionAsync( + const WalletTransaction& transaction, SubmissionCallback callback) override + { + callback(submitPublicTransaction(transaction)); + } + void disconnect() override { ++disconnectCalls; } void finishConnect()