mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
refactor(amm): enrich resolvePool into resolvePoolAccount
Return the pool's full derived state from one read instead of just existence +
reserves, so callers get the derived accounts (for future account views / oracle
setup) without re-deriving.
FFI resolve_pool: drop the `exists` boolean — the presence of data is the signal.
An existing pool returns { status:"ok", ..., poolId, defAHex, defBHex, vaultAId,
vaultBId, lpDefinitionId, reserveA, reserveB, liquiditySupply, feeBps }; a missing /
uninitialized pool is the { status:"error", error:"no_pool", poolId } error (still
carrying the derived poolId for address derivation).
Module: resolvePool -> resolvePoolAccount — { status:"error", error } envelope for
hard failures, and orient reserves + defs + vaults to the caller's requested order.
Backend + QML: rename the slot; SwapCard and NewPositionFlow switch the existence
check from pool.exists to pool.status === "ok" (reserve field names unchanged, so
no other consumer edits). no_pool still routes to create-pool; hard errors still
surface.
This commit is contained in:
@@ -16,10 +16,13 @@ transport-independent JSON FFI), and this module sequences those pure ops with
|
||||
chain I/O delegated to the `logos_execution_zone` wallet module. Its public
|
||||
methods (the module API is generated from the header) are:
|
||||
|
||||
- `resolvePool(defAHex, defBHex)` — derives the pool PDAs
|
||||
(config/pool/vaults/current-tick) and reads the pool's on-chain reserves.
|
||||
Returns `{ exists: false, error }` when the AMM isn't configured/initialized or
|
||||
the pool has no liquidity.
|
||||
- `resolvePoolAccount(defAHex, defBHex)` — derives the pool PDA and reads/decodes
|
||||
the pool account (reserves in canonical `a`/`b` order, fee tier). On success
|
||||
`{ status: "ok", error: "", poolId, defAHex, defBHex, vaultAId, vaultBId,
|
||||
lpDefinitionId, reserveA, reserveB, liquiditySupply, feeBps }`; an absent /
|
||||
uninitialized pool or one with no liquidity is `{ status: "error", error:
|
||||
"no_pool", poolId }` (other codes: `no_program_bin`, `amm_not_initialized`,
|
||||
`bad_config`).
|
||||
- `swapExactInput(defAHex, defBHex, userInputHoldingHex, userOutputHoldingHex, amountIn, minOut, deadline)`
|
||||
— submits an on-chain `SwapExactInput` transaction (defA = token in,
|
||||
defB = token out); returns the tx hash (or empty on failure). See
|
||||
@@ -99,8 +102,8 @@ Both are absolute-path env vars set on the **process that hosts the module**
|
||||
(the `logoscore` daemon, or Basecamp) — not on the `call`:
|
||||
|
||||
- `AMM_PROGRAM_BIN` — the deployed `amm.bin`. Required; its ELF determines the
|
||||
program id and every derived PDA. Without it, `resolvePool` returns
|
||||
`{ exists: false, error: "no_program_bin" }`.
|
||||
program id and every derived PDA. Without it, `resolvePoolAccount` returns
|
||||
`{ status: "error", error: "no_program_bin" }`.
|
||||
- `TOKENS_CONFIG` — JSON array of `{ symbol, name, definitionId, holding, decimals }`
|
||||
consumed by `tokenList()`.
|
||||
|
||||
|
||||
@@ -71,28 +71,45 @@ pub(super) fn swap_pair(request: SwapPairRequest) -> Result<Value, String> {
|
||||
/// Decodes a pool account: whether it holds liquidity, its canonical token ids
|
||||
/// (`defAHex`/`defBHex`), its reserves (same canonical `a`/`b` order — the
|
||||
/// caller matches a reserve to its own direction by comparing its token id
|
||||
/// against `defAHex`), and fee tier. Absent/empty/uninitialized pool →
|
||||
/// `{ exists: false }`.
|
||||
/// against `defAHex`), and fee tier. On success returns a `{ status: "ok",
|
||||
/// error: "", poolId, defAHex, defBHex, vaultAId, vaultBId, lpDefinitionId,
|
||||
/// reserveA, reserveB, liquiditySupply, feeBps }` envelope; an absent / empty /
|
||||
/// uninitialized pool is the `{ status: "error", error: "no_pool", poolId }`
|
||||
/// envelope (the derived `poolId` is still carried, for address derivation).
|
||||
pub(super) fn resolve_pool(request: ResolvePoolRequest) -> Result<Value, String> {
|
||||
// The pool's presence IS the signal: existing pools return their decoded state; a missing /
|
||||
// uninitialized pool is the `no_pool` error (still carrying the derived `poolId` for address
|
||||
// derivation). The read's id is hex (the module reads by hex); undecodable ⇒ "".
|
||||
let pool_id = account_id_from_hex(&request.pool.id, "pool id")
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_default();
|
||||
let missing = || json!({ "status": "error", "error": "no_pool", "poolId": pool_id });
|
||||
|
||||
if request.pool.status != "ok" {
|
||||
return Ok(json!({ "exists": false }));
|
||||
return Ok(missing());
|
||||
}
|
||||
let Ok((_, pool_account)) = decode_account(&request.pool) else {
|
||||
return Ok(json!({ "exists": false }));
|
||||
return Ok(missing());
|
||||
};
|
||||
let Ok(pool) = PoolDefinition::try_from(&pool_account.data) else {
|
||||
return Ok(json!({ "exists": false }));
|
||||
return Ok(missing());
|
||||
};
|
||||
if pool.liquidity_pool_supply == 0 {
|
||||
return Ok(json!({ "exists": false }));
|
||||
return Ok(missing());
|
||||
}
|
||||
let fee_bps = u32::try_from(pool.fees).map_err(|_| String::from("invalid_fee_tier"))?;
|
||||
Ok(json!({
|
||||
"exists": true,
|
||||
"status": "ok",
|
||||
"error": "",
|
||||
"poolId": pool_id,
|
||||
"defAHex": account_id_hex(pool.definition_token_a_id),
|
||||
"defBHex": account_id_hex(pool.definition_token_b_id),
|
||||
"vaultAId": pool.vault_a_id.to_string(),
|
||||
"vaultBId": pool.vault_b_id.to_string(),
|
||||
"lpDefinitionId": pool.liquidity_pool_id.to_string(),
|
||||
"reserveA": pool.reserve_a.to_string(),
|
||||
"reserveB": pool.reserve_b.to_string(),
|
||||
"liquiditySupply": pool.liquidity_pool_supply.to_string(),
|
||||
"feeBps": fee_bps,
|
||||
}))
|
||||
}
|
||||
@@ -441,17 +458,22 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_pool_reports_canonical_token_ids() {
|
||||
fn resolve_pool_reports_canonical_token_ids_and_derived_accounts() {
|
||||
let def_a = AccountId::new([0xAA; 32]);
|
||||
let def_b = AccountId::new([0xBB; 32]);
|
||||
let vault_a = AccountId::new([0xC1; 32]);
|
||||
let vault_b = AccountId::new([0xC2; 32]);
|
||||
let lp = AccountId::new([0xD0; 32]);
|
||||
let pool = PoolDefinition {
|
||||
definition_token_a_id: def_a,
|
||||
definition_token_b_id: def_b,
|
||||
vault_a_id: vault_a,
|
||||
vault_b_id: vault_b,
|
||||
liquidity_pool_id: lp,
|
||||
liquidity_pool_supply: 1_000,
|
||||
reserve_a: 111,
|
||||
reserve_b: 222,
|
||||
fees: 30,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let value = resolve_pool(ResolvePoolRequest {
|
||||
@@ -459,19 +481,24 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(value["exists"], true);
|
||||
assert_eq!(value["status"], "ok");
|
||||
assert_eq!(value["poolId"], AccountId::new([0x11; 32]).to_string());
|
||||
// The Swap UI matches reserveA/reserveB to its own sell/buy direction by
|
||||
// comparing the sold token's id against defAHex — so both ids must be
|
||||
// present in canonical order.
|
||||
assert_eq!(value["defAHex"], account_id_hex(def_a));
|
||||
assert_eq!(value["defBHex"], account_id_hex(def_b));
|
||||
assert_eq!(value["vaultAId"], vault_a.to_string());
|
||||
assert_eq!(value["vaultBId"], vault_b.to_string());
|
||||
assert_eq!(value["lpDefinitionId"], lp.to_string());
|
||||
assert_eq!(value["reserveA"], "111");
|
||||
assert_eq!(value["reserveB"], "222");
|
||||
assert_eq!(value["liquiditySupply"], "1000");
|
||||
assert_eq!(value["feeBps"], 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_pool_absent_when_no_liquidity() {
|
||||
fn resolve_pool_absent_is_a_no_pool_error_carrying_the_pool_id() {
|
||||
let pool = PoolDefinition {
|
||||
liquidity_pool_supply: 0,
|
||||
..Default::default()
|
||||
@@ -480,7 +507,14 @@ mod tests {
|
||||
pool: pool_read(&pool),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(value, json!({ "exists": false }));
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"status": "error",
|
||||
"error": "no_pool",
|
||||
"poolId": AccountId::new([0x11; 32]).to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -341,15 +341,15 @@ nlohmann::json AmmModuleImpl::readConfig(const std::string& amm_program_id) {
|
||||
return readPublicAccount(jStr(configResult.value, "configId"));
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::resolvePool(const std::string& def_a_hex,
|
||||
const std::string& def_b_hex) {
|
||||
LogosMap AmmModuleImpl::resolvePoolAccount(const std::string& def_a_hex,
|
||||
const std::string& def_b_hex) {
|
||||
// A hard failure carries a stable `error` code (no_program_bin /
|
||||
// amm_not_initialized / bad_config) so SwapCard can surface it via poolError.
|
||||
// `no_pool` is the ordinary "no pool / no liquidity yet" state, which SwapCard
|
||||
// treats as its normal empty state (SwapCard.qml `error !== "no_pool"`). The
|
||||
// underlying FFI error string is AMM_TRACE'd to the daemon log.
|
||||
// amm_not_initialized / bad_config) so callers can surface it. `no_pool` is the
|
||||
// ordinary "no pool / no liquidity yet" state — also a `status:"error"` result
|
||||
// (SwapCard treats it as its normal empty state via `error !== "no_pool"`, the
|
||||
// flow routes it to create-pool). The underlying FFI error is AMM_TRACE'd.
|
||||
auto failed = [](const std::string& error) {
|
||||
return LogosMap{{"exists", false}, {"error", error}};
|
||||
return LogosMap{{"status", "error"}, {"error", error}};
|
||||
};
|
||||
|
||||
const std::string amm_program_id = ammProgramId();
|
||||
@@ -387,20 +387,18 @@ LogosMap AmmModuleImpl::resolvePool(const std::string& def_a_hex,
|
||||
const FfiResult resolveResult = call(amm_resolve_pool, json{{"pool", pool}});
|
||||
if (!resolveResult.ok)
|
||||
return failed("bad_config"); // amm_resolve_pool op failed
|
||||
// resolve_pool returns { exists:false } (no error) for a missing pool / no
|
||||
// liquidity; re-tag it "no_pool" — the code SwapCard expects for that state.
|
||||
// resolve_pool returns status:"error"/no_pool for a missing pool (pass through) or
|
||||
// status:"ok" with the decoded state. It labels reserves/vaults in the pool's STORED
|
||||
// order (reserveA is defAHex's); orient them to the CALLER's requested order so reserveA /
|
||||
// vaultAId are token_a's — the stored order needn't match (it can be non-canonical, e.g.
|
||||
// the testnet setup's pool). Callers then read A/B as their own token-a/token-b directly.
|
||||
json resolved = resolveResult.value;
|
||||
if (!resolved.value("exists", false))
|
||||
return failed("no_pool");
|
||||
// resolve_pool labels the reserves in the pool's STORED order (reserveA is defAHex's).
|
||||
// Orient them to the CALLER's requested order so reserveA is token_a's reserve — the
|
||||
// pool's stored order needn't match (it can be non-canonical, e.g. the testnet setup's
|
||||
// pool). Both callers then read reserveA/reserveB as their own token-a/token-b directly.
|
||||
if (jStr(resolved, "defAHex") != token_a) {
|
||||
if (jStr(resolved, "status") == "ok" && jStr(resolved, "defAHex") != token_a) {
|
||||
resolved["reserveA"].swap(resolved["reserveB"]);
|
||||
resolved["defAHex"].swap(resolved["defBHex"]);
|
||||
resolved["vaultAId"].swap(resolved["vaultBId"]);
|
||||
}
|
||||
return resolved; // { exists:true, reserveA, reserveB, feeBps } in the caller's order
|
||||
return resolved;
|
||||
}
|
||||
|
||||
LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex,
|
||||
|
||||
@@ -30,15 +30,15 @@ public:
|
||||
AmmModuleImpl() = default;
|
||||
~AmmModuleImpl() = default;
|
||||
|
||||
/// Derives the pool PDAs (config / pool / vaults / current-tick) for the
|
||||
/// (def_a_hex, def_b_hex) pair and reads the pool's on-chain reserves.
|
||||
/// On success: `{ exists:true, reserveA, reserveB, feeBps }` (reserveA/
|
||||
/// reserveB in the pool's canonical def order). Otherwise
|
||||
/// `{ exists:false, error:<code> }`: `no_program_bin` (AMM_PROGRAM_BIN
|
||||
/// unset/unreadable/bad), `amm_not_initialized` (config undecodable),
|
||||
/// `bad_config` (bad ids / internal decode failure), `same_token_pair`, or
|
||||
/// `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);
|
||||
/// Derives the pool PDA for the (def_a_hex, def_b_hex) pair and reads/decodes the
|
||||
/// pool account. On success: `{ status:"ok", error:"", poolId, defAHex, defBHex,
|
||||
/// vaultAId, vaultBId, lpDefinitionId, reserveA, reserveB, liquiditySupply, feeBps }`
|
||||
/// — the A/B fields oriented to the caller's requested order (A is def_a_hex's).
|
||||
/// Otherwise `{ status:"error", error:<code> }`: `no_program_bin` (AMM_PROGRAM_BIN
|
||||
/// unset/unreadable/bad), `amm_not_initialized` (config undecodable), `bad_config`
|
||||
/// (bad ids / internal decode failure), `same_token_pair`, or `no_pool` for the
|
||||
/// ordinary "no pool / no liquidity yet" state (still carries `poolId`).
|
||||
LogosMap resolvePoolAccount(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,
|
||||
|
||||
Reference in New Issue
Block a user