mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +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:
@@ -30,7 +30,7 @@ Rectangle {
|
||||
property string editingSide: "sell"
|
||||
property real slippageTolerancePercent: 0.5
|
||||
|
||||
// ── Pool resolution (backend.resolvePool) ───────────────────────────────
|
||||
// ── Pool resolution (backend.resolvePoolAccount) ───────────────────────────────
|
||||
// Existence and fee drive the UI; the swap quotes read the pool and
|
||||
// price/orient the swap server-side, so the client no longer prices against
|
||||
// the reserves. The raw reserves are still surfaced (observability only, not
|
||||
@@ -128,13 +128,13 @@ Rectangle {
|
||||
}
|
||||
|
||||
root.poolLoading = true
|
||||
logos.watch(root.backend.resolvePool(reqSell, reqBuy),
|
||||
logos.watch(root.backend.resolvePoolAccount(reqSell, reqBuy),
|
||||
function (pool) {
|
||||
if (isStale())
|
||||
return
|
||||
root.poolLoading = false
|
||||
root.poolResolved = true
|
||||
root.poolExists = !!(pool && pool.exists)
|
||||
root.poolExists = !!(pool && pool.status === "ok")
|
||||
root.poolReserveA = (pool && pool.reserveA) || "0"
|
||||
root.poolReserveB = (pool && pool.reserveB) || "0"
|
||||
// feeBps === 0 is a legitimate zero-fee pool; only fall back
|
||||
|
||||
@@ -84,22 +84,22 @@ QtObject {
|
||||
return
|
||||
}
|
||||
|
||||
// Route on pool existence (read the pool account), like the swap card. resolvePool
|
||||
// returns the reserves oriented to our requested token order (reserveA is tokenAId's).
|
||||
root.runtime.watch(root.backend.resolvePool(built.request.tokenAId, built.request.tokenBId),
|
||||
// Route on pool existence (read the pool account), like the swap card.
|
||||
// resolvePoolAccount returns status:"ok" with reserves oriented to our requested token
|
||||
// order (reserveA is tokenAId's), or status:"error" (no_pool / hard failure).
|
||||
root.runtime.watch(root.backend.resolvePoolAccount(built.request.tokenAId, built.request.tokenBId),
|
||||
function(pool) {
|
||||
if (serial !== root.quoteSerial)
|
||||
return
|
||||
if (pool && pool.exists) {
|
||||
if (pool && pool.status === "ok") {
|
||||
root.poolExists = true
|
||||
root.requestAddQuote(serial, built, pool)
|
||||
return
|
||||
}
|
||||
// resolvePool returns exists:false for BOTH the normal "no pool yet" case and
|
||||
// hard failures (no_program_bin, amm_not_initialized, bad_config). Only the
|
||||
// former — an empty error or no_pool — is a create-pool signal; surface any other
|
||||
// pool.error as a quote error instead of masking it as a create quote (which
|
||||
// would hide the backend failure and enable the wrong flow).
|
||||
// A status:"error" result is EITHER the normal "no pool yet" case (error
|
||||
// "no_pool") or a hard failure (no_program_bin, amm_not_initialized, bad_config).
|
||||
// Only the former is a create-pool signal; surface any other pool.error as a quote
|
||||
// error instead of masking it as a create quote (which would enable the wrong flow).
|
||||
var poolError = pool ? String(pool.error || "") : ""
|
||||
if (poolError.length === 0 || poolError === "no_pool") {
|
||||
root.poolExists = false
|
||||
|
||||
@@ -177,9 +177,9 @@ void AmmUiBackend::syncWalletState()
|
||||
setSequencerReachable(state.sequencerReachable);
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::resolvePool(QString defAHex, QString defBHex)
|
||||
QVariantMap AmmUiBackend::resolvePoolAccount(QString defAHex, QString defBHex)
|
||||
{
|
||||
return m_logos->amm_module.resolvePool(defAHex, defBHex);
|
||||
return m_logos->amm_module.resolvePoolAccount(defAHex, defBHex);
|
||||
}
|
||||
|
||||
QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
|
||||
@@ -54,7 +54,7 @@ public slots:
|
||||
void disconnectWallet() override;
|
||||
|
||||
// AMM — all forwarded to the amm_module core module.
|
||||
QVariantMap resolvePool(QString defAHex, QString defBHex) override;
|
||||
QVariantMap resolvePoolAccount(QString defAHex, QString defBHex) override;
|
||||
QString swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex,
|
||||
QString userOutputHoldingHex, QString amountInDecimal,
|
||||
QString minOutDecimal, QString deadlineDecimal) override;
|
||||
|
||||
@@ -41,13 +41,14 @@ class AmmUiBackend
|
||||
SLOT(void disconnectWallet())
|
||||
|
||||
// AMM
|
||||
// Derives the AMM pool's PDAs (config/pool/vaults/current-tick) from the
|
||||
// deployed AMM program binary (see AMM_PROGRAM_BIN — a RISC Zero
|
||||
// ProgramBinary .bin, not a raw ELF) and reads the pool's
|
||||
// on-chain reserves. Returns `{ exists: false }` if the AMM program bin
|
||||
// isn't configured, the AMM isn't initialized, or the pool has no
|
||||
// liquidity yet.
|
||||
SLOT(QVariantMap resolvePool(QString defAHex, QString defBHex))
|
||||
// Derives the AMM pool PDA for (defAHex, defBHex) from the deployed AMM program
|
||||
// binary (see AMM_PROGRAM_BIN — a RISC Zero ProgramBinary .bin, not a raw ELF)
|
||||
// and reads/decodes the pool account. On success `{ status:"ok", error:"", poolId,
|
||||
// defAHex, defBHex, vaultAId, vaultBId, lpDefinitionId, reserveA, reserveB,
|
||||
// liquiditySupply, feeBps }` (A/B oriented to the requested order); otherwise
|
||||
// `{ status:"error", error:<code> }` — `no_pool` for the ordinary "no pool /
|
||||
// no liquidity yet" state, else no_program_bin / amm_not_initialized / bad_config.
|
||||
SLOT(QVariantMap resolvePoolAccount(QString defAHex, QString defBHex))
|
||||
// Submits a real on-chain SwapExactInput transaction against the pool for
|
||||
// (defAHex, defBHex). amountInDecimal/minOutDecimal are decimal-string
|
||||
// u128 amounts in base units; deadlineDecimal is a decimal-string u64 unix
|
||||
|
||||
@@ -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