mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(apps/amm): drive the exact-input swap preview from swapExactInQuote
Wire the Sell direction of the swap card to the module's server-side quote
instead of the client-side DummySwapState estimate. AmmUiBackend gains a
swapExactInQuote(tokenIn, tokenOut, amountInDecimal, slippageBps) slot that
forwards to amm_module and returns { expectedOutRaw, minReceivedRaw,
priceImpactBps } (read-only, no wallet guard).
SwapCard debounces a swapExactInQuote call as the user types a Sell amount and
sources the expected output, min received, and price impact from it. The exact
figures shown (buy field, confirmation snapshot) and the submitted min_out come
straight from the quote's raw integer strings, so the preview can't drift from
execution and the client no longer orients reserves (fixes the #236 defAHex
bug) or recomputes min_out in double/BigInt. A retyped amount invalidates the
quote immediately and blocks submit until the re-quote lands.
The Buy direction is unchanged — still a local DummySwapState preview, pending
the exact-output wiring. resolvePool stays as the source of pool existence, the
fee row, and the Buy-side reserves.
This commit is contained in:
@@ -37,6 +37,13 @@ Rectangle {
|
||||
property int poolFeeBps: 30
|
||||
property string poolError: ""
|
||||
|
||||
// ── Exact-input quote (backend.swapExactInQuote) ────────────────────────
|
||||
property bool quoteInLoading: false
|
||||
property string quoteInError: ""
|
||||
property string quoteExpectedOutRaw: "0"
|
||||
property string quoteMinReceivedRaw: "0"
|
||||
property int quotePriceImpactBps: 0
|
||||
|
||||
// ── Swap submission (backend.swapExactInput) ────────────────────────────
|
||||
property bool swapInProgress: false
|
||||
property string swapError: ""
|
||||
@@ -81,8 +88,11 @@ Rectangle {
|
||||
resolveDebounce.stop()
|
||||
}
|
||||
|
||||
onSellTokenChanged: root.requestResolve()
|
||||
onBuyTokenChanged: root.requestResolve()
|
||||
onSellTokenChanged: { root.requestResolve(); root.requestQuoteIn() }
|
||||
onBuyTokenChanged: { root.requestResolve(); root.requestQuoteIn() }
|
||||
onSellInputChanged: root.requestQuoteIn()
|
||||
onEditingSideChanged: root.requestQuoteIn()
|
||||
onSlippageTolerancePercentChanged: root.requestQuoteIn()
|
||||
|
||||
function doResolvePool() {
|
||||
if (!root.backend || !root.sellToken || !root.buyToken)
|
||||
@@ -129,6 +139,91 @@ Rectangle {
|
||||
})
|
||||
}
|
||||
|
||||
// ── Exact-input quote ──────────────────────────────────────────────────────
|
||||
Timer {
|
||||
id: quoteInDebounce
|
||||
interval: 350
|
||||
repeat: false
|
||||
onTriggered: root.doQuoteIn()
|
||||
}
|
||||
|
||||
function resetQuoteIn() {
|
||||
root.quoteExpectedOutRaw = "0"
|
||||
root.quoteMinReceivedRaw = "0"
|
||||
root.quotePriceImpactBps = 0
|
||||
}
|
||||
|
||||
function requestQuoteIn() {
|
||||
root.quoteInError = ""
|
||||
// Only the exact-input (Sell) direction is server-quoted; the Buy
|
||||
// direction is still a local preview (see DummySwapState).
|
||||
if (root.backend && root.editingSide === "sell" && root.sellToken && root.buyToken
|
||||
&& root.parsedSellInput > 0) {
|
||||
// Invalidate the previous quote up front: while a re-quote is pending
|
||||
// (debounce + in-flight), the stale expected-out / min_out must not be
|
||||
// shown or submittable. quoteInLoading gates canSubmit until the fresh
|
||||
// quote lands. Gating on backend here avoids setting the loading flag
|
||||
// when doQuoteIn would only bail — which would leave it stuck.
|
||||
root.resetQuoteIn()
|
||||
root.quoteInLoading = true
|
||||
quoteInDebounce.restart()
|
||||
} else {
|
||||
quoteInDebounce.stop()
|
||||
root.quoteInLoading = false
|
||||
root.resetQuoteIn()
|
||||
}
|
||||
}
|
||||
|
||||
function doQuoteIn() {
|
||||
if (!root.backend || root.editingSide !== "sell"
|
||||
|| !root.sellToken || !root.buyToken || root.parsedSellInput <= 0) {
|
||||
root.quoteInLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Capture the request identity: quote callbacks can arrive out of order,
|
||||
// so a stale one (tokens or the typed amount changed since) must not
|
||||
// overwrite the current preview or the submitted min_out.
|
||||
var reqSell = root.sellToken.definitionId
|
||||
var reqBuy = root.buyToken.definitionId
|
||||
var reqAmount = root.sellInput
|
||||
function isStale() {
|
||||
return root.editingSide !== "sell"
|
||||
|| !root.sellToken || !root.buyToken
|
||||
|| root.sellToken.definitionId !== reqSell
|
||||
|| root.buyToken.definitionId !== reqBuy
|
||||
|| root.sellInput !== reqAmount
|
||||
}
|
||||
|
||||
var slippageBps = Math.round(root.slippageTolerancePercent * 100)
|
||||
root.quoteInLoading = true
|
||||
logos.watch(root.backend.swapExactInQuote(reqSell, reqBuy, reqAmount, slippageBps),
|
||||
function (quote) {
|
||||
if (isStale())
|
||||
return
|
||||
root.quoteInLoading = false
|
||||
if (quote && quote.status === "ok") {
|
||||
root.quoteExpectedOutRaw = quote.expectedOutRaw || "0"
|
||||
root.quoteMinReceivedRaw = quote.minReceivedRaw || "0"
|
||||
root.quotePriceImpactBps = quote.priceImpactBps || 0
|
||||
root.quoteInError = ""
|
||||
} else {
|
||||
root.resetQuoteIn()
|
||||
// no_pool is surfaced via the pool status text, not as an error.
|
||||
var code = (quote && quote.error) || "backend_error"
|
||||
root.quoteInError = code === "no_pool" ? "" : code
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
if (isStale())
|
||||
return
|
||||
console.warn("swapExactInQuote error:", error)
|
||||
root.quoteInLoading = false
|
||||
root.resetQuoteIn()
|
||||
root.quoteInError = String(error)
|
||||
})
|
||||
}
|
||||
|
||||
// JS doubles lose precision far below u128 range; these are only used to
|
||||
// drive the *estimate* (expected output / min received / price impact),
|
||||
// never the actual swap amount — the sell amount sent to the backend is
|
||||
@@ -154,13 +249,21 @@ Rectangle {
|
||||
? parsedSellInput
|
||||
: swapState.amountInFor(parsedBuyInput, sellReserveNum, buyReserveNum)
|
||||
|
||||
// Exact-input (Sell) expected output comes from the server quote; the Buy
|
||||
// direction still estimates locally. Number() may lose precision on large
|
||||
// base-unit values, so this drives gating only — the exact figures shown and
|
||||
// submitted come from quoteExpectedOutRaw / quoteMinReceivedRaw directly.
|
||||
readonly property real parsedBuyAmount: editingSide === "buy"
|
||||
? parsedBuyInput
|
||||
: swapState.amountOutFor(parsedSellInput, sellReserveNum, buyReserveNum)
|
||||
: (Number(root.quoteExpectedOutRaw) || 0)
|
||||
|
||||
readonly property real feeAmount: swapState.feeAmount(parsedSellAmount)
|
||||
readonly property real minReceivedAmount: swapState.minReceived(parsedBuyAmount, slippageTolerancePercent)
|
||||
readonly property real priceImpactPercent: swapState.priceImpactPercent(parsedSellAmount, parsedBuyAmount, sellReserveNum, buyReserveNum)
|
||||
readonly property real minReceivedAmount: editingSide === "sell"
|
||||
? (Number(root.quoteMinReceivedRaw) || 0)
|
||||
: swapState.minReceived(parsedBuyAmount, slippageTolerancePercent)
|
||||
readonly property real priceImpactPercent: editingSide === "sell"
|
||||
? root.quotePriceImpactBps / 100
|
||||
: swapState.priceImpactPercent(parsedSellAmount, parsedBuyAmount, sellReserveNum, buyReserveNum)
|
||||
|
||||
readonly property string swapModeText: editingSide === "buy" ? qsTr("Exact output (preview only)") : qsTr("Exact input")
|
||||
|
||||
@@ -180,7 +283,7 @@ Rectangle {
|
||||
&& parsedSellAmount > 0 && parsedBuyAmount > 0
|
||||
&& root.poolResolved && root.poolExists
|
||||
&& !insufficientLiquidity && !root.swapInProgress
|
||||
&& root.walletOpen
|
||||
&& !root.quoteInLoading && root.walletOpen
|
||||
|
||||
readonly property string submitButtonText: {
|
||||
if (!tokensSelected) return qsTr("Select tokens")
|
||||
@@ -189,6 +292,7 @@ Rectangle {
|
||||
if (editingSide === "buy") return qsTr("Enter a sell amount to swap")
|
||||
if (root.poolLoading || !root.poolResolved) return qsTr("Resolving pool…")
|
||||
if (!root.poolExists) return qsTr("No pool / no liquidity")
|
||||
if (root.quoteInLoading) return qsTr("Quoting…")
|
||||
if (insufficientLiquidity) return qsTr("Insufficient liquidity")
|
||||
if (parsedBuyAmount <= 0) return qsTr("Amount too small")
|
||||
if (!root.walletOpen) return qsTr("Connect wallet to swap")
|
||||
@@ -201,6 +305,7 @@ Rectangle {
|
||||
if (root.poolLoading) return qsTr("Looking up pool…")
|
||||
if (root.poolError.length > 0) return root.poolError
|
||||
if (root.poolResolved && !root.poolExists) return qsTr("No pool / no liquidity for this pair.")
|
||||
if (root.quoteInError.length > 0) return qsTr("Quote failed: %1").arg(root.quoteInError)
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -234,17 +339,22 @@ Rectangle {
|
||||
? sellInput
|
||||
: (parsedSellAmount > 0 ? formatBaseUnits(parsedSellAmount) : "")
|
||||
|
||||
// Sell direction shows the quote's exact-integer expected output verbatim
|
||||
// (no double round-trip); Buy direction still renders the local estimate.
|
||||
readonly property string buyDisplay: editingSide === "buy"
|
||||
? buyInput
|
||||
: (parsedBuyAmount > 0 ? formatBaseUnits(parsedBuyAmount) : "")
|
||||
: ((root.quoteExpectedOutRaw && root.quoteExpectedOutRaw !== "0") ? root.quoteExpectedOutRaw : "")
|
||||
|
||||
// Only reached in the Sell (exact-input) direction — canSubmit gates the CTA
|
||||
// to editingSide === "sell" — so the amounts come straight from the raw
|
||||
// input and the quote's exact-integer figures.
|
||||
function buildSnapshot() {
|
||||
return {
|
||||
"sellToken": sellToken ? sellToken.symbol : "",
|
||||
"buyToken": buyToken ? buyToken.symbol : "",
|
||||
"sellAmount": formatBaseUnits(parsedSellAmount),
|
||||
"buyAmount": formatBaseUnits(parsedBuyAmount),
|
||||
"minReceived": formatBaseUnits(minReceivedAmount),
|
||||
"sellAmount": root.sellInput,
|
||||
"buyAmount": root.quoteExpectedOutRaw,
|
||||
"minReceived": root.quoteMinReceivedRaw,
|
||||
"feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""),
|
||||
"priceImpactPercent": swapState.formatPercent(priceImpactPercent),
|
||||
"priceImpactPercentValue": priceImpactPercent,
|
||||
@@ -264,15 +374,10 @@ Rectangle {
|
||||
root.swapInProgress = true
|
||||
root.swapError = ""
|
||||
|
||||
// Compute the submitted slippage floor with exact integer (BigInt) math
|
||||
// rather than the double-based preview: base-unit values for 18-decimal
|
||||
// tokens exceed 2^53, where doubles would understate min_out and weaken
|
||||
// price protection. Sell/buy reserves follow the pool's canonical order.
|
||||
var minOutStr = swapState.minOutBaseUnits(
|
||||
root.sellInput,
|
||||
root.sellIsPoolA ? root.poolReserveA : root.poolReserveB,
|
||||
root.sellIsPoolA ? root.poolReserveB : root.poolReserveA,
|
||||
root.slippageTolerancePercent)
|
||||
// The submitted slippage floor is the quote's exact-integer minReceivedRaw
|
||||
// (base units), derived server-side from the same formula the chain uses —
|
||||
// no client-side reserve orientation or double-precision recompute.
|
||||
var minOutStr = root.quoteMinReceivedRaw
|
||||
// Max u64 sentinel: "ignore deadline", per AmmUiBackend.rep.
|
||||
var deadline = "18446744073709551615"
|
||||
|
||||
|
||||
@@ -17,19 +17,6 @@ QtObject {
|
||||
return parseAmount(amountIn) * root.feeBps / 10000;
|
||||
}
|
||||
|
||||
function amountOutFor(amountIn, reserveIn, reserveOut) {
|
||||
const safeAmountIn = parseAmount(amountIn);
|
||||
const safeReserveIn = parseAmount(reserveIn);
|
||||
const safeReserveOut = parseAmount(reserveOut);
|
||||
|
||||
if (safeAmountIn <= 0 || safeReserveIn <= 0 || safeReserveOut <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const amountInAfterFee = safeAmountIn * (10000 - root.feeBps) / 10000;
|
||||
return safeReserveOut * amountInAfterFee / (safeReserveIn + amountInAfterFee);
|
||||
}
|
||||
|
||||
function amountInFor(amountOut, reserveIn, reserveOut) {
|
||||
const safeAmountOut = parseAmount(amountOut);
|
||||
const safeReserveIn = parseAmount(reserveIn);
|
||||
@@ -73,44 +60,6 @@ QtObject {
|
||||
return safeAmount * (1 - safeSlippage / 100);
|
||||
}
|
||||
|
||||
// Exact-integer minimum-received (base units) for a SwapExactInput, used as
|
||||
// the on-chain slippage floor that is actually submitted. Computed in BigInt
|
||||
// (arbitrary precision, mirroring the on-chain u256 math): base units for
|
||||
// 18-decimal tokens exceed 2^53 and even overflow u128 intermediates, so JS
|
||||
// doubles silently lose precision and would understate min_out — weakening
|
||||
// the user's price protection. amountIn/reserveIn/reserveOut are base-unit
|
||||
// integer strings; returns a decimal string. Falls back to the double
|
||||
// estimate only if BigInt is unavailable in this Qt build.
|
||||
function minOutBaseUnits(amountIn, reserveIn, reserveOut, slippagePercent) {
|
||||
if (typeof BigInt !== "undefined") {
|
||||
var toBig = function (x) {
|
||||
var s = String(x).trim();
|
||||
return /^[0-9]+$/.test(s) ? BigInt(s) : BigInt(0);
|
||||
};
|
||||
var zero = BigInt(0);
|
||||
var denom = BigInt(10000);
|
||||
var amtIn = toBig(amountIn);
|
||||
var resIn = toBig(reserveIn);
|
||||
var resOut = toBig(reserveOut);
|
||||
if (amtIn <= zero || resIn <= zero || resOut <= zero)
|
||||
return "0";
|
||||
|
||||
var feeBps = BigInt(Math.round(Math.min(10000, Math.max(0, Number(root.feeBps) || 0))));
|
||||
var amtInAfterFee = amtIn * (denom - feeBps) / denom; // floor
|
||||
if (amtInAfterFee <= zero)
|
||||
return "0";
|
||||
var out = resOut * amtInAfterFee / (resIn + amtInAfterFee); // floor
|
||||
var slipBps = BigInt(Math.round(clampSlippagePercent(slippagePercent) * 100));
|
||||
if (slipBps < zero) slipBps = zero;
|
||||
if (slipBps > denom) slipBps = denom;
|
||||
var minOut = out * (denom - slipBps) / denom; // floor
|
||||
return minOut.toString();
|
||||
}
|
||||
// Legacy double fallback (no worse than before if BigInt is missing).
|
||||
var estOut = amountOutFor(amountIn, reserveIn, reserveOut);
|
||||
return String(Math.floor(Math.max(0, minReceived(estOut, slippagePercent))));
|
||||
}
|
||||
|
||||
function maxSent(amountIn, slippagePercent) {
|
||||
const safeAmount = parseAmount(amountIn);
|
||||
const safeSlippage = clampSlippagePercent(slippagePercent);
|
||||
|
||||
@@ -231,6 +231,16 @@ QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString u
|
||||
return txHash;
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::swapExactInQuote(QString tokenInHex, QString tokenOutHex,
|
||||
QString amountInDecimal, int slippageBps)
|
||||
{
|
||||
// Read-only preview — no wallet guard. The module reads the pool and prices
|
||||
// the swap server-side; the returned envelope orients reserves and computes
|
||||
// expectedOut/minReceived/priceImpact via the same formula the chain uses.
|
||||
return m_logos->amm_module.swapExactInQuote(
|
||||
tokenInHex, tokenOutHex, amountInDecimal, slippageBps);
|
||||
}
|
||||
|
||||
QVariantList AmmUiBackend::tokenList()
|
||||
{
|
||||
return m_logos->amm_module.tokenList();
|
||||
|
||||
@@ -61,6 +61,8 @@ public slots:
|
||||
QString swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
QString userOutputHoldingHex, QString amountInDecimal,
|
||||
QString minOutDecimal, QString deadlineDecimal) override;
|
||||
QVariantMap swapExactInQuote(QString tokenInHex, QString tokenOutHex,
|
||||
QString amountInDecimal, int slippageBps) override;
|
||||
// Reads the token list from TOKENS_CONFIG (via the module) so the Swap UI's
|
||||
// token picker is config-driven instead of hardcoded.
|
||||
QVariantList tokenList() override;
|
||||
|
||||
@@ -64,6 +64,14 @@ class AmmUiBackend
|
||||
// is enforced). Returns the tx hash, or an empty string on failure
|
||||
// (no pool, unreadable AMM_PROGRAM_BIN, bad inputs, or a failed tx).
|
||||
SLOT(QString swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex, QString userOutputHoldingHex, QString amountInDecimal, QString minOutDecimal, QString deadlineDecimal))
|
||||
// Server-side SwapExactInput preview for (tokenInHex, tokenOutHex): reads the
|
||||
// pool and returns { status:"ok", error:"", expectedOutRaw, minReceivedRaw,
|
||||
// priceImpactBps }, oriented and priced via the shared on-chain formula.
|
||||
// amountInDecimal is a decimal-string base-unit amount; slippageBps is basis
|
||||
// points. On failure { status:"error", error:<code> } — no_pool,
|
||||
// config_missing, bad_amount, invalid_slippage (slippageBps out of range),
|
||||
// backend_error. Read-only, no submission.
|
||||
SLOT(QVariantMap swapExactInQuote(QString tokenInHex, QString tokenOutHex, QString amountInDecimal, int slippageBps))
|
||||
// Reads the token list config at TOKENS_CONFIG (absolute path, JSON array
|
||||
// of { symbol, name, definitionId, holding, decimals }) and returns it as
|
||||
// a QVariantList of QVariantMap entries. Returns an empty list if
|
||||
|
||||
+15
-7
@@ -1,5 +1,5 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// AMM UI test — swap 10000 of the first token in the list for the second.
|
||||
// AMM UI test — swap the first token in the list for the second (SELL_AMOUNT).
|
||||
//
|
||||
// Drives the running AMM UI through the QML inspector (logos-qt-mcp). Run it
|
||||
// against a LIVE app window so you can watch it happen — see "Running the UI
|
||||
@@ -24,7 +24,7 @@ const fwRoot =
|
||||
new URL("../result-mcp", import.meta.url).pathname;
|
||||
const { test, run } = await import(resolve(fwRoot, "test-framework/framework.mjs"));
|
||||
|
||||
const SELL_AMOUNT = "10000";
|
||||
const SELL_AMOUNT = "100";
|
||||
|
||||
// --- small helpers over the raw inspector commands -------------------------
|
||||
|
||||
@@ -86,13 +86,21 @@ async function pickToken(app, index) {
|
||||
|
||||
// Enter the sell amount by setting the SwapCard's state directly. Synthesizing
|
||||
// keystrokes needs the TextInput to hold active focus, which the inspector
|
||||
// can't reliably grant headlessly; setting sellInput drives the exact same
|
||||
// reactive flow (estimate -> CTA -> confirm -> submit) and the bound TextInput
|
||||
// still displays the value.
|
||||
// can't reliably grant headlessly; setting sellInput updates the property (and
|
||||
// the bound TextInput display) so the reactive flow can run.
|
||||
//
|
||||
// Setting a property programmatically re-evaluates dependent BINDINGS but does
|
||||
// not fire the onSellInputChanged HANDLER the way real typing does — and the
|
||||
// Sell preview is now driven by that handler (onSellInputChanged ->
|
||||
// requestQuoteIn -> async backend.swapExactInQuote), not a synchronous binding.
|
||||
// So kick the quote explicitly, mirroring the doResolvePool() nudge used in the
|
||||
// reserve-change check below. Without this the CTA never leaves "Amount too
|
||||
// small" because the server quote never fires.
|
||||
async function setSellAmount(app, amount) {
|
||||
const cardId = await idByObjectName(app, "swapCard");
|
||||
await app.inspector.send("setProperty", { objectId: cardId, property: "editingSide", value: "sell" });
|
||||
await app.inspector.send("setProperty", { objectId: cardId, property: "sellInput", value: String(amount) });
|
||||
await app.inspector.send("evaluate", { expression: "requestQuoteIn()", objectId: cardId });
|
||||
}
|
||||
|
||||
// Read the SwapCard's swap/pool state — explains WHY the CTA isn't "Swap" yet.
|
||||
@@ -136,7 +144,7 @@ async function saveShot(app, name) {
|
||||
|
||||
// --- the test ---------------------------------------------------------------
|
||||
|
||||
test("amm swap: sell 10000 of token #1 for token #2", async (app) => {
|
||||
test("amm swap: sell token #1 for token #2", async (app) => {
|
||||
// 1. Wait for the swap card to render (Trade tab is the default, index 0).
|
||||
await app.waitFor(
|
||||
async () => { await app.expectTexts(["Sell", "Buy"]); },
|
||||
@@ -153,7 +161,7 @@ test("amm swap: sell 10000 of token #1 for token #2", async (app) => {
|
||||
const second = await pickToken(app, 1);
|
||||
console.log(` sell ${first} -> buy ${second}`);
|
||||
|
||||
// 5. Enter 10000 as the sell amount.
|
||||
// 5. Enter the sell amount.
|
||||
await setSellAmount(app, SELL_AMOUNT);
|
||||
await app.expectTexts([SELL_AMOUNT]); // the amount should now be visible
|
||||
|
||||
|
||||
@@ -47,8 +47,9 @@ public:
|
||||
/// decimal string (JSON floats rejected); `slippage_bps` is basis points.
|
||||
/// On failure: `{ status:"error", error:<code> }` — `no_pool` (no pool /
|
||||
/// liquidity), `config_missing` (AMM_PROGRAM_BIN unset/unreadable),
|
||||
/// `bad_amount`, or `backend_error`. Pool metadata (reserves, fee) comes from
|
||||
/// `resolvePool`, so it isn't echoed here.
|
||||
/// `bad_amount`, `invalid_slippage` (`slippage_bps` out of range), or
|
||||
/// `backend_error`. Pool metadata (reserves, fee) comes from `resolvePool`,
|
||||
/// so it isn't echoed here.
|
||||
LogosMap swapExactInQuote(const std::string& token_in_hex,
|
||||
const std::string& token_out_hex,
|
||||
const nlohmann::json& amount_in,
|
||||
@@ -61,7 +62,8 @@ public:
|
||||
/// string (JSON floats rejected); `slippage_bps` is basis points. On failure:
|
||||
/// `{ status:"error", error:<code> }` — `no_pool` (no pool / liquidity),
|
||||
/// `output_exceeds_liquidity` (amount_out ≥ reserve), `config_missing`
|
||||
/// (AMM_PROGRAM_BIN unset/unreadable), `bad_amount`, or `backend_error`.
|
||||
/// (AMM_PROGRAM_BIN unset/unreadable), `bad_amount`, `invalid_slippage`
|
||||
/// (`slippage_bps` out of range), or `backend_error`.
|
||||
LogosMap swapExactOutQuote(const std::string& token_in_hex,
|
||||
const std::string& token_out_hex,
|
||||
const nlohmann::json& amount_out,
|
||||
|
||||
Reference in New Issue
Block a user