fix(amm): reuse pending LP accounts

This commit is contained in:
Ricardo Guilherme Schmidt 2026-07-17 21:26:47 -03:00
parent f704ab6a45
commit d51856a34f
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
3 changed files with 653 additions and 40 deletions

View File

@ -19,6 +19,7 @@
namespace {
const char SCHEMA[] = "new-position.v2";
constexpr qsizetype HASH_BYTES = 32;
constexpr qsizetype ACCOUNT_ID_HEX_SIZE = HASH_BYTES * 2;
constexpr std::size_t BASE58_BUFFER_SIZE = 45;
QString base58TransactionId(const QString& transactionHash)
@ -53,6 +54,32 @@ namespace {
static_cast<qsizetype>(bytes.size())).toHex());
}
bool isLowerHex(const QString& value, qsizetype size)
{
if (value.size() != size)
return false;
for (qsizetype index = 0; index < value.size(); ++index) {
const char16_t character = value.at(index).unicode();
if (!((character >= u'0' && character <= u'9')
|| (character >= u'a' && character <= u'f'))) {
return false;
}
}
return true;
}
bool isDefaultAccountRead(const WalletAccountRead& read,
const QString& expectedAccountId)
{
return read.ok()
&& read.accountId == expectedAccountId
&& isLowerHex(read.accountId, ACCOUNT_ID_HEX_SIZE)
&& read.programOwner == QString(ACCOUNT_ID_HEX_SIZE, QLatin1Char('0'))
&& read.balanceHex == QString(32, QLatin1Char('0'))
&& read.nonceHex == QString(32, QLatin1Char('0'))
&& read.dataHex.isEmpty();
}
QJsonObject issue(const QString& code,
const QJsonArray& blockingFields = {})
{
@ -172,9 +199,10 @@ NewPositionRuntime::NewPositionRuntime(WalletProvider* wallet,
void NewPositionRuntime::clearWalletAccounts()
{
++m_walletGeneration;
cancelSubmit();
m_walletPublicAccountIds.clear();
clearPendingFreshLp();
m_wallet->clearSnapshot();
cancelSubmit();
}
void NewPositionRuntime::setWalletAccounts(const QVector<WalletAccount>& accounts)
@ -184,11 +212,33 @@ void NewPositionRuntime::setWalletAccounts(const QVector<WalletAccount>& account
if (account.isPublic)
publicAccountIds.append(account.address);
}
publicAccountIds.sort(Qt::CaseSensitive);
if (publicAccountIds != m_walletPublicAccountIds) {
++m_walletGeneration;
if (!m_pendingFreshLpAccountId.isEmpty()
&& !publicAccountIds.contains(m_pendingFreshLpAccountId)
&& m_freshLpState == FreshLpState::Ready) {
clearPendingFreshLp();
}
m_walletPublicAccountIds = std::move(publicAccountIds);
cancelSubmit();
return;
}
m_walletPublicAccountIds = std::move(publicAccountIds);
}
void NewPositionRuntime::rememberPendingFreshLp(const QString& accountId)
{
m_pendingFreshLpAccountId = accountId;
m_freshLpState = FreshLpState::Ready;
if (!m_walletPublicAccountIds.contains(accountId)) {
m_walletPublicAccountIds.append(accountId);
m_walletPublicAccountIds.sort(Qt::CaseSensitive);
}
}
void NewPositionRuntime::clearPendingFreshLp()
{
m_pendingFreshLpAccountId.clear();
m_freshLpState = FreshLpState::None;
}
void NewPositionRuntime::cancelSubmit()
@ -558,6 +608,11 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request,
callback(publicError(QStringLiteral("wallet_syncing")).toVariantMap());
return;
}
if (m_freshLpState == FreshLpState::Creating
|| m_freshLpState == FreshLpState::Submitting) {
callback(publicError(QStringLiteral("submit_in_progress")).toVariantMap());
return;
}
m_submitInFlight = true;
const quint64 submitGeneration = ++m_submitGeneration;
const quint64 walletGeneration = m_walletGeneration;
@ -595,35 +650,132 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request,
}
if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) {
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);
});
guard->prepareFreshLpAsync(
std::move(input), quoteHash,
submitGeneration, walletGeneration);
return;
}
guard->submitPlanAsync(
std::move(input), quoteHash, {},
std::move(input), quoteHash, {}, {},
submitGeneration, walletGeneration);
});
}
void NewPositionRuntime::prepareFreshLpAsync(QJsonObject input,
const QString& quoteHash,
quint64 submitGeneration,
quint64 walletGeneration)
{
if (!submitIsCurrent(submitGeneration, walletGeneration))
return;
if (m_freshLpState == FreshLpState::Ready
&& !m_pendingFreshLpAccountId.isEmpty()) {
validatePendingFreshLpAsync(
std::move(input), quoteHash,
submitGeneration, walletGeneration);
return;
}
if (m_freshLpState != FreshLpState::None) {
finishSubmit(
submitGeneration,
publicError(QStringLiteral("submit_in_progress")).toVariantMap());
return;
}
m_freshLpState = FreshLpState::Creating;
QPointer<NewPositionRuntime> guard(this);
m_wallet->createAccountAsync(
true,
[guard, input = std::move(input), quoteHash,
submitGeneration, walletGeneration](
WalletAccountCreation creation) mutable {
if (!guard || walletGeneration != guard->m_walletGeneration)
return;
const bool reusable = creation.ok()
&& isLowerHex(creation.accountId, ACCOUNT_ID_HEX_SIZE);
if (reusable)
guard->rememberPendingFreshLp(creation.accountId);
else
guard->clearPendingFreshLp();
if (!guard->submitIsCurrent(submitGeneration, walletGeneration))
return;
if (!reusable) {
const QString code = creation.failure
== WalletFailure::WalletUnavailable
? QStringLiteral("wallet_unavailable")
: QStringLiteral("wallet_submission_failed");
guard->finishSubmit(
submitGeneration, publicError(code).toVariantMap());
return;
}
WalletAccountRead read = std::move(creation.publicAccount);
read.accountId = creation.accountId;
if (isDefaultAccountRead(read, creation.accountId)) {
guard->submitPlanAsync(
std::move(input), quoteHash, accountReadJson(read),
creation.accountId, submitGeneration, walletGeneration);
return;
}
guard->validatePendingFreshLpAsync(
std::move(input), quoteHash,
submitGeneration, walletGeneration);
});
}
void NewPositionRuntime::validatePendingFreshLpAsync(
QJsonObject input,
const QString& quoteHash,
quint64 submitGeneration,
quint64 walletGeneration)
{
if (!submitIsCurrent(submitGeneration, walletGeneration))
return;
const QString accountId = m_pendingFreshLpAccountId;
if (m_freshLpState != FreshLpState::Ready
|| accountId.isEmpty()
|| !m_sequencer
|| !m_sequencer->isConfigured()) {
finishSubmit(
submitGeneration,
publicError(QStringLiteral("account_read_failed")).toVariantMap());
return;
}
QPointer<NewPositionRuntime> guard(this);
m_sequencer->readAccounts(
{ accountId }, true,
[guard, input = std::move(input), quoteHash, accountId,
submitGeneration, walletGeneration](
QVector<WalletAccountRead> reads) mutable {
if (!guard
|| !guard->submitIsCurrent(
submitGeneration, walletGeneration)) {
return;
}
if (guard->m_pendingFreshLpAccountId != accountId) {
guard->finishSubmit(
submitGeneration,
publicError(QStringLiteral("wallet_unavailable")).toVariantMap());
return;
}
const WalletAccountRead read = reads.value(0);
if (!read.ok()) {
guard->finishSubmit(
submitGeneration,
publicError(QStringLiteral("account_read_failed")).toVariantMap());
return;
}
if (!isDefaultAccountRead(read, accountId)) {
guard->clearPendingFreshLp();
guard->finishSubmit(
submitGeneration,
publicError(QStringLiteral("submission_status_unknown")).toVariantMap());
return;
}
guard->submitPlanAsync(
std::move(input), quoteHash, accountReadJson(read), accountId,
submitGeneration, walletGeneration);
});
}
@ -631,6 +783,7 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request,
void NewPositionRuntime::submitPlanAsync(QJsonObject input,
const QString& quoteHash,
QJsonValue freshLp,
QString freshLpAccountId,
quint64 submitGeneration,
quint64 walletGeneration)
{
@ -678,18 +831,32 @@ void NewPositionRuntime::submitPlanAsync(QJsonObject input,
jsonBoolList(plan.value(QStringLiteral("signingRequirements")).toArray()),
jsonUIntList(plan.value(QStringLiteral("instruction")).toArray()),
};
if (!freshLpAccountId.isEmpty()
&& freshLpAccountId == m_pendingFreshLpAccountId) {
m_freshLpState = FreshLpState::Submitting;
}
QPointer<NewPositionRuntime> guard(this);
m_wallet->submitPublicTransactionAsync(
transaction,
[guard, submitGeneration, walletGeneration, deadlineMs =
[guard, submitGeneration, walletGeneration,
freshLpAccountId = std::move(freshLpAccountId), deadlineMs =
plan.value(QStringLiteral("deadlineMs")), affectedAccountIds =
plan.value(QStringLiteral("affectedAccountIds"))](
WalletSubmission submission) mutable {
if (!guard
|| !guard->submitIsCurrent(submitGeneration, walletGeneration))
if (!guard)
return;
const QString transactionId = submission.accepted()
? base58TransactionId(submission.nativeHash) : QString();
if (walletGeneration == guard->m_walletGeneration
&& !freshLpAccountId.isEmpty()
&& freshLpAccountId == guard->m_pendingFreshLpAccountId) {
if (submission.accepted())
guard->clearPendingFreshLp();
else
guard->m_freshLpState = FreshLpState::Ready;
}
if (!guard->submitIsCurrent(submitGeneration, walletGeneration))
return;
if (transactionId.isEmpty()) {
const QString code = submission.failure
== WalletFailure::WalletUnavailable
@ -803,6 +970,10 @@ QVariantMap NewPositionRuntime::submit(const QVariantMap& request,
return publicError(QStringLiteral("submit_in_progress")).toVariantMap();
if (!walletOpen)
return publicError(QStringLiteral("wallet_unavailable")).toVariantMap();
if (m_freshLpState == FreshLpState::Creating
|| m_freshLpState == FreshLpState::Submitting) {
return publicError(QStringLiteral("submit_in_progress")).toVariantMap();
}
QScopedValueRollback<bool> submitGuard(m_submitInFlight, true);
QJsonObject error;
@ -826,11 +997,34 @@ QVariantMap NewPositionRuntime::submit(const QVariantMap& request,
}
QJsonValue freshLp;
QString freshLpAccountId;
if (quote.value(QStringLiteral("requiresFreshLp")).toBool(false)) {
const WalletAccountCreation creation = m_wallet->createAccount(true);
if (!creation.ok() || !creation.publicAccount.ok())
return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
freshLp = accountReadJson(creation.publicAccount);
WalletAccountRead read;
if (m_freshLpState == FreshLpState::None) {
m_freshLpState = FreshLpState::Creating;
const WalletAccountCreation creation = m_wallet->createAccount(true);
if (!creation.ok()
|| !isLowerHex(creation.accountId, ACCOUNT_ID_HEX_SIZE)) {
clearPendingFreshLp();
return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
}
rememberPendingFreshLp(creation.accountId);
read = creation.publicAccount;
read.accountId = creation.accountId;
} else if (m_freshLpState == FreshLpState::Ready
&& !m_pendingFreshLpAccountId.isEmpty()) {
read = m_wallet->readPublicAccount(m_pendingFreshLpAccountId);
} else {
return publicError(QStringLiteral("submit_in_progress")).toVariantMap();
}
freshLpAccountId = m_pendingFreshLpAccountId;
if (!read.ok())
return publicError(QStringLiteral("account_read_failed")).toVariantMap();
if (!isDefaultAccountRead(read, freshLpAccountId)) {
clearPendingFreshLp();
return publicError(QStringLiteral("submission_status_unknown")).toVariantMap();
}
freshLp = accountReadJson(read);
}
QJsonObject planInput = input;
@ -859,14 +1053,27 @@ QVariantMap NewPositionRuntime::submit(const QVariantMap& request,
|| static_cast<qulonglong>(QDateTime::currentMSecsSinceEpoch()) >= deadline) {
return publicError(QStringLiteral("transaction_deadline_expired")).toVariantMap();
}
if (!freshLpAccountId.isEmpty()
&& freshLpAccountId == m_pendingFreshLpAccountId) {
m_freshLpState = FreshLpState::Submitting;
}
const WalletSubmission submission = m_wallet->submitPublicTransaction({
programId,
accountIds,
signingRequirements,
instruction,
});
if (!submission.accepted())
if (!submission.accepted()) {
if (!freshLpAccountId.isEmpty()
&& freshLpAccountId == m_pendingFreshLpAccountId) {
m_freshLpState = FreshLpState::Ready;
}
return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();
}
if (!freshLpAccountId.isEmpty()
&& freshLpAccountId == m_pendingFreshLpAccountId) {
clearPendingFreshLp();
}
const QString transactionId = base58TransactionId(submission.nativeHash);
if (transactionId.isEmpty())
return publicError(QStringLiteral("wallet_submission_failed")).toVariantMap();

View File

@ -57,6 +57,13 @@ public:
bool walletOpen);
private:
enum class FreshLpState {
None,
Creating,
Ready,
Submitting,
};
QJsonArray walletAccountReads(bool walletOpen, bool refresh) const;
QJsonObject buildQuoteInput(const QVariantMap& request,
const ActiveNetworkSnapshot& network,
@ -71,8 +78,19 @@ private:
void submitPlanAsync(QJsonObject input,
const QString& quoteHash,
QJsonValue freshLp,
QString freshLpAccountId,
quint64 submitGeneration,
quint64 walletGeneration);
void prepareFreshLpAsync(QJsonObject input,
const QString& quoteHash,
quint64 submitGeneration,
quint64 walletGeneration);
void validatePendingFreshLpAsync(QJsonObject input,
const QString& quoteHash,
quint64 submitGeneration,
quint64 walletGeneration);
void rememberPendingFreshLp(const QString& accountId);
void clearPendingFreshLp();
void finishSubmit(quint64 submitGeneration, QVariantMap result);
bool submitIsCurrent(quint64 submitGeneration,
quint64 walletGeneration) const;
@ -81,6 +99,8 @@ private:
AmmClient* m_client;
SequencerClient* m_sequencer;
QStringList m_walletPublicAccountIds;
QString m_pendingFreshLpAccountId;
FreshLpState m_freshLpState = FreshLpState::None;
bool m_submitInFlight = false;
quint64 m_walletGeneration = 0;
quint64 m_submitGeneration = 0;

View File

@ -193,9 +193,13 @@ namespace {
{
++createdAccounts;
WalletAccountCreation creation;
creation.accountId = QStringLiteral("fresh-lp");
if (isPublic)
const QLatin1Char accountDigit(
"abcdef"[(createdAccounts - 1) % 6]);
creation.accountId = QString(64, accountDigit);
if (isPublic && !failCreatedPublicRead)
creation.publicAccount = readPublicAccount(creation.accountId);
else
creation.publicAccount.accountId = creation.accountId;
return creation;
}
@ -215,6 +219,9 @@ namespace {
WalletAccountRead read;
read.accountId = accountId;
read.status = QStringLiteral("ok");
read.programOwner = QString(64, QLatin1Char('0'));
read.balanceHex = readBalanceHex;
read.nonceHex = QString(32, QLatin1Char('0'));
return read;
}
@ -222,6 +229,10 @@ namespace {
{
++submissions;
submitted = transaction;
if (submissionFailuresRemaining > 0) {
--submissionFailuresRemaining;
return { WalletFailure::SubmissionFailed, {} };
}
return { WalletFailure::None, transactionHash };
}
@ -261,6 +272,9 @@ namespace {
WalletTransaction submitted;
bool deferAccountCreation = false;
bool deferSubmission = false;
bool failCreatedPublicRead = false;
int submissionFailuresRemaining = 0;
QString readBalanceHex = QString(32, QLatin1Char('0'));
AccountCreationCallback pendingAccountCreation;
SubmissionCallback pendingSubmissionCallback;
WalletAccountCreation pendingCreation;
@ -316,6 +330,16 @@ namespace {
AmmClientResult plan(const QJsonObject& request) const override
{
sawFreshLp = request.contains(QStringLiteral("freshLp"));
if (sawFreshLp) {
freshLpAccountIds.append(
request.value(QStringLiteral("freshLp")).toObject()
.value(QStringLiteral("id")).toString());
}
++planCalls;
if (planFailuresRemaining > 0) {
--planFailuresRemaining;
return { false, {} };
}
return success({
{ QStringLiteral("status"), QStringLiteral("ready") },
{ QStringLiteral("accountIds"), QJsonArray { QStringLiteral("account") } },
@ -331,11 +355,21 @@ namespace {
AmmClientResult normalizeAccountRpc(const QJsonObject& request) const override
{
normalizedAccountIds.append(
request.value(QStringLiteral("accountId")).toString());
return success({
{ QStringLiteral("id"),
request.value(QStringLiteral("accountId")).toString() },
{ QStringLiteral("status"), QStringLiteral("ok") },
{ QStringLiteral("account"), QJsonObject() },
{ QStringLiteral("account"), QJsonObject {
{ QStringLiteral("program_owner"),
QString(64, QLatin1Char('0')) },
{ QStringLiteral("balance"),
normalizedBalanceHex },
{ QStringLiteral("nonce"),
QString(32, QLatin1Char('0')) },
{ QStringLiteral("data"), QString() },
} },
});
}
@ -347,6 +381,11 @@ namespace {
QString quoteHash = QStringLiteral("sha256:expected");
bool requiresFreshLp = true;
mutable bool sawFreshLp = false;
mutable int planCalls = 0;
mutable int planFailuresRemaining = 0;
mutable QStringList freshLpAccountIds;
mutable QStringList normalizedAccountIds;
QString normalizedBalanceHex = QString(32, QLatin1Char('0'));
};
ActiveNetworkSnapshot readyNetwork()
@ -442,6 +481,125 @@ int main(int argc, char** argv)
"runtime should dispatch the unchanged plan once"))
return 1;
FakeWallet planRetryWallet;
FakeAmmClient planRetryClient;
planRetryClient.planFailuresRemaining = 1;
NewPositionRuntime planRetryRuntime(&planRetryWallet, &planRetryClient);
const QVariantMap failedPlan = planRetryRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
const QVariantMap retriedPlan = planRetryRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(failedPlan.value(QStringLiteral("code")).toString()
== QStringLiteral("backend_error")
&& retriedPlan.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted"),
"plan failure should remain retryable"))
return 1;
if (!expect(planRetryWallet.createdAccounts == 1
&& planRetryClient.freshLpAccountIds.size() == 2
&& planRetryClient.freshLpAccountIds.constFirst()
== planRetryClient.freshLpAccountIds.constLast(),
"plan retry should reuse the pending LP account"))
return 1;
FakeWallet submissionRetryWallet;
submissionRetryWallet.submissionFailuresRemaining = 1;
FakeAmmClient submissionRetryClient;
NewPositionRuntime submissionRetryRuntime(
&submissionRetryWallet, &submissionRetryClient);
const QVariantMap failedSubmission = submissionRetryRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
const QVariantMap retriedSubmission = submissionRetryRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(failedSubmission.value(QStringLiteral("code")).toString()
== QStringLiteral("wallet_submission_failed")
&& retriedSubmission.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted"),
"wallet rejection should remain retryable"))
return 1;
if (!expect(submissionRetryWallet.createdAccounts == 1
&& submissionRetryWallet.submissions == 2
&& submissionRetryClient.freshLpAccountIds.size() == 2
&& submissionRetryClient.freshLpAccountIds.constFirst()
== submissionRetryClient.freshLpAccountIds.constLast(),
"wallet retry should reuse the pending LP account"))
return 1;
FakeWallet consumedWallet;
FakeAmmClient consumedClient;
NewPositionRuntime consumedRuntime(&consumedWallet, &consumedClient);
const QVariantMap firstConsumed = consumedRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
const QVariantMap secondConsumed = consumedRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(firstConsumed.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& secondConsumed.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& consumedWallet.createdAccounts == 2
&& consumedClient.freshLpAccountIds.size() == 2
&& consumedClient.freshLpAccountIds.constFirst()
!= consumedClient.freshLpAccountIds.constLast(),
"accepted submission should consume the LP account reservation"))
return 1;
FakeWallet changedWallet;
FakeAmmClient changedClient;
changedClient.planFailuresRemaining = 1;
NewPositionRuntime changedRuntime(&changedWallet, &changedClient);
const QVariantMap beforeWalletChange = changedRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
changedRuntime.clearWalletAccounts();
const QVariantMap afterWalletChange = changedRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(beforeWalletChange.value(QStringLiteral("code")).toString()
== QStringLiteral("backend_error")
&& afterWalletChange.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& changedWallet.createdAccounts == 2,
"wallet change should discard the previous LP reservation"))
return 1;
FakeWallet readRetryWallet;
readRetryWallet.failCreatedPublicRead = true;
FakeAmmClient readRetryClient;
NewPositionRuntime readRetryRuntime(&readRetryWallet, &readRetryClient);
const QVariantMap failedCreatedRead = readRetryRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
readRetryWallet.failCreatedPublicRead = false;
const QVariantMap retriedCreatedRead = readRetryRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(failedCreatedRead.value(QStringLiteral("code")).toString()
== QStringLiteral("account_read_failed")
&& retriedCreatedRead.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& readRetryWallet.createdAccounts == 1,
"created-account read failure should preserve the LP reservation"))
return 1;
FakeWallet nonDefaultWallet;
FakeAmmClient nonDefaultClient;
nonDefaultClient.planFailuresRemaining = 1;
NewPositionRuntime nonDefaultRuntime(&nonDefaultWallet, &nonDefaultClient);
const QVariantMap reservedAccount = nonDefaultRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
nonDefaultWallet.readBalanceHex = QString(31, QLatin1Char('0'))
+ QLatin1Char('1');
const QVariantMap nonDefaultAccount = nonDefaultRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
nonDefaultWallet.readBalanceHex = QString(32, QLatin1Char('0'));
const QVariantMap replacementAccount = nonDefaultRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(reservedAccount.value(QStringLiteral("code")).toString()
== QStringLiteral("backend_error")
&& nonDefaultAccount.value(QStringLiteral("code")).toString()
== QStringLiteral("submission_status_unknown")
&& replacementAccount.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& nonDefaultWallet.createdAccounts == 2,
"non-default LP reservation should be replaced"))
return 1;
FakeWallet orderedBytesWallet;
orderedBytesWallet.transactionHash = QStringLiteral(
"0102030405060708090a0b0c0d0e0f10"
@ -800,6 +958,234 @@ int main(int argc, char** argv)
"async wallet completion should finish exactly once"))
return 1;
FakeWallet cancelledCreationWallet;
cancelledCreationWallet.deferAccountCreation = true;
FakeAmmClient cancelledCreationClient;
NewPositionRuntime cancelledCreationRuntime(
&cancelledCreationWallet, &cancelledCreationClient, &sequencer);
QVariantMap cancelledCreationResult;
cancelledCreationRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
cancelledCreationResult = std::move(result);
});
if (!expect(waitForCondition(
[&]() { return cancelledCreationWallet.createdAccounts == 1; }),
"cancelled creation should reach the wallet"))
return 1;
WalletAccount creationDiscoveredAccount;
creationDiscoveredAccount.address = QString(64, QLatin1Char('8'));
creationDiscoveredAccount.isPublic = true;
cancelledCreationRuntime.setWalletAccounts({ creationDiscoveredAccount });
QVariantMap blockedCreationRetry;
cancelledCreationRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
blockedCreationRetry = std::move(result);
});
if (!expect(cancelledCreationResult.value(QStringLiteral("code")).toString()
== QStringLiteral("wallet_unavailable")
&& blockedCreationRetry.value(QStringLiteral("code")).toString()
== QStringLiteral("submit_in_progress"),
"account refresh should wait for active creation to settle"))
return 1;
cancelledCreationWallet.finishAccountCreation();
const int creationReadsBeforeRetry = refreshClient.normalizedAccountIds.count(
QString(64, QLatin1Char('a')));
QVariantMap creationRetryResult;
cancelledCreationRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
creationRetryResult = std::move(result);
});
if (!expect(waitForCondition([&]() {
return !creationRetryResult.isEmpty();
}),
"creation retry should complete"))
return 1;
if (!expect(creationRetryResult.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& cancelledCreationWallet.createdAccounts == 1
&& cancelledCreationClient.freshLpAccountIds.size() == 1
&& refreshClient.normalizedAccountIds.count(
QString(64, QLatin1Char('a')))
> creationReadsBeforeRetry,
"late creation should be reserved for the retry"))
return 1;
FakeWallet failedReadWallet;
failedReadWallet.failCreatedPublicRead = true;
FakeAmmClient failedReadClient;
NewPositionRuntime failedReadRuntime(
&failedReadWallet, &failedReadClient, &sequencer);
const int failedReadNormalizations = refreshClient.normalizedAccountIds.count(
QString(64, QLatin1Char('a')));
QVariantMap failedReadResult;
failedReadRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
failedReadResult = std::move(result);
});
if (!expect(waitForCondition([&]() { return !failedReadResult.isEmpty(); }),
"sequencer fallback should complete the failed wallet read"))
return 1;
if (!expect(failedReadResult.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& failedReadWallet.createdAccounts == 1
&& failedReadWallet.submissions == 1
&& refreshClient.normalizedAccountIds.count(
QString(64, QLatin1Char('a')))
> failedReadNormalizations,
"sequencer should validate a newly created LP account"))
return 1;
FakeWallet cancelledSubmissionWallet;
cancelledSubmissionWallet.deferSubmission = true;
cancelledSubmissionWallet.submissionFailuresRemaining = 1;
FakeAmmClient cancelledSubmissionClient;
NewPositionRuntime cancelledSubmissionRuntime(
&cancelledSubmissionWallet, &cancelledSubmissionClient, &sequencer);
QVariantMap cancelledSubmissionResult;
cancelledSubmissionRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
cancelledSubmissionResult = std::move(result);
});
if (!expect(waitForCondition(
[&]() { return cancelledSubmissionWallet.submissions == 1; }),
"cancelled submission should reach the wallet"))
return 1;
WalletAccount pendingFreshLp;
pendingFreshLp.address = QString(64, QLatin1Char('a'));
pendingFreshLp.isPublic = true;
WalletAccount discoveredAccount;
discoveredAccount.address = QString(64, QLatin1Char('9'));
discoveredAccount.isPublic = true;
cancelledSubmissionRuntime.setWalletAccounts(
{ pendingFreshLp, discoveredAccount });
QVariantMap blockedSubmissionRetry;
cancelledSubmissionRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
blockedSubmissionRetry = std::move(result);
});
if (!expect(cancelledSubmissionResult.value(QStringLiteral("code")).toString()
== QStringLiteral("wallet_unavailable")
&& blockedSubmissionRetry.value(QStringLiteral("code")).toString()
== QStringLiteral("submit_in_progress"),
"retry should wait for cancelled wallet submission to settle"))
return 1;
cancelledSubmissionWallet.finishSubmission();
cancelledSubmissionWallet.deferSubmission = false;
QVariantMap settledSubmissionRetry;
cancelledSubmissionRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
settledSubmissionRetry = std::move(result);
});
if (!expect(waitForCondition([&]() {
return !settledSubmissionRetry.isEmpty();
}),
"settled submission retry should complete"))
return 1;
if (!expect(settledSubmissionRetry.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& cancelledSubmissionWallet.createdAccounts == 1
&& cancelledSubmissionWallet.submissions == 2
&& cancelledSubmissionClient.freshLpAccountIds.size() == 2
&& cancelledSubmissionClient.freshLpAccountIds.constFirst()
== cancelledSubmissionClient.freshLpAccountIds.constLast(),
"rejected late submission should restore the LP reservation"))
return 1;
FakeWallet acceptedLateWallet;
acceptedLateWallet.deferSubmission = true;
FakeAmmClient acceptedLateClient;
NewPositionRuntime acceptedLateRuntime(
&acceptedLateWallet, &acceptedLateClient, &sequencer);
QVariantMap acceptedLateResult;
acceptedLateRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
acceptedLateResult = std::move(result);
});
if (!expect(waitForCondition(
[&]() { return acceptedLateWallet.submissions == 1; }),
"late accepted submission should reach the wallet"))
return 1;
acceptedLateRuntime.cancelSubmit();
QVariantMap acceptedLateBlockedRetry;
acceptedLateRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
acceptedLateBlockedRetry = std::move(result);
});
if (!expect(acceptedLateResult.value(QStringLiteral("code")).toString()
== QStringLiteral("wallet_unavailable")
&& acceptedLateBlockedRetry.value(QStringLiteral("code")).toString()
== QStringLiteral("submit_in_progress"),
"accepted late submission should block retry until settled"))
return 1;
acceptedLateWallet.finishSubmission();
acceptedLateWallet.deferSubmission = false;
QVariantMap acceptedLateRetry;
acceptedLateRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
acceptedLateRetry = std::move(result);
});
if (!expect(waitForCondition([&]() { return !acceptedLateRetry.isEmpty(); }),
"accepted late retry should complete"))
return 1;
if (!expect(acceptedLateRetry.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& acceptedLateWallet.createdAccounts == 2
&& acceptedLateWallet.submissions == 2
&& acceptedLateClient.freshLpAccountIds.size() == 2
&& acceptedLateClient.freshLpAccountIds.constFirst()
!= acceptedLateClient.freshLpAccountIds.constLast(),
"accepted late submission should consume its LP reservation"))
return 1;
FakeWallet changedDuringSubmissionWallet;
changedDuringSubmissionWallet.deferSubmission = true;
FakeAmmClient changedDuringSubmissionClient;
NewPositionRuntime changedDuringSubmissionRuntime(
&changedDuringSubmissionWallet, &changedDuringSubmissionClient, &sequencer);
QVariantMap changedDuringSubmissionResult;
changedDuringSubmissionRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
changedDuringSubmissionResult = std::move(result);
});
if (!expect(waitForCondition([&]() {
return changedDuringSubmissionWallet.submissions == 1;
}),
"wallet-change submission should reach the wallet"))
return 1;
changedDuringSubmissionRuntime.clearWalletAccounts();
if (!expect(changedDuringSubmissionResult.value(
QStringLiteral("code")).toString()
== QStringLiteral("wallet_unavailable"),
"wallet change should cancel deferred submission"))
return 1;
changedDuringSubmissionWallet.finishSubmission();
changedDuringSubmissionWallet.deferSubmission = false;
QVariantMap changedWalletRetry;
changedDuringSubmissionRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
changedWalletRetry = std::move(result);
});
if (!expect(waitForCondition([&]() { return !changedWalletRetry.isEmpty(); }),
"new wallet submission should complete"))
return 1;
if (!expect(changedWalletRetry.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted")
&& changedDuringSubmissionWallet.createdAccounts == 2,
"old wallet callback should not mutate new reservation state"))
return 1;
FakeWallet cancelledMutationWallet;
cancelledMutationWallet.deferAccountCreation = true;
FakeAmmClient cancelledMutationClient;