From 02a65aaf091812b54988b8ea3ce10958e6f78877 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Fri, 17 Jul 2026 20:15:39 -0300 Subject: [PATCH] fix(amm): cancel submits after wallet changes --- apps/amm/src/NewPositionRuntime.cpp | 24 ++++++++-- apps/amm/src/NewPositionRuntime.h | 1 + apps/amm/tests/cpp/NewPositionRuntimeTest.cpp | 44 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/amm/src/NewPositionRuntime.cpp b/apps/amm/src/NewPositionRuntime.cpp index 0b94f22..1e18f61 100644 --- a/apps/amm/src/NewPositionRuntime.cpp +++ b/apps/amm/src/NewPositionRuntime.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -169,17 +170,21 @@ NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet, void NewPositionRuntime::clearWalletAccounts() { + ++m_walletGeneration; m_walletPublicAccountIds.clear(); m_wallet->clearSnapshot(); } void NewPositionRuntime::setWalletAccounts(const QVector& accounts) { - m_walletPublicAccountIds.clear(); + QStringList publicAccountIds; for (const WalletAccount& account : accounts) { if (account.isPublic) - m_walletPublicAccountIds.append(account.address); + publicAccountIds.append(account.address); } + if (publicAccountIds != m_walletPublicAccountIds) + ++m_walletGeneration; + m_walletPublicAccountIds = std::move(publicAccountIds); } QJsonArray NewPositionRuntime::walletAccountReads(bool walletOpen, bool refresh) const @@ -501,13 +506,18 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request, return; } m_submitInFlight = true; + const quint64 walletGeneration = m_walletGeneration; auto finish = [this, callback = std::move(callback)](QVariantMap result) mutable { m_submitInFlight = false; callback(std::move(result)); }; buildQuoteInputAsync(request, network, true, true, - [this, quoteHash, finish = std::move(finish)]( + [this, quoteHash, walletGeneration, finish = std::move(finish)]( QJsonObject input, QJsonObject error) mutable { + if (walletGeneration != m_walletGeneration) { + finish(publicError(QStringLiteral("wallet_unavailable")).toVariantMap()); + return; + } if (!error.isEmpty()) { finish(error.toVariantMap()); return; @@ -533,6 +543,10 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request, 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()); @@ -576,6 +590,10 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request, finish(publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap()); return; } + if (walletGeneration != m_walletGeneration) { + finish(publicError(QStringLiteral("wallet_unavailable")).toVariantMap()); + return; + } const WalletSubmission submission = m_wallet->submitPublicTransaction({ plan.value(QStringLiteral("programId")).toString(), accountIds, diff --git a/apps/amm/src/NewPositionRuntime.h b/apps/amm/src/NewPositionRuntime.h index 7180975..f09289a 100644 --- a/apps/amm/src/NewPositionRuntime.h +++ b/apps/amm/src/NewPositionRuntime.h @@ -72,4 +72,5 @@ private: SequencerClient* m_sequencer; QStringList m_walletPublicAccountIds; bool m_submitInFlight = false; + quint64 m_walletGeneration = 0; }; diff --git a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp index 84b5386..584d17c 100644 --- a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp +++ b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp @@ -581,5 +581,49 @@ int main(int argc, char** argv) "forced follow-up read should complete")) return 1; + LocalRpcServer cancelledSubmitServer; + if (!expect(cancelledSubmitServer.listen(), + "cancelled-submit sequencer should listen")) + return 1; + cancelledSubmitServer.holdResponses(); + if (!expect(sequencerConfig.resize(0) && sequencerConfig.seek(0), + "cancelled-submit config should rewind")) + return 1; + sequencerConfig.write(QJsonDocument(QJsonObject { + { QStringLiteral("sequencer_addr"), cancelledSubmitServer.endpoint() }, + }).toJson(QJsonDocument::Compact)); + sequencerConfig.flush(); + if (!expect(sequencer.configure(sequencerConfig.fileName()), + "cancelled-submit sequencer should configure")) + return 1; + + FakeWallet cancelledWallet; + FakeAmmClient cancelledClient; + NewPositionRuntime cancelledRuntime( + &cancelledWallet, &cancelledClient, &sequencer); + bool cancelledCompleted = false; + QVariantMap cancelledResult; + cancelledRuntime.submitAsync( + request, QStringLiteral("sha256:expected"), readyNetwork(), true, + [&](QVariantMap result) { + cancelledResult = std::move(result); + cancelledCompleted = true; + }); + if (!expect(waitForRequestCount(cancelledSubmitServer, 1), + "submit should begin account reads")) + return 1; + cancelledRuntime.clearWalletAccounts(); + cancelledRuntime.setWalletAccounts({}); + cancelledSubmitServer.releaseNextResponse(); + if (!expect(waitForCallbacks(cancelledCompleted, cancelledCompleted), + "cancelled submit should complete")) + return 1; + if (!expect(cancelledResult.value(QStringLiteral("code")).toString() + == QStringLiteral("wallet_unavailable") + && cancelledWallet.createdAccounts == 0 + && cancelledWallet.submissions == 0, + "wallet change should cancel submit before side effects")) + return 1; + return 0; }