mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-26 06:31:19 +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,
|
||||
|
||||
@@ -40,6 +40,20 @@ public:
|
||||
/// `no_pool` for the ordinary "no pool / no liquidity yet" state.
|
||||
LogosMap resolvePool(const std::string& def_a_hex, const std::string& def_b_hex);
|
||||
|
||||
/// Prices a `SwapExactInput` for the (token_in_hex, token_out_hex) pair:
|
||||
/// reads the pool and returns `{ status:"ok", error:"", expectedOutRaw,
|
||||
/// minReceivedRaw, priceImpactBps }`, oriented and computed server-side via
|
||||
/// the shared on-chain formula. `amount_in` accepts a JSON integer or a
|
||||
/// 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.
|
||||
LogosMap swapExactInQuote(const std::string& token_in_hex,
|
||||
const std::string& token_out_hex,
|
||||
const nlohmann::json& amount_in,
|
||||
int64_t slippage_bps);
|
||||
|
||||
/// Submits an on-chain SwapExactInput transaction against the pool for
|
||||
/// (def_a_hex = token in, def_b_hex = token out). amount_in / min_out are
|
||||
/// u128 base-unit amounts; deadline is a u64 unix-ms timestamp. Each accepts
|
||||
|
||||
Reference in New Issue
Block a user