fix(amm): cancel superseded quote work

This commit is contained in:
Ricardo Guilherme Schmidt 2026-07-17 21:51:16 -03:00
parent 942920b51c
commit ec18833e3a
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
8 changed files with 137 additions and 16 deletions

View File

@ -182,7 +182,7 @@ QtObject {
}
root.activeQuoteRequestId = ++root.operationSerial
root.backend.requestNewPositionQuote(
built.request, root.activeQuoteRequestId, true)
built.request, root.activeQuoteRequestId, true, false)
}
function acceptQuoteResult(result) {
@ -299,7 +299,8 @@ QtObject {
const pending = root.pendingPoolProbes[0]
root.poolProbeInFlight = true
root.poolProbeRequestId = ++root.operationSerial
root.backend.requestNewPositionQuote(pending.request, root.poolProbeRequestId, true)
root.backend.requestNewPositionQuote(
pending.request, root.poolProbeRequestId, true, true)
}
function finishPoolProbe(pending, quote) {

View File

@ -164,10 +164,11 @@ void AmmUiBackend::refreshNewPositionContext(QVariantMap request)
void AmmUiBackend::requestNewPositionQuote(QVariantMap request,
int requestId,
bool forceRefresh)
bool forceRefresh,
bool isPoolProbe)
{
m_newPosition->quoteAsync(
request, m_network.snapshot(), isWalletOpen(), forceRefresh,
request, m_network.snapshot(), isWalletOpen(), forceRefresh, isPoolProbe,
[this, requestId](QVariantMap result) {
result.insert(QStringLiteral("requestId"), requestId);
setNewPositionQuoteResult(std::move(result));

View File

@ -47,7 +47,8 @@ public slots:
void refreshNewPositionContext(QVariantMap request) override;
void requestNewPositionQuote(QVariantMap request,
int requestId,
bool forceRefresh) override;
bool forceRefresh,
bool isPoolProbe) override;
void requestNewPositionSubmit(QVariantMap request,
QString quoteHash,
int requestId) override;

View File

@ -36,7 +36,7 @@ class AmmUiBackend
PROP(QVariantMap newPositionQuoteResult READONLY)
PROP(QVariantMap newPositionSubmitResult READONLY)
SLOT(void refreshNewPositionContext(QVariantMap request))
SLOT(void requestNewPositionQuote(QVariantMap request, int requestId, bool forceRefresh))
SLOT(void requestNewPositionQuote(QVariantMap request, int requestId, bool forceRefresh, bool isPoolProbe))
SLOT(void requestNewPositionSubmit(QVariantMap request, QString quoteHash, int requestId))
// Wallet lifecycle. createNewDefault() is the happy path: it creates a

View File

@ -451,8 +451,11 @@ void NewPositionRuntime::buildQuoteInputAsync(
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool forceRefresh,
std::function<bool()> shouldContinue,
std::function<void(QJsonObject, QJsonObject)> callback)
{
if (!shouldContinue())
return;
if (network.status != QStringLiteral("ready")) {
callback({}, publicError(network.status));
return;
@ -472,8 +475,9 @@ void NewPositionRuntime::buildQuoteInputAsync(
QPointer<NewPositionRuntime> guard(this);
m_sequencer->readAccounts({ configId }, forceRefresh,
[guard, request, network, walletOpen, forceRefresh,
shouldContinue,
callback = std::move(callback)](QVector<WalletAccountRead> configReads) mutable {
if (!guard)
if (!guard || !shouldContinue())
return;
const QJsonObject config = accountReadJson(configReads.value(0));
const QJsonObject requestObject = QJsonObject::fromVariantMap(request);
@ -509,9 +513,10 @@ void NewPositionRuntime::buildQuoteInputAsync(
};
guard->m_sequencer->readAccounts(fixedIds, forceRefresh,
[guard, requestObject, network, walletOpen, config, pair,
shouldContinue,
callback = std::move(callback)](
QVector<WalletAccountRead> fixedReads) mutable {
if (!guard)
if (!guard || !shouldContinue())
return;
QStringList selectedIds;
for (const QString& key : {
@ -531,15 +536,19 @@ void NewPositionRuntime::buildQuoteInputAsync(
[guard, requestObject, network, walletOpen, config, pair,
selectedIds = std::move(selectedIds),
fixedReads = std::move(fixedReads),
shouldContinue,
callback = std::move(callback)](
QVector<WalletAccountRead> walletReads) mutable {
if (!guard)
if (!guard || !shouldContinue())
return;
auto finish = [requestObject, network, walletOpen, config,
fixedReads = std::move(fixedReads),
walletReads = std::move(walletReads),
shouldContinue,
callback = std::move(callback)](
QVector<WalletAccountRead> selectedReads) mutable {
if (!shouldContinue())
return;
QHash<QString, WalletAccountRead> walletById;
for (const WalletAccountRead& read : walletReads)
walletById.insert(read.accountId, read);
@ -583,13 +592,23 @@ void NewPositionRuntime::quoteAsync(const QVariantMap& request,
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool forceRefresh,
bool isPoolProbe,
ResultCallback callback)
{
QPointer<NewPositionRuntime> guard(this);
const quint64 quoteGeneration = isPoolProbe
? 0 : ++m_userQuoteGeneration;
std::function<bool()> shouldContinue = [guard, quoteGeneration]() {
return guard
&& (quoteGeneration == 0
|| quoteGeneration == guard->m_userQuoteGeneration);
};
buildQuoteInputAsync(request, network, walletOpen, forceRefresh,
[guard, callback = std::move(callback)](
shouldContinue,
[guard, shouldContinue,
callback = std::move(callback)](
QJsonObject input, QJsonObject error) mutable {
if (!guard)
if (!guard || !shouldContinue())
return;
if (!error.isEmpty()) {
callback(error.toVariantMap());
@ -626,11 +645,17 @@ void NewPositionRuntime::submitAsync(const QVariantMap& request,
const quint64 walletGeneration = m_walletGeneration;
m_submitCallback = std::move(callback);
QPointer<NewPositionRuntime> guard(this);
std::function<bool()> shouldContinue =
[guard, submitGeneration, walletGeneration]() {
return guard
&& guard->submitIsCurrent(submitGeneration, walletGeneration);
};
buildQuoteInputAsync(request, network, true, true,
[guard, quoteHash, submitGeneration, walletGeneration](
shouldContinue,
[guard, quoteHash, submitGeneration, walletGeneration,
shouldContinue](
QJsonObject input, QJsonObject error) mutable {
if (!guard
|| !guard->submitIsCurrent(submitGeneration, walletGeneration))
if (!guard || !shouldContinue())
return;
if (!error.isEmpty()) {
guard->finishSubmit(submitGeneration, error.toVariantMap());

View File

@ -36,6 +36,7 @@ public:
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool forceRefresh,
bool isPoolProbe,
ResultCallback callback);
void submitAsync(const QVariantMap& request,
const QString& quoteHash,
@ -74,6 +75,7 @@ private:
const ActiveNetworkSnapshot& network,
bool walletOpen,
bool forceRefresh,
std::function<bool()> shouldContinue,
std::function<void(QJsonObject, QJsonObject)> callback);
void submitPlanAsync(QJsonObject input,
const QString& quoteHash,
@ -104,6 +106,7 @@ private:
bool m_submitInFlight = false;
quint64 m_walletGeneration = 0;
quint64 m_contextGeneration = 0;
quint64 m_userQuoteGeneration = 0;
quint64 m_submitGeneration = 0;
ResultCallback m_submitCallback;
};

View File

@ -298,6 +298,7 @@ namespace {
AmmClientResult pairIds(const QJsonObject&) const override
{
++pairIdsCalls;
return success({
{ QStringLiteral("status"), QStringLiteral("ok") },
{ QStringLiteral("tokenAId"), QStringLiteral("token-a") },
@ -320,6 +321,7 @@ namespace {
AmmClientResult quote(const QJsonObject&) const override
{
++quoteCalls;
return success({
{ QStringLiteral("schema"), QStringLiteral("new-position.v2") },
{ QStringLiteral("status"), QStringLiteral("ok") },
@ -385,6 +387,8 @@ namespace {
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;
@ -767,6 +771,71 @@ int main(int argc, char** argv)
"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;
@ -828,7 +897,7 @@ int main(int argc, char** argv)
refreshClient.normalizedAccountIds.count(selectedAccountHex);
QVariantMap selectedQuote;
selectedRuntime.quoteAsync(
selectedRequest, readyNetwork(), true, false,
selectedRequest, readyNetwork(), true, false, false,
[&](QVariantMap result) { selectedQuote = std::move(result); });
if (!expect(waitForCondition([&]() { return !selectedQuote.isEmpty(); }),
"selected-holding quote should complete"))

View File

@ -38,6 +38,7 @@ TestCase {
property int contextRefreshCalls: 0
property int contextRequestId: 0
property int quoteCalls: 0
property var quotePoolProbeFlags: []
property int submitCalls: 0
property var lastContextRefreshRequest: ({})
@ -49,8 +50,12 @@ TestCase {
newPositionSubmitResult = result
}
function requestNewPositionQuote(request, requestId, forceRefresh) {
function requestNewPositionQuote(request, requestId, forceRefresh,
isPoolProbe) {
++quoteCalls
quotePoolProbeFlags = quotePoolProbeFlags.concat([
isPoolProbe === true
])
var result = JSON.parse(JSON.stringify(quoteResult || ({})))
result.requestId = requestId
newPositionQuoteResult = result
@ -218,6 +223,22 @@ TestCase {
wait(0)
page.flow.pollPendingPool()
compare(backend.quoteCalls, 1)
compare(backend.quotePoolProbeFlags.length, 1)
compare(backend.quotePoolProbeFlags[0], true)
}
function test_userQuoteUsesInteractiveRequestLane() {
var backend = createTemporaryObject(backendComponent, testCase)
var page = createTemporaryObject(pageComponent, testCase, { "backend": backend })
page.flow.pendingQuoteRequest = {
"ok": true,
"request": ({})
}
page.flow.active = true
page.flow.requestQuoteNow(page.flow.quoteSerial)
verify(backend.quoteCalls > 0)
compare(backend.quotePoolProbeFlags[0], false)
}
function test_activePoolSubmissionDoesNotStartPoolCreationProbe() {