From 64e7614e742483f8f99cf567fc3e7c50bcc50ad7 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:01:55 +0200 Subject: [PATCH] refactor(amm): remove the dead newPosition quote path Both liquidity branches now quote through the lean ops (liquidityQuote / addLiquidityQuote), so quoteNewPosition and the heavy amm_quote machinery it drove are unreachable. Remove them end to end. FFI (modules/amm/ffi): - Drop the amm_quote entry point and the whole quote-evaluation graph: api/{accounts,commitment,funding,position}.rs, the QuoteRequest / PositionRequest / PairSnapshot request types, quote_error::fatal_quote, and api/clock.rs (its decode_clock was quote-only). quote.rs keeps only the shared opening-deposit math (minimum_opening_pair + helpers) that liquidity_quote reuses. - Trim the fields the quote path was the sole reader of: SelectedHolding.account and PairIds.{token_program,twap_program}. - Drop the quote-path unit tests; keep the math / pair / context / holding / swap ones (37 pass, clippy clean). Module (modules/amm/src): - Remove AmmModuleImpl::quoteNewPosition and its buildQuoteInput snapshot helper. App (apps/amm): - Remove the AmmUiBackend quoteNewPosition slot (.rep/.h/.cpp) and the dead QML backend mock + obsolete fresh-quote test. - finishSubmitFailure no longer keeps a submit-returned re-quote (the lean submit ops never return one); it always re-quotes on failure. - submissionSnapshot drops the always-empty quoteHash and derives the confirm dialog's action from the resolved pool state instead of the dead quotePayload.instruction (restores the "Create pool" / "Add liquidity" label). --- .../LiquidityConfirmationSummary.qml | 10 +- .../components/liquidity/NewPositionForm.qml | 4 +- apps/amm/qml/state/NewPositionFlow.qml | 11 +- apps/amm/src/AmmUiBackend.cpp | 5 - apps/amm/src/AmmUiBackend.h | 1 - apps/amm/src/AmmUiBackend.rep | 1 - apps/amm/tests/qml/tst_LiquidityPage.qml | 34 - modules/amm/ffi/include/amm_ffi.h | 2 - modules/amm/ffi/src/api/accounts.rs | 226 ------ modules/amm/ffi/src/api/clock.rs | 12 - modules/amm/ffi/src/api/commitment.rs | 50 -- modules/amm/ffi/src/api/funding.rs | 106 --- modules/amm/ffi/src/api/holding.rs | 7 +- modules/amm/ffi/src/api/mod.rs | 17 +- modules/amm/ffi/src/api/pair.rs | 4 - modules/amm/ffi/src/api/position.rs | 148 ---- modules/amm/ffi/src/api/quote.rs | 646 +----------------- modules/amm/ffi/src/api/quote_error.rs | 14 - modules/amm/ffi/src/api/request.rs | 48 -- modules/amm/ffi/src/api/tests.rs | 328 +-------- modules/amm/ffi/src/ffi.rs | 7 +- modules/amm/ffi/src/lib.rs | 8 +- modules/amm/src/amm_module_impl.cpp | 67 -- modules/amm/src/amm_module_impl.h | 15 - 24 files changed, 34 insertions(+), 1737 deletions(-) delete mode 100644 modules/amm/ffi/src/api/accounts.rs delete mode 100644 modules/amm/ffi/src/api/clock.rs delete mode 100644 modules/amm/ffi/src/api/commitment.rs delete mode 100644 modules/amm/ffi/src/api/funding.rs delete mode 100644 modules/amm/ffi/src/api/position.rs diff --git a/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml b/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml index cbcc3b6..e836e31 100644 --- a/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml +++ b/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml @@ -8,12 +8,8 @@ ColumnLayout { spacing: 8 - function actionText(instruction) { - if (instruction === "NewDefinition") - return qsTr("Create pool") - if (instruction === "AddLiquidity") - return qsTr("Add liquidity") - return instruction || "-" + function actionText() { + return root.snapshot.poolExists ? qsTr("Add liquidity") : qsTr("Create pool") } SummaryRow { @@ -25,7 +21,7 @@ ColumnLayout { SummaryRow { Layout.fillWidth: true label: qsTr("Action") - value: root.actionText(root.snapshot.instruction) + value: root.actionText() } SummaryRow { diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index 50077ad..1b05d52 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -1440,7 +1440,6 @@ AmmActionCard { var built = root.buildQuoteRequest() return { "request": built.request, - "quoteHash": String(root.quotePayload.quoteHash || ""), // Canonical-order holdings for the createPool / addLiquidity calls: the // request's tokenAId/amountARaw are canonical, so holdingAId must be the // canonical token A's holding too (the module re-canonicalizes as a no-op). @@ -1456,7 +1455,8 @@ AmmActionCard { "depositAText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "A"), "depositBText": root.quoteAmount("actualAmountARaw", "actualAmountBRaw", "B"), "expectedLpText": root.rawLpText(root.quotePayload.expectedLpRaw), - "instruction": String(root.quotePayload.instruction || "") + // Confirm-dialog action derives from the resolved pool state (add vs create). + "poolExists": root.activePool } } diff --git a/apps/amm/qml/state/NewPositionFlow.qml b/apps/amm/qml/state/NewPositionFlow.qml index c56e03b..1097616 100644 --- a/apps/amm/qml/state/NewPositionFlow.qml +++ b/apps/amm/qml/state/NewPositionFlow.qml @@ -431,18 +431,11 @@ QtObject { function finishSubmitFailure(result) { root.submitting = false - const hasFreshQuote = result && result.quote - && result.quote.status - if (hasFreshQuote) { - root.newPositionQuote = result.quote - root.quoteLoading = false - root.quoteStale = false - } const code = result && result.code ? result.code : "wallet_submission_failed" root.flowErrorCode = code root.submitFailed() - if (hasFreshQuote) - return + // The lean submit ops report only a status/error (never a re-quote), so a failure + // always re-quotes to refresh state against the current pool. root.scheduleQuote(true, root.pendingQuoteRequest) } diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index a28fa9e..b7f3497 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -132,11 +132,6 @@ void AmmUiBackend::refreshNewPositionContext(QVariantMap request) request, isWalletOpen(), refreshWalletAccounts)); } -QVariantMap AmmUiBackend::quoteNewPosition(QVariantMap request) -{ - return m_logos->amm_module.quoteNewPosition(request, isWalletOpen()); -} - void AmmUiBackend::syncWalletState() { const WalletUiState& state = m_walletController->state(); diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index c501145..72561d8 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -47,7 +47,6 @@ public slots: void refreshBalances() override; QString getBalance(QString accountIdHex, bool isPublic) override; void refreshNewPositionContext(QVariantMap request) override; - QVariantMap quoteNewPosition(QVariantMap request) override; // Return the new wallet's BIP39 mnemonic (empty string on failure) so the // UI can force a one-time seed-phrase backup step. QString createNewDefault(QString password) override; diff --git a/apps/amm/src/AmmUiBackend.rep b/apps/amm/src/AmmUiBackend.rep index 04159e5..d428331 100644 --- a/apps/amm/src/AmmUiBackend.rep +++ b/apps/amm/src/AmmUiBackend.rep @@ -31,7 +31,6 @@ class AmmUiBackend // transactions or duplicates quote state. PROP(QVariantMap newPositionContext READONLY) SLOT(void refreshNewPositionContext(QVariantMap request)) - SLOT(QVariantMap quoteNewPosition(QVariantMap request)) // Wallet lifecycle. createNewDefault() is the happy path: it creates a // fresh wallet at the canonical walletHome with no path picking. createNew() diff --git a/apps/amm/tests/qml/tst_LiquidityPage.qml b/apps/amm/tests/qml/tst_LiquidityPage.qml index 4edc34e..60b0fd5 100644 --- a/apps/amm/tests/qml/tst_LiquidityPage.qml +++ b/apps/amm/tests/qml/tst_LiquidityPage.qml @@ -15,19 +15,11 @@ TestCase { QtObject { property bool walletStateReady: false - property var quoteResult: ({ - "status": "ok", - "poolStatus": "missing_pool" - }) property var newPositionContext: ({ "status": "ready", "tokens": [], "feeTiers": [] }) - - function quoteNewPosition(request) { - return quoteResult - } } } @@ -120,30 +112,4 @@ TestCase { compare(page.flow.contextErrorCode, "") } - function test_submitFailureKeepsReturnedFreshQuoteWithoutRequery() { - var backend = createTemporaryObject(backendComponent, testCase, { - "walletStateReady": true - }) - var page = createTemporaryObject(pageComponent, testCase, { - "backend": backend - }) - verify(backend) - verify(page) - - page.flow.quoteSerial = 7 - page.flow.finishSubmitFailure({ - "status": "error", - "code": "quote_not_submittable", - "quote": { - "status": "ok", - "canSubmit": false, - "quoteHash": "sha256:fresh" - } - }) - - compare(page.flow.quoteSerial, 7) - compare(page.flow.newPositionQuote.quoteHash, "sha256:fresh") - compare(page.flow.quoteStale, false) - } - } diff --git a/modules/amm/ffi/include/amm_ffi.h b/modules/amm/ffi/include/amm_ffi.h index 5477821..2cc0e24 100644 --- a/modules/amm/ffi/include/amm_ffi.h +++ b/modules/amm/ffi/include/amm_ffi.h @@ -22,8 +22,6 @@ char *amm_pair_ids(const char *request_json); char *amm_context(const char *request_json); -char *amm_quote(const char *request_json); - char *amm_swap_pair(const char *request_json); char *amm_resolve_pool(const char *request_json); diff --git a/modules/amm/ffi/src/api/accounts.rs b/modules/amm/ffi/src/api/accounts.rs deleted file mode 100644 index a1c3c88..0000000 --- a/modules/amm/ffi/src/api/accounts.rs +++ /dev/null @@ -1,226 +0,0 @@ -use amm_core::PoolDefinition; -use nssa_core::program::ProgramId; - -use super::{ - funding::{append_holding_source, sources_from_reads}, - pair::PairIds, - position::{AccountPlan, AccountPlanHoldings, AccountPlanRow}, - QuoteRequest, -}; - -pub(super) fn missing_account_plan( - input: &QuoteRequest, - pair: PairIds, - amm_program: ProgramId, - holdings: AccountPlanHoldings<'_>, -) -> Result { - let mut sources = sources_from_reads(&[ - ("config", &input.snapshot.config), - ("token_a", &input.snapshot.token_a), - ("token_b", &input.snapshot.token_b), - ("pool", &input.snapshot.pool), - ("vault_a", &input.snapshot.vault_a), - ("vault_b", &input.snapshot.vault_b), - ("lp_definition", &input.snapshot.lp_definition), - ("lp_lock_holding", &input.snapshot.lp_lock_holding), - ("current_tick", &input.snapshot.current_tick), - ])?; - append_holding_source(&mut sources, "holding_a", holdings.token_a); - append_holding_source(&mut sources, "holding_b", holdings.token_b); - Ok(AccountPlan { - rows: vec![ - AccountPlanRow::new( - "config", - Some(pair.config), - Some(amm_program), - "read", - false, - false, - ), - AccountPlanRow::new( - "pool", - Some(pair.pool), - Some(amm_program), - "create", - false, - true, - ), - AccountPlanRow::new( - "vault_a", - Some(pair.vault_a), - Some(pair.token_program), - "create", - false, - true, - ), - AccountPlanRow::new( - "vault_b", - Some(pair.vault_b), - Some(pair.token_program), - "create", - false, - true, - ), - AccountPlanRow::new( - "lp_definition", - Some(pair.lp_definition), - Some(pair.token_program), - "create", - false, - true, - ), - AccountPlanRow::new( - "lp_lock_holding", - Some(pair.lp_lock_holding), - Some(pair.token_program), - "create", - false, - true, - ), - AccountPlanRow::new( - "user_holding_a", - holdings.token_a.map(|value| value.id), - Some(pair.token_program), - "update", - true, - false, - ), - AccountPlanRow::new( - "user_holding_b", - holdings.token_b.map(|value| value.id), - Some(pair.token_program), - "update", - true, - false, - ), - AccountPlanRow::new( - "user_holding_lp", - None, - Some(pair.token_program), - "create", - true, - true, - ), - AccountPlanRow::new( - "current_tick", - Some(pair.current_tick), - Some(pair.twap_program), - "create", - false, - true, - ), - AccountPlanRow::new("clock", Some(pair.clock), None, "read", false, false), - ], - sources, - }) -} - -pub(super) fn active_account_plan( - input: &QuoteRequest, - pair: PairIds, - amm_program: ProgramId, - pool: &PoolDefinition, - stored_reversed: bool, - holdings: AccountPlanHoldings<'_>, -) -> Result { - let (stored_holding_a, stored_holding_b) = if stored_reversed { - (holdings.token_b, holdings.token_a) - } else { - (holdings.token_a, holdings.token_b) - }; - let mut sources = sources_from_reads(&[ - ("config", &input.snapshot.config), - ("token_a", &input.snapshot.token_a), - ("token_b", &input.snapshot.token_b), - ("pool", &input.snapshot.pool), - ("vault_a", &input.snapshot.vault_a), - ("vault_b", &input.snapshot.vault_b), - ("lp_definition", &input.snapshot.lp_definition), - ("current_tick", &input.snapshot.current_tick), - ])?; - append_holding_source(&mut sources, "holding_a", holdings.token_a); - append_holding_source(&mut sources, "holding_b", holdings.token_b); - append_holding_source(&mut sources, "holding_lp", holdings.lp); - Ok(AccountPlan { - rows: vec![ - AccountPlanRow::new( - "config", - Some(pair.config), - Some(amm_program), - "read", - false, - false, - ), - AccountPlanRow::new( - "pool", - Some(pair.pool), - Some(amm_program), - "update", - false, - false, - ), - AccountPlanRow::new( - "vault_a", - Some(pool.vault_a_id), - Some(pair.token_program), - "update", - false, - false, - ), - AccountPlanRow::new( - "vault_b", - Some(pool.vault_b_id), - Some(pair.token_program), - "update", - false, - false, - ), - AccountPlanRow::new( - "lp_definition", - Some(pair.lp_definition), - Some(pair.token_program), - "update", - false, - false, - ), - AccountPlanRow::new( - "user_holding_a", - stored_holding_a.map(|value| value.id), - Some(pair.token_program), - "update", - true, - false, - ), - AccountPlanRow::new( - "user_holding_b", - stored_holding_b.map(|value| value.id), - Some(pair.token_program), - "update", - true, - false, - ), - AccountPlanRow::new( - "user_holding_lp", - holdings.lp.map(|value| value.id), - Some(pair.token_program), - if holdings.lp.is_some() { - "update" - } else { - "create" - }, - holdings.lp.is_none(), - holdings.lp.is_none(), - ), - AccountPlanRow::new( - "current_tick", - Some(pair.current_tick), - Some(pair.twap_program), - "update", - false, - false, - ), - AccountPlanRow::new("clock", Some(pair.clock), None, "read", false, false), - ], - sources, - }) -} diff --git a/modules/amm/ffi/src/api/clock.rs b/modules/amm/ffi/src/api/clock.rs deleted file mode 100644 index 358750d..0000000 --- a/modules/amm/ffi/src/api/clock.rs +++ /dev/null @@ -1,12 +0,0 @@ -use borsh::from_slice; -use clock_core::ClockAccountData; -use nssa_core::account::AccountId; - -use crate::account::{decode_account, AccountRead}; - -pub(super) fn decode_clock(read: &AccountRead) -> Result<(AccountId, ClockAccountData), String> { - let (id, account) = decode_account(read)?; - let clock = from_slice(account.data.as_ref()) - .map_err(|error| format!("invalid clock account: {error}"))?; - Ok((id, clock)) -} diff --git a/modules/amm/ffi/src/api/commitment.rs b/modules/amm/ffi/src/api/commitment.rs deleted file mode 100644 index c30d855..0000000 --- a/modules/amm/ffi/src/api/commitment.rs +++ /dev/null @@ -1,50 +0,0 @@ -use borsh::BorshSerialize; - -#[derive(BorshSerialize)] -pub(super) enum RequestCommitment { - Missing { - amount_a: u128, - amount_b: u128, - }, - Active { - max_a: u128, - max_b: u128, - slippage_bps: u32, - }, -} - -#[derive(BorshSerialize)] -pub(super) struct SourceCommitment { - pub(super) role: String, - pub(super) commitment: [u8; 32], -} - -#[derive(BorshSerialize)] -pub(super) struct FundingCommitment { - pub(super) token_id: [u8; 32], - pub(super) holding_id: Option<[u8; 32]>, - pub(super) available: u128, - pub(super) requested: u128, -} - -#[derive(BorshSerialize)] -pub(super) struct QuoteCommitment { - pub(super) network_id: String, - pub(super) network_fingerprint: String, - pub(super) amm_program_id: [u8; 32], - pub(super) token_a_id: [u8; 32], - pub(super) token_b_id: [u8; 32], - pub(super) fee_bps: u32, - pub(super) pool_status: u8, - pub(super) request: RequestCommitment, - pub(super) max_a: u128, - pub(super) max_b: u128, - pub(super) actual_a: u128, - pub(super) actual_b: u128, - pub(super) expected_lp: u128, - pub(super) lp_guard: u128, - pub(super) requires_fresh_lp: bool, - pub(super) sources: Vec, - pub(super) funding: Vec, - pub(super) warnings: Vec, -} diff --git a/modules/amm/ffi/src/api/funding.rs b/modules/amm/ffi/src/api/funding.rs deleted file mode 100644 index b328c4c..0000000 --- a/modules/amm/ffi/src/api/funding.rs +++ /dev/null @@ -1,106 +0,0 @@ -use nssa_core::Commitment; -use serde_json::{json, Value}; -use sha2::{Digest as _, Sha256}; - -use super::{ - commitment::{FundingCommitment, QuoteCommitment, SourceCommitment}, - holding::SelectedHolding, - pair::PairIds, - quote_error::issue, -}; -use crate::account::{decode_account, AccountRead}; - -pub(super) fn funding_issues( - wallet_available: bool, - pair: PairIds, - holding_a: &Option, - requested_a: u128, - holding_b: &Option, - requested_b: u128, - fields: [&str; 2], -) -> Vec { - if !wallet_available { - return vec![issue( - "no_wallet", - "Connect a wallet to submit.", - &[], - json!({}), - )]; - } - let mut errors = Vec::new(); - for (token_id, holding, requested, field) in [ - (pair.token_a, holding_a, requested_a, fields[0]), - (pair.token_b, holding_b, requested_b, fields[1]), - ] { - let available = holding.as_ref().map_or(0, |value| value.balance); - if available < requested { - errors.push(issue( - "amount_exceeds_balance", - "Amount exceeds the selected wallet holding balance.", - &[field], - json!({ - "requestedRaw": requested.to_string(), - "availableRaw": available.to_string(), - "holdingFound": holding.is_some(), - "tokenId": token_id.to_string(), - }), - )); - } - } - errors -} - -pub(super) fn funding_commitments( - pair: PairIds, - holding_a: &Option, - requested_a: u128, - holding_b: &Option, - requested_b: u128, -) -> Vec { - [ - (pair.token_a, holding_a, requested_a), - (pair.token_b, holding_b, requested_b), - ] - .into_iter() - .map(|(token_id, holding, requested)| FundingCommitment { - token_id: token_id.into_value(), - holding_id: holding.as_ref().map(|value| value.id.into_value()), - available: holding.as_ref().map_or(0, |value| value.balance), - requested, - }) - .collect() -} - -pub(super) fn sources_from_reads( - reads: &[(&str, &AccountRead)], -) -> Result, String> { - reads - .iter() - .map(|(role, read)| { - let (id, account) = decode_account(read)?; - Ok(SourceCommitment { - role: String::from(*role), - commitment: Commitment::new(&id, &account).to_byte_array(), - }) - }) - .collect() -} - -pub(super) fn append_holding_source( - sources: &mut Vec, - role: &str, - holding: Option<&SelectedHolding>, -) { - if let Some(holding) = holding { - sources.push(SourceCommitment { - role: String::from(role), - commitment: Commitment::new(&holding.id, &holding.account).to_byte_array(), - }); - } -} - -pub(super) fn hash_quote(commitment: &QuoteCommitment) -> Result { - let bytes = borsh::to_vec(commitment) - .map_err(|error| format!("quote commitment serialization failed: {error}"))?; - Ok(format!("sha256:{}", hex::encode(Sha256::digest(bytes)))) -} diff --git a/modules/amm/ffi/src/api/holding.rs b/modules/amm/ffi/src/api/holding.rs index 8481040..7f73b97 100644 --- a/modules/amm/ffi/src/api/holding.rs +++ b/modules/amm/ffi/src/api/holding.rs @@ -1,7 +1,4 @@ -use nssa_core::{ - account::{Account, AccountId}, - program::ProgramId, -}; +use nssa_core::{account::AccountId, program::ProgramId}; use token_core::TokenHolding; use crate::account::{decode_account, AccountRead}; @@ -11,7 +8,6 @@ pub(super) struct SelectedHolding { pub(super) id: AccountId, pub(super) definition_id: AccountId, pub(super) balance: u128, - pub(super) account: Account, } pub(super) fn wallet_holdings( @@ -44,7 +40,6 @@ pub(super) fn decode_fungible_holding( id, definition_id, balance, - account, }) } diff --git a/modules/amm/ffi/src/api/mod.rs b/modules/amm/ffi/src/api/mod.rs index 6110927..6b30a72 100644 --- a/modules/amm/ffi/src/api/mod.rs +++ b/modules/amm/ffi/src/api/mod.rs @@ -1,15 +1,10 @@ //! Transport-independent AMM client operations. -mod accounts; -mod clock; -mod commitment; mod config; mod context; -mod funding; mod holding; mod liquidity; mod pair; -mod position; mod quote; mod quote_error; mod request; @@ -23,10 +18,9 @@ use std::{error::Error, fmt}; pub use request::{ AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, ContextRequest, - CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, PairSnapshot, PoolIdRequest, - PositionRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest, - SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, - TokenHoldingsRequest, TokenIdsRequest, + CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, + ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, + SwapExactOutQuoteRequest, SwapPairRequest, TokenHoldingsRequest, TokenIdsRequest, }; use serde_json::Value; @@ -86,11 +80,6 @@ pub fn context(request: ContextRequest) -> AmmResult { context::context(request).map_err(Into::into) } -/// Evaluates a pool-creation or add-liquidity request. -pub fn quote(request: QuoteRequest) -> AmmResult { - quote::quote(request).map_err(Into::into) -} - /// Derives the canonical account ids for a swap pair (tokens in either order). pub fn swap_pair(request: SwapPairRequest) -> AmmResult { swap::swap_pair(request).map_err(Into::into) diff --git a/modules/amm/ffi/src/api/pair.rs b/modules/amm/ffi/src/api/pair.rs index f6cb3c7..89972a0 100644 --- a/modules/amm/ffi/src/api/pair.rs +++ b/modules/amm/ffi/src/api/pair.rs @@ -22,8 +22,6 @@ pub(super) struct PairIds { pub(super) lp_lock_holding: AccountId, pub(super) current_tick: AccountId, pub(super) clock: AccountId, - pub(super) token_program: ProgramId, - pub(super) twap_program: ProgramId, } pub(super) fn pair_ids(request: PairIdsRequest) -> Result { @@ -71,8 +69,6 @@ pub(super) fn derive_pair( lp_lock_holding: compute_lp_lock_holding_pda(amm_program, pool), current_tick: compute_current_tick_account_pda(config.twap_oracle_program_id, pool), clock: CLOCK_01_PROGRAM_ACCOUNT_ID, - token_program: config.token_program_id, - twap_program: config.twap_oracle_program_id, }) } diff --git a/modules/amm/ffi/src/api/position.rs b/modules/amm/ffi/src/api/position.rs deleted file mode 100644 index 79de99f..0000000 --- a/modules/amm/ffi/src/api/position.rs +++ /dev/null @@ -1,148 +0,0 @@ -use nssa_core::{account::AccountId, program::ProgramId}; -use serde_json::{json, Value}; - -use super::{ - commitment::SourceCommitment, holding::SelectedHolding, pair::PairIds, quote_error::issue, - PairSnapshot, PositionRequest, -}; -use crate::account::{account_id_from_hex, program_id_base58}; - -pub(super) struct EvaluatedQuote { - pub(super) value: Value, -} - -pub(super) enum QuoteComputation { - Failed(QuoteFailure), - Evaluated(EvaluatedQuote), -} - -pub(super) struct QuoteFailure { - pub(super) code: &'static str, - pub(super) fields: Vec<&'static str>, - pub(super) details: Value, -} - -pub(super) struct AccountPlan { - pub(super) rows: Vec, - pub(super) sources: Vec, -} - -pub(super) struct AccountPlanRow { - pub(super) role: &'static str, - pub(super) account_id: Option, - pub(super) expected_program: Option, - pub(super) action: &'static str, - pub(super) signer: bool, - pub(super) init: bool, -} - -pub(super) struct AccountPlanHoldings<'a> { - pub(super) token_a: Option<&'a SelectedHolding>, - pub(super) token_b: Option<&'a SelectedHolding>, - pub(super) lp: Option<&'a SelectedHolding>, -} - -impl QuoteComputation { - pub(super) fn into_value(self, request: &PositionRequest) -> Value { - match self { - Self::Failed(failure) => failure.into_value(request), - Self::Evaluated(EvaluatedQuote { value, .. }) => value, - } - } -} - -impl QuoteFailure { - pub(super) fn into_value(self, request: &PositionRequest) -> Value { - json!({ - "status": "error", - "canSubmit": false, - "code": self.code, - "poolStatus": "unavailable_pool", - "tokenAId": request.token_a_id, - "tokenBId": request.token_b_id, - "accountPreview": [], - "errors": [issue( - self.code, - "Position quote is unavailable.", - &self.fields, - self.details, - )], - "warnings": [], - }) - } -} - -impl AccountPlan { - pub(super) fn validate_snapshot_ids( - pair: &PairIds, - snapshot: &PairSnapshot, - ) -> Option<&'static str> { - let expected = [ - (&snapshot.config, pair.config, "config"), - (&snapshot.token_a, pair.token_a, "token_a"), - (&snapshot.token_b, pair.token_b, "token_b"), - (&snapshot.pool, pair.pool, "pool"), - (&snapshot.vault_a, pair.vault_a, "vault_a"), - (&snapshot.vault_b, pair.vault_b, "vault_b"), - (&snapshot.lp_definition, pair.lp_definition, "lp_definition"), - ( - &snapshot.lp_lock_holding, - pair.lp_lock_holding, - "lp_lock_holding", - ), - (&snapshot.current_tick, pair.current_tick, "current_tick"), - (&snapshot.clock, pair.clock, "clock"), - ]; - expected.into_iter().find_map(|(read, id, role)| { - match account_id_from_hex(&read.id, "account id") { - Ok(actual) if actual == id => None, - _ => Some(role), - } - }) - } - - pub(super) fn preview(&self) -> Vec { - self.rows - .iter() - .enumerate() - .map(|(order, row)| row.preview(order)) - .collect() - } - - pub(super) fn take_sources(&mut self) -> Vec { - std::mem::take(&mut self.sources) - } -} - -impl AccountPlanRow { - pub(super) fn new( - role: &'static str, - account_id: Option, - expected_program: Option, - action: &'static str, - signer: bool, - init: bool, - ) -> Self { - Self { - role, - account_id, - expected_program, - action, - signer, - init, - } - } - - fn preview(&self, order: usize) -> Value { - json!({ - "order": order, - "role": self.role, - "accountId": self.account_id.map(|id| id.to_string()), - "expectedProgramId": self.expected_program.map(program_id_base58), - "action": self.action, - "writable": self.action != "read", - "signer": self.signer, - "init": self.init, - }) - } -} diff --git a/modules/amm/ffi/src/api/quote.rs b/modules/amm/ffi/src/api/quote.rs index 70b2ad3..2c725a4 100644 --- a/modules/amm/ffi/src/api/quote.rs +++ b/modules/amm/ffi/src/api/quote.rs @@ -1,643 +1,17 @@ +//! Shared opening-deposit math for pool creation. +//! +//! Reused by `liquidity::liquidity_quote` to size the smallest deposit that clears +//! `MINIMUM_LIQUIDITY` for a given opening price. + use alloy_primitives::U256; -use amm_core::{ - is_supported_fee_tier, isqrt_product, mul_div_floor, spot_price_q64_64, PoolDefinition, - FEE_BPS_DENOMINATOR, MINIMUM_LIQUIDITY, -}; -use nssa_core::{ - account::{Account, AccountId}, - program::ProgramId, -}; -use serde_json::{json, Value}; -use token_core::TokenDefinition; -use twap_oracle_core::CurrentTickAccount; +use amm_core::MINIMUM_LIQUIDITY; -use super::{ - accounts::{active_account_plan, missing_account_plan}, - clock::decode_clock, - commitment::{QuoteCommitment, RequestCommitment}, - context::fungible_definition, - funding::{funding_commitments, funding_issues, hash_quote}, - holding::{decode_fungible_holding, select_holding, wallet_holdings}, - pair::{derive_pair, is_canonical_pair, PairIds}, - position::{AccountPlan, AccountPlanHoldings, EvaluatedQuote, QuoteComputation}, - quote_error::{fatal_quote, issue}, - QuoteRequest, -}; -use crate::account::{ - decode_account, parse_base58_id, parse_program_id, program_id_bytes, AccountRead, -}; - -const DEFAULT_SLIPPAGE_BPS: u32 = 50; -const MAX_SLIPPAGE_BPS: u32 = 5_000; -const HIGH_SLIPPAGE_BPS: u32 = 2_000; +/// `Q64.64` scaling factor (`2^64`). pub(super) const Q64: u128 = 1_u128 << 64; -pub(super) fn quote(request: QuoteRequest) -> Result { - Ok(compute_quote(&request)?.into_value(&request.request)) -} - -pub(super) fn compute_quote(input: &QuoteRequest) -> Result { - let amm_program = parse_program_id(&input.amm_program_id)?; - let token_a = match parse_base58_id(&input.request.token_a_id, "token A id") { - Ok(id) => id, - Err(_) => return Ok(fatal_quote("invalid_token_id", &["tokenAId"], json!({}))), - }; - let token_b = match parse_base58_id(&input.request.token_b_id, "token B id") { - Ok(id) => id, - Err(_) => return Ok(fatal_quote("invalid_token_id", &["tokenBId"], json!({}))), - }; - if token_a == token_b { - return Ok(fatal_quote( - "same_token_pair", - &["tokenAId", "tokenBId"], - json!({}), - )); - } - if !is_canonical_pair(token_a, token_b) { - return Ok(fatal_quote( - "non_canonical_pair", - &["tokenAId", "tokenBId"], - json!({}), - )); - } - if !is_supported_fee_tier(u128::from(input.request.fee_bps)) { - return Ok(fatal_quote( - "invalid_fee_tier", - &["feeBps"], - json!({ "feeBps": input.request.fee_bps }), - )); - } - - let pair = match derive_pair(amm_program, token_a, token_b, &input.snapshot.config) { - Ok(pair) => pair, - Err(_) => return Ok(fatal_quote("config_unavailable", &[], json!({}))), - }; - if let Some(error) = AccountPlan::validate_snapshot_ids(&pair, &input.snapshot) { - return Ok(fatal_quote( - "account_read_failed", - &[], - json!({ "role": error }), - )); - } - for (read, token_id, field) in [ - (&input.snapshot.token_a, token_a, "tokenAId"), - (&input.snapshot.token_b, token_b, "tokenBId"), - ] { - if let Err(error) = fungible_definition(Some(read), token_id, pair.token_program) { - return Ok(fatal_quote( - error.code, - &[field], - json!({ "tokenId": token_id.to_string() }), - )); - } - } - - let (_, pool_account) = match decode_account(&input.snapshot.pool) { - Ok(value) => value, - Err(_) => { - return Ok(fatal_quote( - "account_read_failed", - &[], - json!({ "role": "pool", "accountId": pair.pool.to_string() }), - )) - } - }; - if pool_account == Account::default() { - compute_missing_quote(input, amm_program, pair) - } else { - compute_active_quote(input, amm_program, pair, pool_account) - } -} - -fn compute_missing_quote( - input: &QuoteRequest, - amm_program: ProgramId, - pair: PairIds, -) -> Result { - for (read, role) in [ - (&input.snapshot.vault_a, "vault_a"), - (&input.snapshot.vault_b, "vault_b"), - (&input.snapshot.lp_definition, "lp_definition"), - (&input.snapshot.lp_lock_holding, "lp_lock_holding"), - (&input.snapshot.current_tick, "current_tick"), - ] { - let Ok((_, account)) = decode_account(read) else { - return Ok(fatal_quote( - "account_read_failed", - &[], - json!({ "role": role }), - )); - }; - if account != Account::default() { - return Ok(fatal_quote( - "pool_unavailable", - &[], - json!({ "role": role }), - )); - } - } - if !valid_clock(&input.snapshot.clock, pair.clock) { - return Ok(fatal_quote( - "account_read_failed", - &[], - json!({ "role": "clock" }), - )); - } - - let requested_price = match raw_value(input.request.initial_price_real_raw.as_deref()) { - Ok(value) if value > 0 => value, - Ok(_) => { - return Ok(fatal_quote( - "amount_must_be_positive", - &["initialPriceRealRaw"], - json!({}), - )) - } - Err(code) => return Ok(fatal_quote(code, &["initialPriceRealRaw"], json!({}))), - }; - let (minimum_a, minimum_b) = minimum_opening_pair(requested_price)?; - let direct_amounts = - input.request.amount_a_raw.is_some() || input.request.amount_b_raw.is_some(); - let (amount_a, amount_b) = if direct_amounts { - let amount_a = match raw_value(input.request.amount_a_raw.as_deref()) { - Ok(value) if value > 0 => value, - Ok(_) => { - return Ok(fatal_quote( - "amount_must_be_positive", - &["amountARaw"], - json!({}), - )) - } - Err(code) => return Ok(fatal_quote(code, &["amountARaw"], json!({}))), - }; - let amount_b = match raw_value(input.request.amount_b_raw.as_deref()) { - Ok(value) if value > 0 => value, - Ok(_) => { - return Ok(fatal_quote( - "amount_must_be_positive", - &["amountBRaw"], - json!({}), - )) - } - Err(code) => return Ok(fatal_quote(code, &["amountBRaw"], json!({}))), - }; - if spot_price_q64_64(amount_a, amount_b) != requested_price { - return Ok(fatal_quote( - "deposit_ratio_mismatch", - &["amountARaw", "amountBRaw"], - json!({}), - )); - } - (amount_a, amount_b) - } else { - (minimum_a, minimum_b) - }; - let initial_lp = isqrt_product(amount_a, amount_b); - if initial_lp <= MINIMUM_LIQUIDITY { - return Ok(fatal_quote( - "amount_too_low", - &["amountARaw", "amountBRaw"], - json!({ "minimumLiquidityRaw": MINIMUM_LIQUIDITY.to_string() }), - )); - } - let expected_lp = initial_lp - MINIMUM_LIQUIDITY; - - let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program); - let holding_a = select_holding(&holdings, pair.token_a); - let holding_b = select_holding(&holdings, pair.token_b); - let funding = funding_issues( - input.snapshot.wallet_available, - pair, - &holding_a, - amount_a, - &holding_b, - amount_b, - ["amountARaw", "amountBRaw"], - ); - let can_submit = funding.is_empty(); - let mut account_plan = missing_account_plan( - input, - pair, - amm_program, - AccountPlanHoldings { - token_a: holding_a.as_ref(), - token_b: holding_b.as_ref(), - lp: None, - }, - )?; - let sources = account_plan.take_sources(); - let funding_commitment = funding_commitments(pair, &holding_a, amount_a, &holding_b, amount_b); - let commitment = QuoteCommitment { - network_id: input.network_id.clone(), - network_fingerprint: input.network_fingerprint.clone(), - amm_program_id: program_id_bytes(amm_program), - token_a_id: pair.token_a.into_value(), - token_b_id: pair.token_b.into_value(), - fee_bps: input.request.fee_bps, - pool_status: 0, - request: RequestCommitment::Missing { amount_a, amount_b }, - max_a: amount_a, - max_b: amount_b, - actual_a: amount_a, - actual_b: amount_b, - expected_lp, - lp_guard: MINIMUM_LIQUIDITY, - requires_fresh_lp: true, - sources, - funding: funding_commitment, - warnings: Vec::new(), - }; - let quote_hash = hash_quote(&commitment)?; - let preview = account_plan.preview(); - let value = json!({ - "status": "ok", - "canSubmit": can_submit, - "code": if can_submit { "ready" } else { "funding_required" }, - "poolStatus": "missing_pool", - "instruction": "NewDefinition", - "quoteHash": quote_hash, - "feeBps": input.request.fee_bps, - "poolId": pair.pool.to_string(), - "tokenAId": pair.token_a.to_string(), - "tokenBId": pair.token_b.to_string(), - "maxAmountARaw": amount_a.to_string(), - "maxAmountBRaw": amount_b.to_string(), - "actualAmountARaw": amount_a.to_string(), - "actualAmountBRaw": amount_b.to_string(), - "expectedLpRaw": expected_lp.to_string(), - "lockedLpRaw": MINIMUM_LIQUIDITY.to_string(), - "initialPriceRealRaw": spot_price_q64_64(amount_a, amount_b).to_string(), - "minimumAmountARaw": minimum_a.to_string(), - "minimumAmountBRaw": minimum_b.to_string(), - "requiresFreshLp": true, - "accountPreview": preview, - "errors": funding, - "warnings": [], - }); - Ok(QuoteComputation::Evaluated(EvaluatedQuote { value })) -} - -fn compute_active_quote( - input: &QuoteRequest, - amm_program: ProgramId, - pair: PairIds, - pool_account: Account, -) -> Result { - if pool_account.program_owner != amm_program { - return Ok(fatal_quote( - "pool_unavailable", - &[], - json!({ "reason": "owner_mismatch" }), - )); - } - let Ok(pool) = PoolDefinition::try_from(&pool_account.data) else { - return Ok(fatal_quote( - "pool_unavailable", - &[], - json!({ "reason": "invalid_pool_data" }), - )); - }; - let stored_reversed = if pool.definition_token_a_id == pair.token_a - && pool.definition_token_b_id == pair.token_b - { - false - } else if pool.definition_token_a_id == pair.token_b - && pool.definition_token_b_id == pair.token_a - { - true - } else { - return Ok(fatal_quote( - "pool_unavailable", - &[], - json!({ "reason": "pair_mismatch" }), - )); - }; - if pool.reserve_a == 0 || pool.reserve_b == 0 || pool.liquidity_pool_supply == 0 { - return Ok(fatal_quote("pool_inactive", &[], json!({}))); - } - if pool.fees != u128::from(input.request.fee_bps) { - return Ok(fatal_quote( - "fee_tier_mismatch", - &["feeBps"], - json!({ "poolFeeBps": pool.fees.to_string() }), - )); - } - if !is_supported_fee_tier(pool.fees) { - return Ok(fatal_quote( - "pool_unavailable", - &[], - json!({ "reason": "unsupported_pool_fee" }), - )); - } - - let max_a = match raw_value(input.request.max_amount_a_raw.as_deref()) { - Ok(value) if value > 0 => value, - Ok(_) => { - return Ok(fatal_quote( - "amount_must_be_positive", - &["maxAmountARaw"], - json!({}), - )) - } - Err(code) => return Ok(fatal_quote(code, &["maxAmountARaw"], json!({}))), - }; - let max_b = match raw_value(input.request.max_amount_b_raw.as_deref()) { - Ok(value) if value > 0 => value, - Ok(_) => { - return Ok(fatal_quote( - "amount_must_be_positive", - &["maxAmountBRaw"], - json!({}), - )) - } - Err(code) => return Ok(fatal_quote(code, &["maxAmountBRaw"], json!({}))), - }; - let slippage_bps = input.request.slippage_bps.unwrap_or(DEFAULT_SLIPPAGE_BPS); - if slippage_bps > MAX_SLIPPAGE_BPS { - return Ok(fatal_quote( - "invalid_slippage", - &["slippageBps"], - json!({ "maximum": MAX_SLIPPAGE_BPS }), - )); - } - - let (stored_max_a, stored_max_b) = if stored_reversed { - (max_b, max_a) - } else { - (max_a, max_b) - }; - let ideal_a = mul_div_floor(pool.reserve_a, stored_max_b, pool.reserve_b); - let ideal_b = mul_div_floor(pool.reserve_b, stored_max_a, pool.reserve_a); - let stored_actual_a = stored_max_a.min(ideal_a); - let stored_actual_b = stored_max_b.min(ideal_b); - if stored_actual_a == 0 || stored_actual_b == 0 { - return Ok(fatal_quote( - "amount_too_low", - &["maxAmountARaw", "maxAmountBRaw"], - json!({}), - )); - } - let expected_lp = - mul_div_floor(pool.liquidity_pool_supply, stored_actual_a, pool.reserve_a).min( - mul_div_floor(pool.liquidity_pool_supply, stored_actual_b, pool.reserve_b), - ); - if expected_lp == 0 { - return Ok(fatal_quote( - "amount_too_low", - &["maxAmountARaw", "maxAmountBRaw"], - json!({}), - )); - } - let minimum_lp = mul_div_floor( - expected_lp, - FEE_BPS_DENOMINATOR - u128::from(slippage_bps), - FEE_BPS_DENOMINATOR, - ); - if minimum_lp == 0 { - return Ok(fatal_quote("minimum_lp_zero", &["slippageBps"], json!({}))); - } - let (actual_a, actual_b, reserve_a, reserve_b) = if stored_reversed { - ( - stored_actual_b, - stored_actual_a, - pool.reserve_b, - pool.reserve_a, - ) - } else { - ( - stored_actual_a, - stored_actual_b, - pool.reserve_a, - pool.reserve_b, - ) - }; - - if let Some(error) = validate_active_accounts(input, pair, &pool, stored_reversed) { - return Ok(error); - } - let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program); - let holding_a = select_holding(&holdings, pair.token_a); - let holding_b = select_holding(&holdings, pair.token_b); - let lp_holding = select_holding(&holdings, pair.lp_definition); - let requires_fresh_lp = lp_holding.is_none(); - let funding = funding_issues( - input.snapshot.wallet_available, - pair, - &holding_a, - actual_a, - &holding_b, - actual_b, - ["maxAmountARaw", "maxAmountBRaw"], - ); - let can_submit = funding.is_empty(); - let warnings = if slippage_bps >= HIGH_SLIPPAGE_BPS { - vec![issue( - "high_slippage", - "High slippage tolerance.", - &["slippageBps"], - json!({ "slippageBps": slippage_bps }), - )] - } else { - Vec::new() - }; - let warning_codes = warnings - .iter() - .filter_map(|warning| warning["code"].as_str().map(String::from)) - .collect(); - let mut account_plan = active_account_plan( - input, - pair, - amm_program, - &pool, - stored_reversed, - AccountPlanHoldings { - token_a: holding_a.as_ref(), - token_b: holding_b.as_ref(), - lp: lp_holding.as_ref(), - }, - )?; - let sources = account_plan.take_sources(); - let commitment = QuoteCommitment { - network_id: input.network_id.clone(), - network_fingerprint: input.network_fingerprint.clone(), - amm_program_id: program_id_bytes(amm_program), - token_a_id: pair.token_a.into_value(), - token_b_id: pair.token_b.into_value(), - fee_bps: input.request.fee_bps, - pool_status: 1, - request: RequestCommitment::Active { - max_a, - max_b, - slippage_bps, - }, - max_a, - max_b, - actual_a, - actual_b, - expected_lp, - lp_guard: minimum_lp, - requires_fresh_lp, - sources, - funding: funding_commitments(pair, &holding_a, actual_a, &holding_b, actual_b), - warnings: warning_codes, - }; - let quote_hash = hash_quote(&commitment)?; - let preview = account_plan.preview(); - let value = json!({ - "status": "ok", - "canSubmit": can_submit, - "code": if can_submit { "ready" } else { "funding_required" }, - "poolStatus": "active_pool", - "instruction": "AddLiquidity", - "quoteHash": quote_hash, - "feeBps": input.request.fee_bps, - "poolFeeBps": pool.fees.to_string(), - "poolId": pair.pool.to_string(), - "tokenAId": pair.token_a.to_string(), - "tokenBId": pair.token_b.to_string(), - "maxAmountARaw": max_a.to_string(), - "maxAmountBRaw": max_b.to_string(), - "actualAmountARaw": actual_a.to_string(), - "actualAmountBRaw": actual_b.to_string(), - "reserveARaw": reserve_a.to_string(), - "reserveBRaw": reserve_b.to_string(), - "liquiditySupplyRaw": pool.liquidity_pool_supply.to_string(), - "expectedLpRaw": expected_lp.to_string(), - "minimumLpRaw": minimum_lp.to_string(), - "initialPriceRealRaw": spot_price_q64_64(reserve_a, reserve_b).to_string(), - "requiresFreshLp": requires_fresh_lp, - "accountPreview": preview, - "errors": funding, - "warnings": warnings, - }); - Ok(QuoteComputation::Evaluated(EvaluatedQuote { value })) -} - -fn validate_active_accounts( - input: &QuoteRequest, - pair: PairIds, - pool: &PoolDefinition, - stored_reversed: bool, -) -> Option { - let expected_vault_a = if stored_reversed { - pair.vault_b - } else { - pair.vault_a - }; - let expected_vault_b = if stored_reversed { - pair.vault_a - } else { - pair.vault_b - }; - if pool.vault_a_id != expected_vault_a - || pool.vault_b_id != expected_vault_b - || pool.liquidity_pool_id != pair.lp_definition - { - return Some(fatal_quote( - "pool_unavailable", - &[], - json!({ "reason": "pool_account_mismatch" }), - )); - } - let (canonical_vault_a, canonical_vault_b) = ( - decode_holding(&input.snapshot.vault_a, pair.token_program, pair.token_a), - decode_holding(&input.snapshot.vault_b, pair.token_program, pair.token_b), - ); - let (Ok(vault_a_balance), Ok(vault_b_balance)) = (canonical_vault_a, canonical_vault_b) else { - return Some(fatal_quote( - "account_read_failed", - &[], - json!({ "role": "vault" }), - )); - }; - let (reserve_a, reserve_b) = if stored_reversed { - (pool.reserve_b, pool.reserve_a) - } else { - (pool.reserve_a, pool.reserve_b) - }; - if vault_a_balance < reserve_a || vault_b_balance < reserve_b { - return Some(fatal_quote( - "pool_unavailable", - &[], - json!({ "reason": "vault_below_reserve" }), - )); - } - let Ok((lp_id, lp_account)) = decode_account(&input.snapshot.lp_definition) else { - return Some(fatal_quote( - "account_read_failed", - &[], - json!({ "role": "lp_definition" }), - )); - }; - if lp_id != pair.lp_definition - || lp_account.program_owner != pair.token_program - || !matches!( - TokenDefinition::try_from(&lp_account.data), - Ok(TokenDefinition::Fungible { .. }) - ) - { - return Some(fatal_quote( - "pool_unavailable", - &[], - json!({ "reason": "invalid_lp_definition" }), - )); - } - let Ok((tick_id, tick_account)) = decode_account(&input.snapshot.current_tick) else { - return Some(fatal_quote( - "account_read_failed", - &[], - json!({ "role": "current_tick" }), - )); - }; - if tick_id != pair.current_tick - || tick_account.program_owner != pair.twap_program - || CurrentTickAccount::try_from(&tick_account.data).is_err() - { - return Some(fatal_quote( - "pool_unavailable", - &[], - json!({ "reason": "invalid_current_tick" }), - )); - } - if !valid_clock(&input.snapshot.clock, pair.clock) { - return Some(fatal_quote( - "account_read_failed", - &[], - json!({ "role": "clock" }), - )); - } - None -} - -fn decode_holding( - read: &AccountRead, - token_program: ProgramId, - definition_id: AccountId, -) -> Result { - let holding = decode_fungible_holding(read, token_program)?; - if holding.definition_id != definition_id { - return Err(String::from("invalid fungible holding")); - } - Ok(holding.balance) -} - -fn valid_clock(read: &AccountRead, expected_id: AccountId) -> bool { - matches!(decode_clock(read), Ok((id, _)) if id == expected_id) -} - -fn raw_value(value: Option<&str>) -> Result { - let Some(value) = value else { - return Err("amount_required"); - }; - if value.is_empty() { - return Err("amount_required"); - } - if !value.bytes().all(|byte| byte.is_ascii_digit()) { - return Err("invalid_raw_amount"); - } - value.parse().map_err(|_| "invalid_raw_amount") -} - +/// Smallest `(amount_a, amount_b)` deposit whose geometric-mean LP clears +/// `MINIMUM_LIQUIDITY`, holding the canonical opening `price` (token B per token A, +/// `Q64.64`). Binary-searches the smaller side, then derives the paired amount by ceil. pub(super) fn minimum_opening_pair(price: u128) -> Result<(u128, u128), String> { let minimum_initial_lp = U256::from(MINIMUM_LIQUIDITY + 1); let target_product = minimum_initial_lp diff --git a/modules/amm/ffi/src/api/quote_error.rs b/modules/amm/ffi/src/api/quote_error.rs index df10ffd..435f51f 100644 --- a/modules/amm/ffi/src/api/quote_error.rs +++ b/modules/amm/ffi/src/api/quote_error.rs @@ -1,7 +1,5 @@ use serde_json::{json, Value}; -use super::position::{QuoteComputation, QuoteFailure}; - pub(super) fn issue(code: &str, message: &str, fields: &[&str], details: Value) -> Value { json!({ "code": code, @@ -11,15 +9,3 @@ pub(super) fn issue(code: &str, message: &str, fields: &[&str], details: Value) "blockingFields": fields, }) } - -pub(super) fn fatal_quote( - code: &'static str, - fields: &[&'static str], - details: Value, -) -> QuoteComputation { - QuoteComputation::Failed(QuoteFailure { - code, - fields: fields.to_vec(), - details, - }) -} diff --git a/modules/amm/ffi/src/api/request.rs b/modules/amm/ffi/src/api/request.rs index 0cfa543..0eeee7c 100644 --- a/modules/amm/ffi/src/api/request.rs +++ b/modules/amm/ffi/src/api/request.rs @@ -230,51 +230,3 @@ pub struct TokenHoldingsRequest { pub struct ProgramIdRequest { pub elf: String, } - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct PositionRequest { - pub token_a_id: String, - pub token_b_id: String, - pub fee_bps: u32, - #[serde(default)] - pub amount_a_raw: Option, - #[serde(default)] - pub amount_b_raw: Option, - #[serde(default)] - pub max_amount_a_raw: Option, - #[serde(default)] - pub max_amount_b_raw: Option, - #[serde(default)] - pub slippage_bps: Option, - #[serde(default)] - pub initial_price_real_raw: Option, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct PairSnapshot { - pub config: AccountRead, - pub token_a: AccountRead, - pub token_b: AccountRead, - pub pool: AccountRead, - pub vault_a: AccountRead, - pub vault_b: AccountRead, - pub lp_definition: AccountRead, - pub lp_lock_holding: AccountRead, - pub current_tick: AccountRead, - pub clock: AccountRead, - pub wallet_available: bool, - #[serde(default)] - pub wallet_accounts: Vec, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct QuoteRequest { - pub network_id: String, - pub network_fingerprint: String, - pub amm_program_id: String, - pub request: PositionRequest, - pub snapshot: PairSnapshot, -} diff --git a/modules/amm/ffi/src/api/tests.rs b/modules/amm/ffi/src/api/tests.rs index 0789486..f374c85 100644 --- a/modules/amm/ffi/src/api/tests.rs +++ b/modules/amm/ffi/src/api/tests.rs @@ -1,29 +1,26 @@ use alloy_primitives::U256; use amm_core::{ compute_config_pda, compute_liquidity_token_pda, compute_lp_lock_holding_pda, compute_pool_pda, - compute_vault_pda, isqrt_product, spot_price_q64_64, AmmConfig, PoolDefinition, - MINIMUM_LIQUIDITY, + compute_vault_pda, isqrt_product, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY, }; -use clock_core::{ClockAccountData, CLOCK_01_PROGRAM_ACCOUNT_ID}; +use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID; use nssa_core::{ account::{Account, AccountId, Data, Nonce}, program::ProgramId, }; use pretty_assertions::assert_eq; -use serde_json::{json, Value}; +use serde_json::json; use token_core::{TokenDefinition, TokenHolding}; -use twap_oracle_core::{compute_current_tick_account_pda, CurrentTickAccount}; +use twap_oracle_core::compute_current_tick_account_pda; use super::{ - accounts::{active_account_plan, missing_account_plan}, context::{context, token_ids}, - holding::{select_holding, wallet_holdings, SelectedHolding}, + holding::{select_holding, SelectedHolding}, pair::{is_canonical_pair, pair_ids, PairIds}, - position::AccountPlanHoldings, - quote::{div_ceil_u256, minimum_opening_pair, quote, Q64}, + quote::{div_ceil_u256, minimum_opening_pair, Q64}, swap::{swap_exact_in_plan, swap_exact_out_plan}, - ContextRequest, PairIdsRequest, PairSnapshot, PositionRequest, QuoteRequest, - SwapExactInPlanRequest, SwapExactOutPlanRequest, TokenIdsRequest, + ContextRequest, PairIdsRequest, SwapExactInPlanRequest, SwapExactOutPlanRequest, + TokenIdsRequest, }; use crate::{ account::{account_id_hex, account_read, decode_account, program_id_bytes}, @@ -80,20 +77,6 @@ fn token_holding(definition_id: AccountId, balance: u128) -> Account { ) } -fn clock_account() -> Account { - account( - [44; 8], - Data::try_from( - ClockAccountData { - block_id: 10, - timestamp: 1_000, - } - .to_bytes(), - ) - .unwrap(), - ) -} - fn ids() -> PairIds { let token_a = AccountId::new([2; 32]); let token_b = AccountId::new([1; 32]); @@ -110,45 +93,6 @@ fn ids() -> PairIds { lp_lock_holding: compute_lp_lock_holding_pda(AMM_PROGRAM, pool), current_tick: compute_current_tick_account_pda(TWAP_PROGRAM, pool), clock: CLOCK_01_PROGRAM_ACCOUNT_ID, - token_program: TOKEN_PROGRAM, - twap_program: TWAP_PROGRAM, - } -} - -fn base_snapshot(pair: PairIds) -> PairSnapshot { - let holding_a_id = AccountId::new([61; 32]); - let holding_b_id = AccountId::new([62; 32]); - PairSnapshot { - config: account_read(pair.config, &config_account()), - token_a: account_read(pair.token_a, &token_definition("A", 1_000_000)), - token_b: account_read(pair.token_b, &token_definition("B", 2_000_000)), - pool: default_read(pair.pool), - vault_a: default_read(pair.vault_a), - vault_b: default_read(pair.vault_b), - lp_definition: default_read(pair.lp_definition), - lp_lock_holding: default_read(pair.lp_lock_holding), - current_tick: default_read(pair.current_tick), - clock: account_read(pair.clock, &clock_account()), - wallet_available: true, - wallet_accounts: vec![ - account_read(holding_a_id, &token_holding(pair.token_a, 1_000_000)), - account_read(holding_b_id, &token_holding(pair.token_b, 1_000_000)), - ], - } -} - -fn request(pair: PairIds) -> PositionRequest { - assert!(is_canonical_pair(pair.token_a, pair.token_b)); - PositionRequest { - token_a_id: pair.token_a.to_string(), - token_b_id: pair.token_b.to_string(), - fee_bps: 30, - amount_a_raw: None, - amount_b_raw: None, - max_amount_a_raw: None, - max_amount_b_raw: None, - slippage_bps: None, - initial_price_real_raw: Some(Q64.to_string()), } } @@ -156,124 +100,6 @@ fn amm_program_id() -> String { hex::encode(program_id_bytes(AMM_PROGRAM)) } -struct Scenario { - pair: PairIds, - request: PositionRequest, - snapshot: PairSnapshot, - network_id: &'static str, - network_fingerprint: &'static str, -} - -impl Scenario { - fn devnet() -> Self { - Self::new("devnet", "channel:test") - } - - fn testnet() -> Self { - Self::new("testnet", "block10:test") - } - - fn new(network_id: &'static str, network_fingerprint: &'static str) -> Self { - let pair = ids(); - Self { - pair, - request: request(pair), - snapshot: base_snapshot(pair), - network_id, - network_fingerprint, - } - } - - fn quote_request(&self) -> QuoteRequest { - QuoteRequest { - network_id: String::from(self.network_id), - network_fingerprint: String::from(self.network_fingerprint), - amm_program_id: amm_program_id(), - request: self.request.clone(), - snapshot: self.snapshot.clone(), - } - } - - fn quote(&self) -> Value { - quote(self.quote_request()).unwrap() - } -} - -#[test] -fn account_plan_sources_follow_pool_branch() { - let scenario = Scenario::devnet(); - let pair = scenario.pair; - let input = scenario.quote_request(); - let holdings = wallet_holdings(&input.snapshot.wallet_accounts, pair.token_program); - let holding_a = select_holding(&holdings, pair.token_a); - let holding_b = select_holding(&holdings, pair.token_b); - - let missing = missing_account_plan( - &input, - pair, - AMM_PROGRAM, - AccountPlanHoldings { - token_a: holding_a.as_ref(), - token_b: holding_b.as_ref(), - lp: None, - }, - ) - .unwrap(); - assert_eq!( - missing - .sources - .iter() - .map(|source| source.role.as_str()) - .collect::>(), - vec![ - "config", - "token_a", - "token_b", - "pool", - "vault_a", - "vault_b", - "lp_definition", - "lp_lock_holding", - "current_tick", - "holding_a", - "holding_b", - ] - ); - - let active = active_account_plan( - &input, - pair, - AMM_PROGRAM, - &PoolDefinition::default(), - false, - AccountPlanHoldings { - token_a: holding_a.as_ref(), - token_b: holding_b.as_ref(), - lp: None, - }, - ) - .unwrap(); - assert_eq!( - active - .sources - .iter() - .map(|source| source.role.as_str()) - .collect::>(), - vec![ - "config", - "token_a", - "token_b", - "pool", - "vault_a", - "vault_b", - "lp_definition", - "current_tick", - "holding_a", - "holding_b", - ] - ); -} - #[test] fn minimum_pair_exceeds_protocol_lock() { for price in [1, Q64 / 2_500, Q64 / 10, Q64, Q64 * 2, u128::MAX] { @@ -304,13 +130,6 @@ fn highest_balance_holding_wins_then_lowest_id() { id: AccountId::new([id; 32]), definition_id: definition, balance, - account: account( - TOKEN_PROGRAM, - Data::from(&TokenHolding::Fungible { - definition_id: definition, - balance, - }), - ), }; let selected = select_holding( &[holding(4, 10), holding(2, 20), holding(1, 20)], @@ -481,137 +300,6 @@ fn missing_pool_snapshot_defaults_remain_real_accounts() { assert_eq!(decoded, Account::default()); } -#[test] -fn missing_pool_quote_uses_current_account_order() { - let scenario = Scenario::devnet(); - let quote_value = scenario.quote(); - assert_eq!(quote_value["status"], "ok"); - assert_eq!(quote_value["poolStatus"], "missing_pool"); - assert_eq!(quote_value["canSubmit"], true); - // The 11-account NewDefinition preview order (config, pool, vaults, lp def/lock, - // holdings a/b/lp, current_tick, clock) — the create submit consumes the same shape. - let preview = quote_value["accountPreview"].as_array().unwrap(); - assert_eq!(preview.len(), 11); - assert_eq!(preview[6]["role"], "user_holding_a"); - assert_eq!(preview[6]["signer"], true); - assert_eq!(preview[7]["signer"], true); - assert_eq!(preview[8]["role"], "user_holding_lp"); - assert_eq!(preview[8]["signer"], true); -} - -#[test] -fn missing_pool_quote_accepts_large_direct_raw_amounts() { - let mut scenario = Scenario::devnet(); - let amount_a = 100_000_000; - let amount_b = 150_000_000; - scenario.request.amount_a_raw = Some(amount_a.to_string()); - scenario.request.amount_b_raw = Some(amount_b.to_string()); - scenario.request.initial_price_real_raw = - Some(spot_price_q64_64(amount_a, amount_b).to_string()); - - let quote_value = scenario.quote(); - - assert_eq!(quote_value["status"], "ok"); - assert_eq!(quote_value["actualAmountARaw"], amount_a.to_string()); - assert_eq!(quote_value["actualAmountBRaw"], amount_b.to_string()); - assert!(quote_value.get("depositScaleBps").is_none()); -} - -#[test] -fn active_pool_quote_uses_ratio_and_existing_lp_holding() { - let mut scenario = Scenario::testnet(); - let pair = scenario.pair; - let pool = PoolDefinition { - definition_token_a_id: pair.token_a, - definition_token_b_id: pair.token_b, - vault_a_id: pair.vault_a, - vault_b_id: pair.vault_b, - liquidity_pool_id: pair.lp_definition, - liquidity_pool_supply: 10_000, - reserve_a: 10_000, - reserve_b: 20_000, - fees: 30, - }; - scenario.snapshot.pool = account_read(pair.pool, &account(AMM_PROGRAM, Data::from(&pool))); - scenario.snapshot.vault_a = - account_read(pair.vault_a, &token_holding(pair.token_a, pool.reserve_a)); - scenario.snapshot.vault_b = - account_read(pair.vault_b, &token_holding(pair.token_b, pool.reserve_b)); - scenario.snapshot.lp_definition = account_read( - pair.lp_definition, - &account( - TOKEN_PROGRAM, - Data::from(&TokenDefinition::Fungible { - name: String::from("LP"), - total_supply: pool.liquidity_pool_supply, - metadata_id: None, - authority: Some(pair.lp_definition), - }), - ), - ); - scenario.snapshot.current_tick = account_read( - pair.current_tick, - &account( - TWAP_PROGRAM, - Data::from(&CurrentTickAccount { - tick: 0, - last_updated: 1_000, - }), - ), - ); - scenario.snapshot.wallet_accounts = vec![ - account_read( - AccountId::new([61; 32]), - &token_holding(pair.token_a, 1_000), - ), - account_read( - AccountId::new([62; 32]), - &token_holding(pair.token_b, 2_000), - ), - ]; - let lp_holding = AccountId::new([64; 32]); - scenario.snapshot.wallet_accounts.push(account_read( - lp_holding, - &token_holding(pair.lp_definition, 500), - )); - scenario.request.initial_price_real_raw = None; - scenario.request.max_amount_a_raw = Some(String::from("1000")); - scenario.request.max_amount_b_raw = Some(String::from("3000")); - scenario.request.slippage_bps = Some(50); - - let quote_value = scenario.quote(); - assert_eq!(quote_value["poolStatus"], "active_pool"); - assert_eq!(quote_value["actualAmountARaw"], "1000"); - assert_eq!(quote_value["actualAmountBRaw"], "2000"); - assert_eq!(quote_value["expectedLpRaw"], "1000"); - assert_eq!(quote_value["minimumLpRaw"], "995"); - assert_eq!(quote_value["requiresFreshLp"], false); - assert_eq!(quote_value["canSubmit"], true); - assert_eq!(quote_value["errors"], json!([])); - - // The existing LP holding (not a fresh one) fills the LP slot of the 10-account - // AddLiquidity preview, and only the two token holdings sign. - let preview = quote_value["accountPreview"].as_array().unwrap(); - assert_eq!(preview.len(), 10); - assert_eq!(preview[7]["role"], "user_holding_lp"); - assert_eq!( - preview[7]["accountId"].as_str().unwrap(), - lp_holding.to_string() - ); - assert_eq!(preview[7]["signer"], false); - assert_eq!(preview[5]["signer"], true); - assert_eq!(preview[6]["signer"], true); -} - -#[test] -fn unfunded_quote_cannot_submit() { - let mut scenario = Scenario::devnet(); - scenario.snapshot.wallet_available = false; - scenario.snapshot.wallet_accounts.clear(); - let quote_value = scenario.quote(); - assert_eq!(quote_value["canSubmit"], false); -} - #[test] fn swap_plan_uses_the_pool_stored_vaults_not_canonical_order() { // A pool created NON-canonically: its stored def_a is the smaller-valued diff --git a/modules/amm/ffi/src/ffi.rs b/modules/amm/ffi/src/ffi.rs index e841656..497dc7a 100644 --- a/modules/amm/ffi/src/ffi.rs +++ b/modules/amm/ffi/src/ffi.rs @@ -8,7 +8,7 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::api::{ self, AddLiquidityPlanRequest, AddLiquidityQuoteRequest, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, - PoolIdRequest, ProgramIdRequest, QuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest, + PoolIdRequest, ProgramIdRequest, ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, TokenHoldingsRequest, TokenIdsRequest, }; @@ -99,11 +99,6 @@ pub extern "C" fn amm_context(request_json: *const c_char) -> *mut c_char { call::(request_json, api::context) } -#[unsafe(no_mangle)] -pub extern "C" fn amm_quote(request_json: *const c_char) -> *mut c_char { - call::(request_json, api::quote) -} - #[unsafe(no_mangle)] pub extern "C" fn amm_swap_pair(request_json: *const c_char) -> *mut c_char { call::(request_json, api::swap_pair) diff --git a/modules/amm/ffi/src/lib.rs b/modules/amm/ffi/src/lib.rs index 3b6f8d5..f90d3f6 100644 --- a/modules/amm/ffi/src/lib.rs +++ b/modules/amm/ffi/src/lib.rs @@ -6,11 +6,11 @@ mod ffi; pub mod api; pub use api::{ - config_id, context, create_pool_plan, liquidity_quote, pair_ids, pool_id, program_id, quote, + config_id, context, create_pool_plan, liquidity_quote, pair_ids, pool_id, program_id, resolve_pool, swap_exact_in_plan, swap_exact_in_quote, swap_exact_out_plan, swap_exact_out_quote, swap_pair, token_ids, AccountRead, AmmApiError, AmmResponse, AmmResult, ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest, - PairSnapshot, PoolIdRequest, PositionRequest, ProgramIdRequest, QuoteRequest, - ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, - SwapExactOutQuoteRequest, SwapPairRequest, TokenIdsRequest, WalletAccount, + PoolIdRequest, ProgramIdRequest, ResolvePoolRequest, SwapExactInPlanRequest, + SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, + TokenIdsRequest, WalletAccount, }; diff --git a/modules/amm/src/amm_module_impl.cpp b/modules/amm/src/amm_module_impl.cpp index cc362ef..7d73818 100644 --- a/modules/amm/src/amm_module_impl.cpp +++ b/modules/amm/src/amm_module_impl.cpp @@ -1096,63 +1096,6 @@ LogosList AmmModuleImpl::tokenHoldings(bool wallet_open) { return out; } -nlohmann::json AmmModuleImpl::buildQuoteInput(const LogosMap& request, - const Network& net, - bool wallet_open, - bool fresh_wallet_accounts, - nlohmann::json* error) { - if (net.status != "ready") { - *error = publicError(net.status); - return json(); - } - const FfiResult configResult = - call(amm_config_id, json{{"ammProgramId", net.amm_program_id}}); - if (!configResult.ok) { - *error = publicError("backend_error"); - return json(); - } - const json config = readPublicAccount(jStr(configResult.value, "configId")); - - const FfiResult pairResult = call(amm_pair_ids, json{ - {"ammProgramId", net.amm_program_id}, - {"config", config}, - {"tokenAId", request.value("tokenAId", json())}, - {"tokenBId", request.value("tokenBId", json())}, - }); - if (!pairResult.ok) { - *error = publicError("backend_error"); - return json(); - } - const json pairManifest = pairResult.value; - if (jStr(pairManifest, "status") != "ok") { - *error = publicError(jStr(pairManifest, "code")); - return json(); - } - - const json walletAccounts = walletAccountReads(wallet_open, fresh_wallet_accounts); - const json snapshot = { - {"config", config}, - {"tokenA", readPublicAccount(jStr(pairManifest, "tokenAId"))}, - {"tokenB", readPublicAccount(jStr(pairManifest, "tokenBId"))}, - {"pool", readPublicAccount(jStr(pairManifest, "poolId"))}, - {"vaultA", readPublicAccount(jStr(pairManifest, "vaultAId"))}, - {"vaultB", readPublicAccount(jStr(pairManifest, "vaultBId"))}, - {"lpDefinition", readPublicAccount(jStr(pairManifest, "lpDefinitionId"))}, - {"lpLockHolding", readPublicAccount(jStr(pairManifest, "lpLockHoldingId"))}, - {"currentTick", readPublicAccount(jStr(pairManifest, "currentTickId"))}, - {"clock", readPublicAccount(jStr(pairManifest, "clockId"))}, - {"walletAvailable", wallet_open}, - {"walletAccounts", walletAccounts}, - }; - return { - {"networkId", net.id}, - {"networkFingerprint", net.fingerprint}, - {"ammProgramId", net.amm_program_id}, - {"request", request}, - {"snapshot", snapshot}, - }; -} - LogosMap AmmModuleImpl::newPositionContext(const LogosMap& request, bool wallet_open, bool refresh_wallet_accounts) { @@ -1210,13 +1153,3 @@ LogosMap AmmModuleImpl::newPositionContext(const LogosMap& request, : contextState("error", net.id, net.fingerprint, "backend_error"); } -LogosMap AmmModuleImpl::quoteNewPosition(const LogosMap& request, bool wallet_open) { - const Network net = network(); - json error; - const json input = buildQuoteInput(request, net, wallet_open, /*fresh=*/false, &error); - if (!error.is_null()) return error; - - const FfiResult result = call(amm_quote, input); - return result.ok ? result.value : publicError("backend_error"); -} - diff --git a/modules/amm/src/amm_module_impl.h b/modules/amm/src/amm_module_impl.h index e4a9be9..c309ebe 100644 --- a/modules/amm/src/amm_module_impl.h +++ b/modules/amm/src/amm_module_impl.h @@ -184,11 +184,6 @@ public: bool wallet_open, bool refresh_wallet_accounts); - /// Prices an add-liquidity request against current on-chain state and - /// returns the new-position quote map (quoteHash, canSubmit, - /// requiresFreshLp, amounts, warnings). Read-only — no submission. - LogosMap quoteNewPosition(const LogosMap& request, bool wallet_open); - private: // Off-chain "network" context, derived from the process env (the same // sources the app backend used): AMM deployment id from AMM_PROGRAM_BIN, @@ -235,16 +230,6 @@ private: // live sequencer round-trip. nlohmann::json walletAccountReads(bool wallet_open, bool refresh); - // Builds the { networkId, networkFingerprint, ammProgramId, request, - // snapshot } input for quoteNewPosition's amm_quote call. On a recoverable - // precondition failure, sets *error to a new-position error map and returns - // a null json. - nlohmann::json buildQuoteInput(const LogosMap& request, - const Network& net, - bool wallet_open, - bool fresh_wallet_accounts, - nlohmann::json* error); - // Process-lifetime network config, resolved once (see network()). Serialized // module dispatch means no locking is needed; there is no invalidation, as // runtime env reload is not supported.