diff --git a/apps/amm/qml/components/swap/SwapCard.qml b/apps/amm/qml/components/swap/SwapCard.qml index eaf9214..b7615e1 100644 --- a/apps/amm/qml/components/swap/SwapCard.qml +++ b/apps/amm/qml/components/swap/SwapCard.qml @@ -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 diff --git a/apps/amm/qml/state/NewPositionFlow.qml b/apps/amm/qml/state/NewPositionFlow.qml index 9024f07..02926bf 100644 --- a/apps/amm/qml/state/NewPositionFlow.qml +++ b/apps/amm/qml/state/NewPositionFlow.qml @@ -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 diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index c4ec371..d1aaa9a 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -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, diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index d6932d5..957abf6 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -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; diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep index dfd7a31..639e85d 100644 --- a/apps/amm/src/AmmUiBackend.rep +++ b/apps/amm/src/AmmUiBackend.rep @@ -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: }` — `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 diff --git a/modules/amm/README.md b/modules/amm/README.md index a181191..e44b365 100644 --- a/modules/amm/README.md +++ b/modules/amm/README.md @@ -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()`. diff --git a/modules/amm/ffi/src/api/swap.rs b/modules/amm/ffi/src/api/swap.rs index 8473aae..db641dd 100644 --- a/modules/amm/ffi/src/api/swap.rs +++ b/modules/amm/ffi/src/api/swap.rs @@ -71,28 +71,45 @@ pub(super) fn swap_pair(request: SwapPairRequest) -> Result { /// 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 { + // 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] diff --git a/modules/amm/src/amm_module_impl.cpp b/modules/amm/src/amm_module_impl.cpp index 28e7cfb..d8aa1f9 100644 --- a/modules/amm/src/amm_module_impl.cpp +++ b/modules/amm/src/amm_module_impl.cpp @@ -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, diff --git a/modules/amm/src/amm_module_impl.h b/modules/amm/src/amm_module_impl.h index 0464b5e..e93ae4c 100644 --- a/modules/amm/src/amm_module_impl.h +++ b/modules/amm/src/amm_module_impl.h @@ -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: }`: `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: }`: `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,