mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-20 05:50:32 +00:00
fix(amm): reuse pending LP accounts
This commit is contained in:
parent
f704ab6a45
commit
d51856a34f
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user