fix(amm): submit wallet mutations asynchronously

This commit is contained in:
Ricardo Guilherme Schmidt
2026-07-18 19:55:35 -03:00
parent 6b654ff937
commit 5b3663ef27
4 changed files with 363 additions and 93 deletions
+3
View File
@@ -279,6 +279,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();
@@ -300,6 +301,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();
+184 -91
View File
@@ -8,6 +8,7 @@
#include <QByteArray>
#include <QDateTime>
#include <QJsonObject>
#include <QPointer>
#include <QScopedValueRollback>
#include <libbase58.h>
@@ -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<WalletAccount>& 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<NewPositionRuntime> guard(this);
m_sequencer->readAccounts({ configId }, refreshPublicData,
[this, request, network, walletOpen, refreshPublicData,
[guard, request, network, walletOpen, refreshPublicData,
callback = std::move(callback)](QVector<WalletAccountRead> 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<WalletAccountRead> 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<WalletAccountRead> 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<void(QJsonObject, QJsonObject)> callback) const
std::function<void(QJsonObject, QJsonObject)> 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<NewPositionRuntime> guard(this);
m_sequencer->readAccounts({ configId }, forceRefresh,
[this, request, network, walletOpen, forceRefresh,
[guard, request, network, walletOpen, forceRefresh,
callback = std::move(callback)](QVector<WalletAccountRead> 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<WalletAccountRead> 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<WalletAccountRead> 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<NewPositionRuntime> 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<NewPositionRuntime> 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<bool> signingRequirements = jsonBoolList(
plan.value(QStringLiteral("signingRequirements")).toArray());
const QVector<quint32> instruction = jsonUIntList(
plan.value(QStringLiteral("instruction")).toArray());
bool deadlineValid = false;
const qulonglong deadline = plan.value(QStringLiteral("deadlineMs"))
.toString().toULongLong(&deadlineValid);
if (!deadlineValid
|| static_cast<qulonglong>(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<qulonglong>(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<NewPositionRuntime> 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());
});
}
+14 -2
View File
@@ -2,6 +2,7 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QString>
#include <QVariantMap>
#include <QVector>
@@ -15,7 +16,7 @@ class WalletProvider;
class SequencerClient;
struct WalletAccount;
class NewPositionRuntime {
class NewPositionRuntime : public QObject {
public:
using ResultCallback = std::function<void(QVariantMap)>;
@@ -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<void(QJsonObject, QJsonObject)> callback) const;
std::function<void(QJsonObject, QJsonObject)> 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;
};
@@ -15,6 +15,8 @@
#include <QTemporaryFile>
#include <QTimer>
#include <functional>
#include <memory>
#include <utility>
namespace {
@@ -143,6 +145,15 @@ namespace {
return first && second;
}
bool waitForCondition(const std::function<bool()>& 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<NewPositionRuntime>(
&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;
}