feat(modules/amm): add swap_exact_out_quote op and module swapExactOutQuote

Server-side SwapExactOutput preview. The swap_exact_out_quote FFI op orients
the pool reserves to the requested in/out direction, prices via the shared
amm_core::swap_exact_out_amounts (so requiredIn matches the chain), and
derives the slippage ceiling: { requiredInRaw, maxInRaw, priceImpactBps }.
no_pool and output_exceeds_liquidity (amount_out >= reserve) are returned as
errors. Read-only — no quoteHash; the on-chain max_amount_in is the real
guard.

The module swapExactOutQuote(tokenIn, tokenOut, amountOut, slippageBps)
method derives the pool via the config-free pool_id op, reads it, and wraps
the op in the { status, error, ... } envelope. Mirrors swapExactInQuote.

Not yet consumed by the QML swap view, so nothing breaks. Lets the buy field
stay editable when the SwapCard is rewired in a follow-up.
This commit is contained in:
r4bbit
2026-08-06 14:31:49 +02:00
parent cd8a84374b
commit d038b3d060
8 changed files with 252 additions and 12 deletions
+45
View File
@@ -535,6 +535,51 @@ LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
return out;
}
LogosMap AmmModuleImpl::swapExactOutQuote(const std::string& token_in_hex,
const std::string& token_out_hex,
const nlohmann::json& amount_out,
int64_t slippage_bps) {
auto error = [](const std::string& err) {
return LogosMap{{"status", "error"}, {"error", err}};
};
std::string amount_out_decimal;
if (!jsonAmountToDecimal(amount_out, amount_out_decimal))
return error("bad_amount");
const std::string amm_program_id = ammProgramId();
if (amm_program_id.empty())
return error("config_missing");
// Derive the pool id (config-free) and read the pool account; its raw data is
// handed to the pricing op. An absent account has no data → `no_pool`.
const FfiResult poolId = call(amm_pool_id, json{
{"ammProgramId", amm_program_id},
{"tokenInId", token_in_hex},
{"tokenOutId", token_out_hex},
});
if (!poolId.ok)
return error(poolId.error.empty() ? "backend_error" : poolId.error);
const json pool = readPublicAccount(jStr(poolId.value, "poolId"));
const std::string pool_data = jStr(pool.value("account", json::object()), "data");
const FfiResult quoteResult = call(amm_swap_exact_out_quote, json{
{"tokenInId", token_in_hex},
{"tokenOutId", token_out_hex},
{"amountOutRaw", amount_out_decimal},
{"slippageBps", slippage_bps},
{"poolData", pool_data},
});
if (!quoteResult.ok)
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
// Success: wrap { requiredInRaw, maxInRaw, priceImpactBps } in the envelope.
LogosMap out = quoteResult.value;
out["status"] = "ok";
out["error"] = "";
return out;
}
std::string AmmModuleImpl::swapExactInput(const std::string& def_a_hex,
const std::string& def_b_hex,
const std::string& user_input_holding_hex,