mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(modules/amm): add swap_exact_in_quote op and module swapExactInQuote
Server-side SwapExactInput preview. The swap_exact_in_quote FFI op orients the pool reserves to the requested in/out direction, prices via the shared amm_core::swap_exact_in_amounts (so expectedOut matches the chain), and derives the slippage floor: { expectedOutRaw, minReceivedRaw, priceImpactBps }. no_pool is returned as an error; pool metadata (reserves/fee) comes from resolve_pool, so it isn't echoed. Read-only — no quoteHash; the on-chain min_amount_out is the real guard.
The module swapExactInQuote(tokenIn, tokenOut, amountIn, slippageBps) method derives the pool via the config-free pool_id op, reads it, and wraps the op in the { status, error, ... } envelope; call() now surfaces the op error code so no_pool propagates.
The QML swap view still uses the old path, so nothing breaks. Will make the QML consume it in a follow-up.
This commit is contained in:
@@ -139,6 +139,11 @@ bool jsonAmountToDecimal(const json& j, std::string& out) {
|
||||
s = s.substr(1, s.size() - 2);
|
||||
trim(s);
|
||||
}
|
||||
// A valid base-unit amount is a non-empty run of decimal digits. Reject
|
||||
// anything else (empty, signs, decimal points, exponents, letters) here
|
||||
// rather than passing it downstream to surface as an opaque backend error.
|
||||
if (s.empty() || s.find_first_not_of("0123456789") != std::string::npos)
|
||||
return false;
|
||||
out = s;
|
||||
return true;
|
||||
}
|
||||
@@ -193,10 +198,13 @@ std::vector<uint8_t> jsonWordsToLeBytes(const json& arr) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Result of an amm_ffi JSON op: the `{ ok, value }` envelope decoded.
|
||||
// Result of an amm_ffi JSON op: the `{ ok, value, error }` envelope decoded.
|
||||
// `error` carries the op's failure code (e.g. "no_pool") when `ok` is false, so
|
||||
// callers can surface it in their own response envelope.
|
||||
struct FfiResult {
|
||||
bool ok = false;
|
||||
json value;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// Serialize `request`, hand it to an amm_ffi op, and decode its
|
||||
@@ -218,15 +226,16 @@ FfiResult call(char* (*op)(const char*), const json& request) {
|
||||
return {};
|
||||
}
|
||||
if (!doc.value("ok", false)) {
|
||||
AMM_TRACE("amm_ffi op failure: " << doc.value("error", std::string()));
|
||||
return {};
|
||||
std::string error = doc.value("error", std::string());
|
||||
AMM_TRACE("amm_ffi op failure: " << error);
|
||||
return {false, json(), std::move(error)};
|
||||
}
|
||||
const auto it = doc.find("value");
|
||||
if (it == doc.end() || !it->is_object()) {
|
||||
AMM_TRACE("amm_ffi op value is not an object");
|
||||
return {};
|
||||
}
|
||||
return {true, *it};
|
||||
return {true, *it, {}};
|
||||
}
|
||||
|
||||
// new-position response envelope builders.
|
||||
@@ -471,6 +480,61 @@ LogosMap AmmModuleImpl::resolvePool(const std::string& def_a_hex,
|
||||
return resolved; // { exists:true, reserveA, reserveB, feeBps }
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
|
||||
const std::string& token_out_hex,
|
||||
const nlohmann::json& amount_in,
|
||||
int64_t slippage_bps) {
|
||||
auto error = [](const std::string& err) {
|
||||
return LogosMap{{"status", "error"}, {"error", err}};
|
||||
};
|
||||
|
||||
// amountIn arrives as a JSON number (CLI) or decimal string (UI); coerce to a
|
||||
// canonical decimal string (rejects floats — see jsonAmountToDecimal).
|
||||
std::string amount_in_decimal;
|
||||
if (!jsonAmountToDecimal(amount_in, amount_in_decimal))
|
||||
return error("bad_amount");
|
||||
|
||||
// slippageBps is a fraction of 100% in basis points. The FFI request field is
|
||||
// a u32, so a negative value would fail deserialization with an opaque serde
|
||||
// message; gate the full range here for a stable code (100% = 10000 bps, the
|
||||
// FFI's FEE_BPS_DENOMINATOR, which also rejects the upper bound as a backstop).
|
||||
if (slippage_bps < 0 || slippage_bps >= 10000)
|
||||
return error("invalid_slippage");
|
||||
|
||||
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_in_quote, json{
|
||||
{"tokenInId", token_in_hex},
|
||||
{"tokenOutId", token_out_hex},
|
||||
{"amountInRaw", amount_in_decimal},
|
||||
{"slippageBps", slippage_bps},
|
||||
{"poolData", pool_data},
|
||||
});
|
||||
if (!quoteResult.ok)
|
||||
return error(quoteResult.error.empty() ? "backend_error" : quoteResult.error);
|
||||
|
||||
// Success: wrap the priced payload { expectedOutRaw, minReceivedRaw,
|
||||
// priceImpactBps } in the standard 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,
|
||||
|
||||
Reference in New Issue
Block a user