lez-programs/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp
2026-07-17 21:51:16 -03:00

1406 lines
61 KiB
C++

#include "AmmClient.h"
#include "NewPositionRuntime.h"
#include "SequencerClient.h"
#include "WalletProvider.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QHash>
#include <QHostAddress>
#include <QJsonDocument>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTemporaryFile>
#include <QTimer>
#include <functional>
#include <memory>
#include <utility>
namespace {
bool expect(bool condition, const char* message)
{
if (!condition)
qCritical("%s", message);
return condition;
}
class LocalRpcServer final {
public:
LocalRpcServer()
{
QObject::connect(&m_server, &QTcpServer::newConnection, [&]() {
while (m_server.hasPendingConnections()) {
QTcpSocket* socket = m_server.nextPendingConnection();
QObject::connect(socket, &QTcpSocket::readyRead, socket,
[this, socket]() { process(socket); });
if (socket->bytesAvailable() > 0)
process(socket);
}
});
}
bool listen()
{
return m_server.listen(QHostAddress::LocalHost);
}
QString endpoint() const
{
return QStringLiteral("http://127.0.0.1:%1").arg(m_server.serverPort());
}
int requestCount() const { return m_requestCount; }
QByteArray lastRequest() const
{
return m_lastRequest;
}
void failNextRequest() { ++m_failuresRemaining; }
void holdResponses() { m_holdResponses = true; }
void releaseNextResponse()
{
if (m_heldResponses.isEmpty())
return;
const HeldResponse held = m_heldResponses.takeFirst();
respond(held.socket, held.fail);
}
private:
struct HeldResponse {
QTcpSocket* socket = nullptr;
bool fail = false;
};
static void respond(QTcpSocket* socket, bool fail)
{
if (!socket)
return;
const QByteArray payload = QByteArrayLiteral(
R"({"jsonrpc":"2.0","id":1,"result":null})");
QByteArray response = fail
? QByteArrayLiteral(
"HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nContent-Length: ")
: QByteArrayLiteral(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ");
response += QByteArray::number(payload.size());
response += QByteArrayLiteral("\r\nConnection: close\r\n\r\n");
response += payload;
socket->write(response);
socket->disconnectFromHost();
}
void process(QTcpSocket* socket)
{
QByteArray& request = m_requests[socket];
request.append(socket->readAll());
const qsizetype headerEnd = request.indexOf("\r\n\r\n");
if (headerEnd < 0)
return;
qsizetype contentLength = 0;
for (QByteArray line : request.first(headerEnd).split('\n')) {
line = line.trimmed();
if (line.toLower().startsWith("content-length:")) {
contentLength = line.mid(sizeof("content-length:") - 1)
.trimmed().toLongLong();
}
}
const qsizetype bodyStart = headerEnd + 4;
if (request.size() - bodyStart < contentLength)
return;
++m_requestCount;
m_lastRequest = request;
const bool fail = m_failuresRemaining > 0;
if (fail)
--m_failuresRemaining;
m_requests.remove(socket);
if (m_holdResponses) {
m_heldResponses.append({ socket, fail });
return;
}
respond(socket, fail);
}
QTcpServer m_server;
QHash<QTcpSocket*, QByteArray> m_requests;
QByteArray m_lastRequest;
int m_requestCount = 0;
int m_failuresRemaining = 0;
bool m_holdResponses = false;
QVector<HeldResponse> m_heldResponses;
};
bool waitForRequestCount(const LocalRpcServer& server, int expected)
{
QElapsedTimer timer;
timer.start();
while (server.requestCount() < expected && timer.elapsed() < 3000)
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
return server.requestCount() >= expected;
}
bool waitForCallbacks(const bool& first, const bool& second)
{
QElapsedTimer timer;
timer.start();
while ((!first || !second) && timer.elapsed() < 3000)
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
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
{
return {};
}
void connectAsync(const WalletPaths&, SessionCallback callback) override
{
callback({});
}
WalletCreation createWallet(const WalletPaths&, const QString&) override
{
return {};
}
WalletSnapshot snapshot(bool) override
{
return {};
}
void snapshotAsync(bool, SnapshotCallback callback) override
{
callback({});
}
void clearSnapshot() override {}
WalletAccountCreation createAccount(bool isPublic) override
{
++createdAccounts;
WalletAccountCreation creation;
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;
}
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;
read.accountId = accountId;
read.status = QStringLiteral("ok");
read.programOwner = QString(64, QLatin1Char('0'));
read.balanceHex = readBalanceHex;
read.nonceHex = QString(32, QLatin1Char('0'));
return read;
}
WalletSubmission submitPublicTransaction(const WalletTransaction& transaction) override
{
++submissions;
submitted = transaction;
if (submissionFailuresRemaining > 0) {
--submissionFailuresRemaining;
return { WalletFailure::SubmissionFailed, {} };
}
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(
"000102030405060708090a0b0c0d0e0f"
"101112131415161718191a1b1c1d1e1f");
int createdAccounts = 0;
int submissions = 0;
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;
WalletSubmission pendingSubmission;
};
class FakeAmmClient final : public AmmClient {
public:
AmmClientResult configId(const QJsonObject&) const override
{
return success({
{ QStringLiteral("configId"), QString(64, QLatin1Char('1')) },
});
}
AmmClientResult tokenIds(const QJsonObject&) const override
{
++tokenIdsCalls;
return success({ { QStringLiteral("status"), QStringLiteral("ok") } });
}
AmmClientResult pairIds(const QJsonObject&) const override
{
++pairIdsCalls;
return success({
{ QStringLiteral("status"), QStringLiteral("ok") },
{ QStringLiteral("tokenAId"), QStringLiteral("token-a") },
{ QStringLiteral("tokenBId"), QStringLiteral("token-b") },
{ QStringLiteral("poolId"), QStringLiteral("pool") },
{ QStringLiteral("vaultAId"), QStringLiteral("vault-a") },
{ QStringLiteral("vaultBId"), QStringLiteral("vault-b") },
{ QStringLiteral("lpDefinitionId"), QStringLiteral("lp") },
{ QStringLiteral("lpLockHoldingId"), QStringLiteral("lp-lock") },
{ QStringLiteral("currentTickId"), QStringLiteral("tick") },
{ QStringLiteral("clockId"), QStringLiteral("clock") },
});
}
AmmClientResult context(const QJsonObject&) const override
{
++contextCalls;
return success({});
}
AmmClientResult quote(const QJsonObject&) const override
{
++quoteCalls;
return success({
{ QStringLiteral("schema"), QStringLiteral("new-position.v2") },
{ QStringLiteral("status"), QStringLiteral("ok") },
{ QStringLiteral("canSubmit"), true },
{ QStringLiteral("quoteHash"), quoteHash },
{ QStringLiteral("requiresFreshLp"), requiresFreshLp },
});
}
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") } },
{ QStringLiteral("affectedAccountIds"),
QJsonArray { QStringLiteral("account") } },
{ QStringLiteral("signingRequirements"), QJsonArray { true } },
{ QStringLiteral("instruction"), QJsonArray { 1 } },
{ QStringLiteral("programId"), QStringLiteral("program") },
{ QStringLiteral("deadlineMs"),
QString::number(QDateTime::currentMSecsSinceEpoch() + 60'000) },
});
}
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("program_owner"),
QString(64, QLatin1Char('0')) },
{ QStringLiteral("balance"),
normalizedBalanceHex },
{ QStringLiteral("nonce"),
QString(32, QLatin1Char('0')) },
{ QStringLiteral("data"), QString() },
} },
});
}
static AmmClientResult success(const QJsonObject& value)
{
return { true, value };
}
QString quoteHash = QStringLiteral("sha256:expected");
bool requiresFreshLp = true;
mutable bool sawFreshLp = false;
mutable int tokenIdsCalls = 0;
mutable int contextCalls = 0;
mutable int pairIdsCalls = 0;
mutable int quoteCalls = 0;
mutable int planCalls = 0;
mutable int planFailuresRemaining = 0;
mutable QStringList freshLpAccountIds;
mutable QStringList normalizedAccountIds;
QString normalizedBalanceHex = QString(32, QLatin1Char('0'));
};
ActiveNetworkSnapshot readyNetwork()
{
return {
QStringLiteral("testnet"),
QStringLiteral("ready"),
QStringLiteral("block10:identity"),
QStringLiteral("program"),
{},
};
}
bool waitForContext(NewPositionRuntime& runtime, bool forceRefresh)
{
bool completed = false;
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
runtime.contextAsync({}, readyNetwork(), true, forceRefresh,
[&](QVariantMap) {
completed = true;
loop.quit();
});
timeout.start(3000);
if (!completed)
loop.exec();
return completed;
}
bool waitForAccounts(SequencerClient& sequencer,
const QStringList& accountIds,
bool forceRefresh,
QVector<WalletAccountRead>* reads)
{
bool completed = false;
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
sequencer.readAccounts(accountIds, forceRefresh,
[&](QVector<WalletAccountRead> result) {
*reads = std::move(result);
completed = true;
loop.quit();
});
timeout.start(3000);
if (!completed)
loop.exec();
return completed;
}
}
int main(int argc, char** argv)
{
QCoreApplication application(argc, argv);
const QVariantMap request {
{ QStringLiteral("schema"), QStringLiteral("new-position.v2") },
{ QStringLiteral("tokenAId"), QStringLiteral("token-a") },
{ QStringLiteral("tokenBId"), QStringLiteral("token-b") },
{ QStringLiteral("feeBps"), 30 },
};
FakeWallet wallet;
FakeAmmClient client;
NewPositionRuntime runtime(&wallet, &client);
const QVariantMap result = runtime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(result.value(QStringLiteral("status")).toString()
== QStringLiteral("submitted"),
"valid plan should submit"))
return 1;
if (!expect(result.value(QStringLiteral("transactionId")).toString()
== QStringLiteral("1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"),
"native hash should become the expected base58 transaction ID"))
return 1;
if (!expect(result.value(QStringLiteral("nativeTransactionHash")).toString()
== wallet.transactionHash,
"submitted result should preserve the native hash for polling"))
return 1;
if (!expect(wallet.createdAccounts == 1 && client.sawFreshLp,
"fresh LP account should enter the plan"))
return 1;
if (!expect(wallet.submissions == 1
&& wallet.submitted.accountIds
== QStringList { QStringLiteral("account") }
&& wallet.submitted.signingRequirements.size() == 1
&& wallet.submitted.signingRequirements.constFirst()
&& wallet.submitted.instruction.size() == 1
&& wallet.submitted.instruction.constFirst() == 1
&& wallet.submitted.programId == QStringLiteral("program"),
"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"
"1112131415161718191a1b1c1d1e1f20");
FakeAmmClient orderedBytesClient;
orderedBytesClient.requiresFreshLp = false;
NewPositionRuntime orderedBytesRuntime(&orderedBytesWallet, &orderedBytesClient);
const QVariantMap orderedBytes = orderedBytesRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(orderedBytes.value(QStringLiteral("transactionId")).toString()
== QStringLiteral("4wBqpZM9xaSheZzJSMawUKKwhdpChKbZ5eu5ky4Vigw"),
"ordered native hash bytes should preserve byte order"))
return 1;
FakeWallet invalidHashWallet;
invalidHashWallet.transactionHash = QStringLiteral("not-a-hash");
FakeAmmClient invalidHashClient;
invalidHashClient.requiresFreshLp = false;
NewPositionRuntime invalidHashRuntime(&invalidHashWallet, &invalidHashClient);
const QVariantMap invalidHash = invalidHashRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(invalidHash.value(QStringLiteral("code")).toString()
== QStringLiteral("wallet_submission_failed")
&& !invalidHash.contains(QStringLiteral("transactionId")),
"invalid native hash should fail without a hex fallback"))
return 1;
if (!expect(invalidHashWallet.submissions == 1,
"hash conversion should happen after wallet submission"))
return 1;
FakeWallet staleWallet;
FakeAmmClient staleClient;
staleClient.quoteHash = QStringLiteral("sha256:changed");
NewPositionRuntime staleRuntime(&staleWallet, &staleClient);
const QVariantMap stale = staleRuntime.submit(
request, QStringLiteral("sha256:expected"), readyNetwork(), true);
if (!expect(stale.value(QStringLiteral("code")).toString()
== QStringLiteral("quote_changed"),
"changed quote should stop submission"))
return 1;
if (!expect(staleWallet.createdAccounts == 0 && staleWallet.submissions == 0,
"stale quote should have no wallet side effects"))
return 1;
LocalRpcServer configuredEndpointServer;
LocalRpcServer adoptedEndpointServer;
if (!expect(configuredEndpointServer.listen()
&& adoptedEndpointServer.listen(),
"endpoint-alignment sequencers should listen"))
return 1;
QTemporaryFile endpointConfig;
if (!expect(endpointConfig.open(), "endpoint-alignment config should open"))
return 1;
endpointConfig.write(QJsonDocument(QJsonObject {
{ QStringLiteral("sequencer_addr"), configuredEndpointServer.endpoint() },
{ QStringLiteral("basic_auth"), QJsonObject {
{ QStringLiteral("username"), QStringLiteral("user") },
{ QStringLiteral("password"), QStringLiteral("pass") },
} },
}).toJson(QJsonDocument::Compact));
endpointConfig.flush();
FakeAmmClient endpointClient;
SequencerClient alignedSequencer(&endpointClient);
if (!expect(alignedSequencer.configure(
endpointConfig.fileName(), adoptedEndpointServer.endpoint()),
"adopted wallet endpoint should configure"))
return 1;
if (!expect(alignedSequencer.endpoint() == adoptedEndpointServer.endpoint(),
"adopted wallet endpoint should override stale config"))
return 1;
QVector<WalletAccountRead> adoptedEndpointReads;
const QString endpointAccount(64, QLatin1Char('3'));
if (!expect(waitForAccounts(
alignedSequencer, { endpointAccount }, false,
&adoptedEndpointReads),
"adopted endpoint read should complete"))
return 1;
if (!expect(configuredEndpointServer.requestCount() == 0
&& adoptedEndpointServer.requestCount() == 1,
"direct reads should follow adopted wallet endpoint"))
return 1;
if (!expect(!adoptedEndpointServer.lastRequest().toLower().contains(
"authorization:"),
"stale config credentials must not reach adopted endpoint"))
return 1;
if (!expect(alignedSequencer.configure(
endpointConfig.fileName(), configuredEndpointServer.endpoint()),
"matching wallet endpoint should configure"))
return 1;
QVector<WalletAccountRead> configuredEndpointReads;
if (!expect(waitForAccounts(
alignedSequencer, { endpointAccount }, false,
&configuredEndpointReads),
"matching endpoint read should complete"))
return 1;
if (!expect(configuredEndpointServer.requestCount() == 1
&& configuredEndpointServer.lastRequest().toLower().contains(
"authorization: basic dxnlcjpwyxnz"),
"matching endpoint should retain configured credentials"))
return 1;
LocalRpcServer staleContextServer;
if (!expect(staleContextServer.listen(),
"stale-context sequencer should listen"))
return 1;
staleContextServer.holdResponses();
QTemporaryFile staleContextConfig;
if (!expect(staleContextConfig.open(),
"stale-context config should open"))
return 1;
staleContextConfig.write(QJsonDocument(QJsonObject {
{ QStringLiteral("sequencer_addr"), staleContextServer.endpoint() },
}).toJson(QJsonDocument::Compact));
staleContextConfig.flush();
FakeWallet staleContextWallet;
FakeAmmClient staleContextClient;
SequencerClient staleContextSequencer(&staleContextClient);
if (!expect(staleContextSequencer.configure(staleContextConfig.fileName()),
"stale-context sequencer should configure"))
return 1;
NewPositionRuntime staleContextRuntime(
&staleContextWallet, &staleContextClient, &staleContextSequencer);
WalletAccount staleContextHolding;
staleContextHolding.address = QString(64, QLatin1Char('4'));
staleContextHolding.isPublic = true;
staleContextRuntime.setWalletAccounts({ staleContextHolding });
int staleContextCallbacks = 0;
int latestContextCallbacks = 0;
staleContextRuntime.contextAsync(
{}, readyNetwork(), true, false,
[&](QVariantMap) { ++staleContextCallbacks; });
if (!expect(waitForRequestCount(staleContextServer, 1),
"first context should begin the config read"))
return 1;
staleContextRuntime.contextAsync(
{}, readyNetwork(), true, false,
[&](QVariantMap) { ++latestContextCallbacks; });
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
if (!expect(staleContextServer.requestCount() == 1,
"superseding context should share the active config read"))
return 1;
staleContextServer.releaseNextResponse();
if (!expect(waitForRequestCount(staleContextServer, 2),
"latest context should continue with the wallet read"))
return 1;
if (!expect(staleContextCallbacks == 0
&& latestContextCallbacks == 0
&& staleContextClient.tokenIdsCalls == 0
&& staleContextClient.contextCalls == 0,
"context should wait for the latest wallet read"))
return 1;
staleContextServer.releaseNextResponse();
if (!expect(waitForCondition([&]() { return latestContextCallbacks == 1; }),
"latest context should complete"))
return 1;
if (!expect(staleContextCallbacks == 0
&& staleContextClient.tokenIdsCalls == 1
&& staleContextClient.contextCalls == 1
&& staleContextServer.requestCount() == 2,
"superseded context should stop before downstream work"))
return 1;
LocalRpcServer staleQuoteServer;
if (!expect(staleQuoteServer.listen(),
"stale-quote sequencer should listen"))
return 1;
staleQuoteServer.holdResponses();
QTemporaryFile staleQuoteConfig;
if (!expect(staleQuoteConfig.open(),
"stale-quote config should open"))
return 1;
staleQuoteConfig.write(QJsonDocument(QJsonObject {
{ QStringLiteral("sequencer_addr"), staleQuoteServer.endpoint() },
}).toJson(QJsonDocument::Compact));
staleQuoteConfig.flush();
FakeWallet staleQuoteWallet;
FakeAmmClient staleQuoteClient;
SequencerClient staleQuoteSequencer(&staleQuoteClient);
if (!expect(staleQuoteSequencer.configure(staleQuoteConfig.fileName()),
"stale-quote sequencer should configure"))
return 1;
NewPositionRuntime staleQuoteRuntime(
&staleQuoteWallet, &staleQuoteClient, &staleQuoteSequencer);
int staleQuoteCallbacks = 0;
int latestQuoteCallbacks = 0;
staleQuoteRuntime.quoteAsync(
request, readyNetwork(), true, false, false,
[&](QVariantMap) { ++staleQuoteCallbacks; });
if (!expect(waitForRequestCount(staleQuoteServer, 1),
"first quote should begin the config read"))
return 1;
staleQuoteRuntime.quoteAsync(
request, readyNetwork(), true, false, false,
[&](QVariantMap) { ++latestQuoteCallbacks; });
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
if (!expect(staleQuoteServer.requestCount() == 1,
"superseding quote should share the active config read"))
return 1;
staleQuoteServer.releaseNextResponse();
if (!expect(waitForCondition([&]() { return latestQuoteCallbacks == 1; }),
"latest quote should complete"))
return 1;
if (!expect(staleQuoteCallbacks == 0
&& staleQuoteClient.pairIdsCalls == 1
&& staleQuoteClient.quoteCalls == 1,
"superseded user quote should stop before pair work"))
return 1;
int poolProbeCallbacks = 0;
int concurrentUserCallbacks = 0;
staleQuoteRuntime.quoteAsync(
request, readyNetwork(), true, false, true,
[&](QVariantMap) { ++poolProbeCallbacks; });
staleQuoteRuntime.quoteAsync(
request, readyNetwork(), true, false, false,
[&](QVariantMap) { ++concurrentUserCallbacks; });
if (!expect(waitForCondition([&]() {
return poolProbeCallbacks == 1
&& concurrentUserCallbacks == 1;
}),
"pool probe and user quote should complete independently"))
return 1;
if (!expect(staleQuoteClient.pairIdsCalls == 3
&& staleQuoteClient.quoteCalls == 3,
"user quote cancellation must not cancel the pool probe"))
return 1;
LocalRpcServer server;
if (!expect(server.listen(), "local sequencer should listen"))
return 1;
QTemporaryFile sequencerConfig;
if (!expect(sequencerConfig.open(), "sequencer config should open"))
return 1;
sequencerConfig.write(QJsonDocument(QJsonObject {
{ QStringLiteral("sequencer_addr"), server.endpoint() },
}).toJson(QJsonDocument::Compact));
sequencerConfig.flush();
FakeWallet refreshWallet;
FakeAmmClient refreshClient;
SequencerClient sequencer(&refreshClient);
if (!expect(sequencer.configure(sequencerConfig.fileName()),
"sequencer client should configure"))
return 1;
NewPositionRuntime refreshRuntime(&refreshWallet, &refreshClient, &sequencer);
WalletAccount holding;
holding.address = QString(64, QLatin1Char('2'));
holding.isPublic = true;
refreshRuntime.setWalletAccounts({ holding });
if (!expect(waitForContext(refreshRuntime, false),
"initial context should complete"))
return 1;
if (!expect(server.requestCount() == 2,
"initial context should read config and wallet holding"))
return 1;
if (!expect(waitForContext(refreshRuntime, false),
"cached context should complete"))
return 1;
if (!expect(server.requestCount() == 2,
"cached context should not reread accounts"))
return 1;
if (!expect(waitForContext(refreshRuntime, true),
"forced context should complete"))
return 1;
if (!expect(server.requestCount() == 4,
"forced context should reread config and wallet holding"))
return 1;
FakeWallet selectedWallet;
NewPositionRuntime selectedRuntime(
&selectedWallet, &refreshClient, &sequencer);
const QString selectedAccountHex = QStringLiteral(
"000102030405060708090a0b0c0d0e0f"
"101112131415161718191a1b1c1d1e1f");
WalletAccount selectedAccount;
selectedAccount.address = selectedAccountHex;
selectedAccount.isPublic = true;
selectedRuntime.setWalletAccounts({ selectedAccount });
QVariantMap selectedRequest = request;
selectedRequest.insert(
QStringLiteral("holdingAId"),
QStringLiteral("1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"));
const int requestsBeforeSelectedQuote = server.requestCount();
const int normalizationsBeforeSelectedQuote =
refreshClient.normalizedAccountIds.count(selectedAccountHex);
QVariantMap selectedQuote;
selectedRuntime.quoteAsync(
selectedRequest, readyNetwork(), true, false, false,
[&](QVariantMap result) { selectedQuote = std::move(result); });
if (!expect(waitForCondition([&]() { return !selectedQuote.isEmpty(); }),
"selected-holding quote should complete"))
return 1;
if (!expect(server.requestCount() == requestsBeforeSelectedQuote + 1
&& refreshClient.normalizedAccountIds.count(selectedAccountHex)
== normalizationsBeforeSelectedQuote + 1,
"selected holding should be read once per cold quote"))
return 1;
server.failNextRequest();
QVector<WalletAccountRead> failedRefresh;
if (!expect(waitForAccounts(sequencer, { holding.address }, true,
&failedRefresh),
"failed forced holding refresh should complete"))
return 1;
if (!expect(failedRefresh.size() == 1 && !failedRefresh.constFirst().ok(),
"forced holding refresh should surface the sequencer failure"))
return 1;
const int requestsAfterFailure = server.requestCount();
QVector<WalletAccountRead> recoveredRead;
if (!expect(waitForAccounts(sequencer, { holding.address }, false,
&recoveredRead),
"holding read after failure should complete"))
return 1;
if (!expect(server.requestCount() == requestsAfterFailure + 1,
"failed forced refresh should evict the stale holding cache"))
return 1;
if (!expect(recoveredRead.size() == 1 && recoveredRead.constFirst().ok(),
"holding read should recover from the sequencer"))
return 1;
const int requestsBeforeUnchangedConfig = server.requestCount();
if (!expect(sequencer.configure(sequencerConfig.fileName()),
"unchanged sequencer config should remain valid"))
return 1;
QVector<WalletAccountRead> unchangedConfigRead;
if (!expect(waitForAccounts(sequencer, { holding.address }, false,
&unchangedConfigRead),
"cached read after unchanged config should complete"))
return 1;
if (!expect(server.requestCount() == requestsBeforeUnchangedConfig,
"unchanged config should preserve the account cache"))
return 1;
LocalRpcServer replacementServer;
if (!expect(replacementServer.listen(),
"replacement sequencer should listen"))
return 1;
if (!expect(sequencerConfig.resize(0) && sequencerConfig.seek(0),
"sequencer config should rewind"))
return 1;
sequencerConfig.write(QJsonDocument(QJsonObject {
{ QStringLiteral("sequencer_addr"), replacementServer.endpoint() },
}).toJson(QJsonDocument::Compact));
sequencerConfig.flush();
if (!expect(sequencer.configure(sequencerConfig.fileName()),
"same-path endpoint update should configure"))
return 1;
QVector<WalletAccountRead> replacementRead;
if (!expect(waitForAccounts(sequencer, { holding.address }, false,
&replacementRead),
"same-path endpoint read should complete"))
return 1;
if (!expect(replacementServer.requestCount() == 1,
"same-path endpoint update should clear cache and use new sequencer"))
return 1;
LocalRpcServer forcedRefreshServer;
if (!expect(forcedRefreshServer.listen(),
"forced-refresh sequencer should listen"))
return 1;
forcedRefreshServer.holdResponses();
bool staleCachedCompleted = false;
QVector<WalletAccountRead> staleCachedRead;
sequencer.readAccounts({ holding.address }, false,
[&](QVector<WalletAccountRead> reads) {
staleCachedRead = std::move(reads);
staleCachedCompleted = true;
});
if (!expect(sequencerConfig.resize(0) && sequencerConfig.seek(0),
"forced-refresh config should rewind"))
return 1;
sequencerConfig.write(QJsonDocument(QJsonObject {
{ QStringLiteral("sequencer_addr"), forcedRefreshServer.endpoint() },
}).toJson(QJsonDocument::Compact));
sequencerConfig.flush();
if (!expect(sequencer.configure(sequencerConfig.fileName()),
"forced-refresh sequencer should configure"))
return 1;
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
if (!expect(staleCachedCompleted
&& staleCachedRead.size() == 1
&& !staleCachedRead.constFirst().ok(),
"cached read should fail after sequencer reconfiguration"))
return 1;
bool ordinaryCompleted = false;
bool forcedCompleted = false;
sequencer.readAccounts({ holding.address }, false,
[&](QVector<WalletAccountRead>) { ordinaryCompleted = true; });
if (!expect(waitForRequestCount(forcedRefreshServer, 1),
"ordinary read should reach sequencer"))
return 1;
sequencer.readAccounts({ holding.address }, true,
[&](QVector<WalletAccountRead>) { forcedCompleted = true; });
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
if (!expect(forcedRefreshServer.requestCount() == 1,
"forced refresh should wait for active account read"))
return 1;
forcedRefreshServer.releaseNextResponse();
if (!expect(waitForRequestCount(forcedRefreshServer, 2),
"forced refresh should issue a follow-up account read"))
return 1;
if (!expect(ordinaryCompleted && !forcedCompleted,
"pre-refresh result must not complete forced callback"))
return 1;
forcedRefreshServer.releaseNextResponse();
if (!expect(waitForCallbacks(ordinaryCompleted, forcedCompleted),
"forced follow-up read should complete"))
return 1;
LocalRpcServer cancelledSubmitServer;
if (!expect(cancelledSubmitServer.listen(),
"cancelled-submit sequencer should listen"))
return 1;
cancelledSubmitServer.holdResponses();
if (!expect(sequencerConfig.resize(0) && sequencerConfig.seek(0),
"cancelled-submit config should rewind"))
return 1;
sequencerConfig.write(QJsonDocument(QJsonObject {
{ QStringLiteral("sequencer_addr"), cancelledSubmitServer.endpoint() },
}).toJson(QJsonDocument::Compact));
sequencerConfig.flush();
if (!expect(sequencer.configure(sequencerConfig.fileName()),
"cancelled-submit sequencer should configure"))
return 1;
FakeWallet cancelledWallet;
FakeAmmClient cancelledClient;
NewPositionRuntime cancelledRuntime(
&cancelledWallet, &cancelledClient, &sequencer);
bool cancelledCompleted = false;
QVariantMap cancelledResult;
cancelledRuntime.submitAsync(
request, QStringLiteral("sha256:expected"), readyNetwork(), true,
[&](QVariantMap result) {
cancelledResult = std::move(result);
cancelledCompleted = true;
});
if (!expect(waitForRequestCount(cancelledSubmitServer, 1),
"submit should begin account reads"))
return 1;
cancelledRuntime.clearWalletAccounts();
cancelledRuntime.setWalletAccounts({});
cancelledSubmitServer.releaseNextResponse();
if (!expect(waitForCallbacks(cancelledCompleted, cancelledCompleted),
"cancelled submit should complete"))
return 1;
if (!expect(cancelledResult.value(QStringLiteral("code")).toString()
== QStringLiteral("wallet_unavailable")
&& cancelledWallet.createdAccounts == 0
&& cancelledWallet.submissions == 0,
"wallet change should cancel submit before side effects"))
return 1;
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 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;
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;
}