diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index bd4f4c2..8282849 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -1548,9 +1548,11 @@ AmmActionCard { function submissionSnapshot() { var built = root.buildQuoteRequest() + var poolProbe = root.poolProbeRequest(built.request) + poolProbe.poolId = String(root.quotePayload.poolId || "") return { "request": built.request, - "poolProbeRequest": root.poolProbeRequest(built.request), + "poolProbeRequest": poolProbe, "quoteHash": String(root.quotePayload.quoteHash || ""), "pairText": qsTr("%1 / %2").arg(root.shortTokenName(root.tokenA)).arg(root.shortTokenName(root.tokenB)), "feeText": root.feeLabel(root.selectedFeeBps), diff --git a/apps/amm/src/NewPositionRuntime.cpp b/apps/amm/src/NewPositionRuntime.cpp index 04b722a..af15959 100644 --- a/apps/amm/src/NewPositionRuntime.cpp +++ b/apps/amm/src/NewPositionRuntime.cpp @@ -588,21 +588,15 @@ void NewPositionRuntime::buildQuoteInputAsync( }); } -void NewPositionRuntime::quoteAsync(const QVariantMap& request, - const ActiveNetworkSnapshot& network, - bool walletOpen, - bool forceRefresh, - bool isPoolProbe, - ResultCallback callback) +void NewPositionRuntime::quoteFromAccountsAsync( + const QVariantMap& request, + const ActiveNetworkSnapshot& network, + bool walletOpen, + bool forceRefresh, + std::function shouldContinue, + ResultCallback callback) { QPointer guard(this); - const quint64 quoteGeneration = isPoolProbe - ? 0 : ++m_userQuoteGeneration; - std::function shouldContinue = [guard, quoteGeneration]() { - return guard - && (quoteGeneration == 0 - || quoteGeneration == guard->m_userQuoteGeneration); - }; buildQuoteInputAsync(request, network, walletOpen, forceRefresh, shouldContinue, [guard, shouldContinue, @@ -621,6 +615,67 @@ void NewPositionRuntime::quoteAsync(const QVariantMap& request, }); } +void NewPositionRuntime::quoteAsync(const QVariantMap& request, + const ActiveNetworkSnapshot& network, + bool walletOpen, + bool forceRefresh, + bool isPoolProbe, + ResultCallback callback) +{ + QPointer guard(this); + const quint64 quoteGeneration = isPoolProbe + ? 0 : ++m_userQuoteGeneration; + std::function shouldContinue = [guard, quoteGeneration]() { + return guard + && (quoteGeneration == 0 + || quoteGeneration == guard->m_userQuoteGeneration); + }; + const QString poolId = accountIdHex( + request.value(QStringLiteral("poolId")).toString()); + if (!isPoolProbe || poolId.isEmpty() + || network.status != QStringLiteral("ready") + || !m_sequencer || !m_sequencer->isConfigured()) { + quoteFromAccountsAsync( + request, network, walletOpen, forceRefresh, + std::move(shouldContinue), std::move(callback)); + return; + } + + m_sequencer->readAccounts( + { poolId }, true, + [guard, request, network, walletOpen, forceRefresh, poolId, + shouldContinue = std::move(shouldContinue), + callback = std::move(callback)]( + QVector reads) mutable { + if (!guard || !shouldContinue()) + return; + const WalletAccountRead read = reads.value(0); + if (!read.ok()) { + callback(publicError( + QStringLiteral("account_read_failed")).toVariantMap()); + return; + } + if (isDefaultAccountRead(read, poolId)) { + callback(QJsonObject { + { QStringLiteral("schema"), QString::fromLatin1(SCHEMA) }, + { QStringLiteral("status"), QStringLiteral("ok") }, + { QStringLiteral("canSubmit"), false }, + { QStringLiteral("poolStatus"), QStringLiteral("missing_pool") }, + { QStringLiteral("poolId"), + request.value(QStringLiteral("poolId")).toString() }, + { QStringLiteral("tokenAId"), + request.value(QStringLiteral("tokenAId")).toString() }, + { QStringLiteral("tokenBId"), + request.value(QStringLiteral("tokenBId")).toString() }, + }.toVariantMap()); + return; + } + guard->quoteFromAccountsAsync( + request, network, walletOpen, forceRefresh, + std::move(shouldContinue), std::move(callback)); + }); +} + void NewPositionRuntime::submitAsync(const QVariantMap& request, const QString& quoteHash, const ActiveNetworkSnapshot& network, diff --git a/apps/amm/src/NewPositionRuntime.h b/apps/amm/src/NewPositionRuntime.h index 4637f08..75914c5 100644 --- a/apps/amm/src/NewPositionRuntime.h +++ b/apps/amm/src/NewPositionRuntime.h @@ -77,6 +77,12 @@ private: bool forceRefresh, std::function shouldContinue, std::function callback); + void quoteFromAccountsAsync(const QVariantMap& request, + const ActiveNetworkSnapshot& network, + bool walletOpen, + bool forceRefresh, + std::function shouldContinue, + ResultCallback callback); void submitPlanAsync(QJsonObject input, const QString& quoteHash, QJsonValue freshLp, diff --git a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp index c56735e..a4ae158 100644 --- a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp +++ b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp @@ -326,6 +326,7 @@ namespace { { QStringLiteral("schema"), QStringLiteral("new-position.v2") }, { QStringLiteral("status"), QStringLiteral("ok") }, { QStringLiteral("canSubmit"), true }, + { QStringLiteral("poolStatus"), quotePoolStatus }, { QStringLiteral("quoteHash"), quoteHash }, { QStringLiteral("requiresFreshLp"), requiresFreshLp }, }); @@ -383,6 +384,7 @@ namespace { } QString quoteHash = QStringLiteral("sha256:expected"); + QString quotePoolStatus = QStringLiteral("active_pool"); bool requiresFreshLp = true; mutable bool sawFreshLp = false; mutable int tokenIdsCalls = 0; @@ -836,6 +838,81 @@ int main(int argc, char** argv) "user quote cancellation must not cancel the pool probe")) return 1; + LocalRpcServer poolProbeServer; + if (!expect(poolProbeServer.listen(), + "pool-probe sequencer should listen")) + return 1; + QTemporaryFile poolProbeConfig; + if (!expect(poolProbeConfig.open(), + "pool-probe config should open")) + return 1; + poolProbeConfig.write(QJsonDocument(QJsonObject { + { QStringLiteral("sequencer_addr"), poolProbeServer.endpoint() }, + }).toJson(QJsonDocument::Compact)); + poolProbeConfig.flush(); + FakeWallet poolProbeWallet; + FakeAmmClient poolProbeClient; + SequencerClient poolProbeSequencer(&poolProbeClient); + if (!expect(poolProbeSequencer.configure(poolProbeConfig.fileName()), + "pool-probe sequencer should configure")) + return 1; + NewPositionRuntime poolProbeRuntime( + &poolProbeWallet, &poolProbeClient, &poolProbeSequencer); + const QString poolIdBase58 = QStringLiteral( + "1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"); + const QString poolIdHex = QStringLiteral( + "000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f"); + QVariantMap poolProbeRequest = request; + poolProbeRequest.insert(QStringLiteral("poolId"), poolIdBase58); + QVariantMap poolProbeResult; + poolProbeRuntime.quoteAsync( + poolProbeRequest, readyNetwork(), true, true, true, + [&](QVariantMap result) { poolProbeResult = std::move(result); }); + if (!expect(waitForCondition([&]() { return !poolProbeResult.isEmpty(); }), + "default pool probe should complete")) + return 1; + if (!expect(poolProbeResult.value(QStringLiteral("poolStatus")).toString() + == QStringLiteral("missing_pool") + && poolProbeServer.requestCount() == 1 + && poolProbeClient.pairIdsCalls == 0 + && poolProbeClient.quoteCalls == 0 + && poolProbeClient.normalizedAccountIds.contains(poolIdHex), + "default pool probe should read only the pool account")) + return 1; + + poolProbeClient.normalizedBalanceHex = QString(32, QLatin1Char('1')); + poolProbeResult.clear(); + poolProbeRuntime.quoteAsync( + poolProbeRequest, readyNetwork(), true, true, true, + [&](QVariantMap result) { poolProbeResult = std::move(result); }); + if (!expect(waitForCondition([&]() { return !poolProbeResult.isEmpty(); }), + "nondefault pool probe should complete")) + return 1; + if (!expect(poolProbeResult.value(QStringLiteral("poolStatus")).toString() + == QStringLiteral("active_pool") + && poolProbeServer.requestCount() == 3 + && poolProbeClient.pairIdsCalls == 1 + && poolProbeClient.quoteCalls == 1, + "nondefault pool should run one validating full quote")) + return 1; + + poolProbeServer.failNextRequest(); + poolProbeResult.clear(); + poolProbeRuntime.quoteAsync( + poolProbeRequest, readyNetwork(), true, true, true, + [&](QVariantMap result) { poolProbeResult = std::move(result); }); + if (!expect(waitForCondition([&]() { return !poolProbeResult.isEmpty(); }), + "failed pool probe should complete")) + return 1; + if (!expect(poolProbeResult.value(QStringLiteral("code")).toString() + == QStringLiteral("account_read_failed") + && poolProbeServer.requestCount() == 4 + && poolProbeClient.pairIdsCalls == 1 + && poolProbeClient.quoteCalls == 1, + "failed pool probe should wait for the next one-account retry")) + return 1; + LocalRpcServer server; if (!expect(server.listen(), "local sequencer should listen")) return 1; diff --git a/apps/amm/tests/qml/tst_NewPositionForm.qml b/apps/amm/tests/qml/tst_NewPositionForm.qml index 16de739..15b77fa 100644 --- a/apps/amm/tests/qml/tst_NewPositionForm.qml +++ b/apps/amm/tests/qml/tst_NewPositionForm.qml @@ -441,6 +441,29 @@ TestCase { compare(form.localErrors.length, 0) } + function test_submissionSnapshotCarriesPoolAccountForProbe() { + var form = createForm() + form.flowState = flowState({ + "schema": "new-position.v2", + "status": "ok", + "canSubmit": true, + "tokenAId": tokenHigh, + "tokenBId": tokenLow, + "poolStatus": "missing_pool", + "poolId": submittedTransactionId, + "instruction": "NewDefinition", + "quoteHash": "sha256:expected", + "actualAmountARaw": "2000000", + "actualAmountBRaw": "3", + "expectedLpRaw": "10" + }) + wait(0) + + var snapshot = form.submissionSnapshot() + compare(snapshot.poolProbeRequest.poolId, submittedTransactionId) + verify(snapshot.request.poolId === undefined) + } + function test_staleQuoteErrorsDoNotMarkCurrentDraft() { var quote = { "schema": "new-position.v2",