From 56c80ce29dcc63a0b06e3043fddecdd819515224 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Thu, 13 Aug 2026 10:15:10 +0200 Subject: [PATCH] feat(amm): add configAccount read (decode the singleton config) Expose the AMM config account as a read op, so a future config/admin view can show the authority and the token/twap program ids the AMM chains into. --- apps/amm/src/AmmUiBackend.cpp | 5 ++++ apps/amm/src/AmmUiBackend.h | 1 + apps/amm/src/AmmUiBackend.rep | 4 +++ modules/amm/ffi/include/amm_ffi.h | 2 ++ modules/amm/ffi/src/account.rs | 4 +++ modules/amm/ffi/src/api/config.rs | 26 +++++++++++++++++-- modules/amm/ffi/src/api/mod.rs | 11 +++++--- modules/amm/ffi/src/api/request.rs | 10 ++++++++ modules/amm/ffi/src/api/tests.rs | 39 +++++++++++++++++++++++++++-- modules/amm/ffi/src/ffi.rs | 9 +++++-- modules/amm/ffi/src/lib.rs | 14 +++++------ modules/amm/src/amm_module_impl.cpp | 19 ++++++++++++++ modules/amm/src/amm_module_impl.h | 7 ++++++ 13 files changed, 135 insertions(+), 16 deletions(-) diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index d1aaa9a..67de93e 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -182,6 +182,11 @@ QVariantMap AmmUiBackend::resolvePoolAccount(QString defAHex, QString defBHex) return m_logos->amm_module.resolvePoolAccount(defAHex, defBHex); } +QVariantMap AmmUiBackend::configAccount() +{ + return m_logos->amm_module.configAccount(); +} + QString AmmUiBackend::swapExactInput(QString defAHex, QString defBHex, QString userInputHoldingHex, QString userOutputHoldingHex, QString amountInDecimal, QString minOutDecimal, QString deadlineDecimal) diff --git a/apps/amm/src/AmmUiBackend.h b/apps/amm/src/AmmUiBackend.h index 957abf6..0a2deba 100644 --- a/apps/amm/src/AmmUiBackend.h +++ b/apps/amm/src/AmmUiBackend.h @@ -55,6 +55,7 @@ public slots: // AMM β€” all forwarded to the amm_module core module. QVariantMap resolvePoolAccount(QString defAHex, QString defBHex) override; + QVariantMap configAccount() 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 639e85d..5a688c1 100644 --- a/apps/amm/src/AmmUiBackend.rep +++ b/apps/amm/src/AmmUiBackend.rep @@ -49,6 +49,10 @@ class AmmUiBackend // `{ 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)) + // Decodes the singleton AMM config account: `{ status:"ok", error:"", configId, + // ammProgramId, authority, tokenProgramId, twapOracleProgramId }` (ids base58), or + // `{ status:"error", error:"config_missing"|"config_unavailable"|"backend_error" }`. + SLOT(QVariantMap configAccount()) // 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/ffi/include/amm_ffi.h b/modules/amm/ffi/include/amm_ffi.h index 1d9c60c..c255c24 100644 --- a/modules/amm/ffi/include/amm_ffi.h +++ b/modules/amm/ffi/include/amm_ffi.h @@ -16,6 +16,8 @@ extern "C" { char *amm_config_id(const char *request_json); +char *amm_config_account(const char *request_json); + char *amm_pair_ids(const char *request_json); char *amm_resolve_tokens(const char *request_json); diff --git a/modules/amm/ffi/src/account.rs b/modules/amm/ffi/src/account.rs index 5c66097..2a8d020 100644 --- a/modules/amm/ffi/src/account.rs +++ b/modules/amm/ffi/src/account.rs @@ -60,6 +60,10 @@ pub(crate) fn program_id_hex(program_id: ProgramId) -> String { hex::encode(bytes) } +pub(crate) fn program_id_base58(program_id: ProgramId) -> String { + AccountId::new(program_id_bytes(program_id)).to_string() +} + pub(crate) fn program_id_bytes(program_id: ProgramId) -> [u8; 32] { let mut bytes = [0_u8; 32]; for (chunk, word) in bytes.chunks_exact_mut(4).zip(program_id) { diff --git a/modules/amm/ffi/src/api/config.rs b/modules/amm/ffi/src/api/config.rs index 563537d..926c342 100644 --- a/modules/amm/ffi/src/api/config.rs +++ b/modules/amm/ffi/src/api/config.rs @@ -2,8 +2,10 @@ use amm_core::{compute_config_pda, AmmConfig}; use nssa_core::{account::Account, program::ProgramId}; use serde_json::{json, Value}; -use super::ConfigIdRequest; -use crate::account::{account_id_hex, decode_account, parse_program_id, AccountRead}; +use super::{ConfigAccountRequest, ConfigIdRequest}; +use crate::account::{ + account_id_hex, decode_account, parse_program_id, program_id_base58, AccountRead, +}; pub(super) fn config_id(request: ConfigIdRequest) -> Result { let amm_program = parse_program_id(&request.amm_program_id)?; @@ -13,6 +15,26 @@ pub(super) fn config_id(request: ConfigIdRequest) -> Result { })) } +/// Decodes the singleton config account: authority + the token/twap program ids the AMM chains +/// into. Ids are base58 (app-facing). `config_unavailable` when the config PDA isn't on-chain +/// yet / undecodable; `configId` / `ammProgramId` are still derivable from `amm_program_id` via +/// `config_id` for address derivation. +pub(super) fn config_account(request: ConfigAccountRequest) -> Result { + let amm_program = parse_program_id(&request.amm_program_id)?; + let Ok(config) = load_config(amm_program, &request.config) else { + return Ok(json!({ "status": "error", "error": "config_unavailable" })); + }; + Ok(json!({ + "status": "ok", + "error": "", + "configId": compute_config_pda(amm_program).to_string(), + "ammProgramId": program_id_base58(amm_program), + "authority": config.authority.to_string(), + "tokenProgramId": program_id_base58(config.token_program_id), + "twapOracleProgramId": program_id_base58(config.twap_oracle_program_id), + })) +} + pub(super) fn load_config(amm_program: ProgramId, read: &AccountRead) -> Result { let (id, account) = decode_account(read)?; if id != compute_config_pda(amm_program) diff --git a/modules/amm/ffi/src/api/mod.rs b/modules/amm/ffi/src/api/mod.rs index bc83e0d..b78433d 100644 --- a/modules/amm/ffi/src/api/mod.rs +++ b/modules/amm/ffi/src/api/mod.rs @@ -17,9 +17,9 @@ mod tests; use std::{error::Error, fmt}; pub use request::{ - AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, CreatePoolPlanRequest, - CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, - RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest, + AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigAccountRequest, ConfigIdRequest, + CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest, + ProgramIdRequest, RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, SyncReservesPlanRequest, TokenHoldingsRequest, }; @@ -66,6 +66,11 @@ pub fn config_id(request: ConfigIdRequest) -> AmmResult { config::config_id(request).map_err(Into::into) } +/// Decodes the singleton config account (authority + token/twap program ids). +pub fn config_account(request: ConfigAccountRequest) -> AmmResult { + config::config_account(request).map_err(Into::into) +} + /// Derives canonical accounts for one token pair. pub fn pair_ids(request: PairIdsRequest) -> AmmResult { pair::pair_ids(request).map_err(Into::into) diff --git a/modules/amm/ffi/src/api/request.rs b/modules/amm/ffi/src/api/request.rs index 9a590ad..31458f0 100644 --- a/modules/amm/ffi/src/api/request.rs +++ b/modules/amm/ffi/src/api/request.rs @@ -8,6 +8,16 @@ pub struct ConfigIdRequest { pub amm_program_id: String, } +/// Decodes the singleton AMM config account. `config` is the read of the config PDA the module +/// derives from `amm_program_id`; the op returns the authority + program ids (or +/// `{ status:"error", error:"config_unavailable" }` when the config isn't on-chain yet). +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ConfigAccountRequest { + pub amm_program_id: String, + pub config: AccountRead, +} + /// Resolves an app-provided set of token ids into selector rows. `token_ids` are hex β€” the /// module normalizes base58β†’hex and reads each definition into `token_definitions` (keyed by /// hex id) plus the wallet accounts; the FFI is stateless and reads nothing itself. diff --git a/modules/amm/ffi/src/api/tests.rs b/modules/amm/ffi/src/api/tests.rs index 7410c84..cfcd0f7 100644 --- a/modules/amm/ffi/src/api/tests.rs +++ b/modules/amm/ffi/src/api/tests.rs @@ -14,15 +14,17 @@ use token_core::{TokenDefinition, TokenHolding}; use twap_oracle_core::compute_current_tick_account_pda; use super::{ + config::config_account as decode_config_account, context::resolve_tokens, holding::{select_holding, SelectedHolding}, pair::{is_canonical_pair, pair_ids, PairIds}, quote::{div_ceil_u256, minimum_opening_pair, Q64}, swap::{swap_exact_in_plan, swap_exact_out_plan}, - PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactOutPlanRequest, + ConfigAccountRequest, PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest, + SwapExactOutPlanRequest, }; use crate::{ - account::{account_id_hex, account_read, decode_account, program_id_bytes}, + account::{account_id_hex, account_read, decode_account, program_id_base58, program_id_bytes}, AccountRead, }; @@ -238,6 +240,39 @@ fn resolve_tokens_returns_lean_rows_held_first_and_omits_unresolvable() { ); } +#[test] +fn config_account_decodes_authority_and_program_ids() { + let config_id = compute_config_pda(AMM_PROGRAM); + let value = decode_config_account(ConfigAccountRequest { + amm_program_id: amm_program_id(), + config: account_read(config_id, &config_account()), + }) + .unwrap(); + + assert_eq!(value["status"], "ok"); + assert_eq!(value["configId"], config_id.to_string()); + assert_eq!(value["ammProgramId"], program_id_base58(AMM_PROGRAM)); + assert_eq!(value["authority"], AccountId::new([7; 32]).to_string()); + assert_eq!(value["tokenProgramId"], program_id_base58(TOKEN_PROGRAM)); + assert_eq!( + value["twapOracleProgramId"], + program_id_base58(TWAP_PROGRAM) + ); +} + +#[test] +fn config_account_is_unavailable_when_not_on_chain() { + let config_id = compute_config_pda(AMM_PROGRAM); + let value = decode_config_account(ConfigAccountRequest { + amm_program_id: amm_program_id(), + config: default_read(config_id), + }) + .unwrap(); + + assert_eq!(value["status"], "error"); + assert_eq!(value["error"], "config_unavailable"); +} + #[test] fn missing_pool_snapshot_defaults_remain_real_accounts() { let id = AccountId::new([5; 32]); diff --git a/modules/amm/ffi/src/ffi.rs b/modules/amm/ffi/src/ffi.rs index 8bb9abc..ed23a2f 100644 --- a/modules/amm/ffi/src/ffi.rs +++ b/modules/amm/ffi/src/ffi.rs @@ -7,8 +7,8 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::api::{ self, AddLiquidityPlanRequest, AddLiquidityQuoteRequest, AmmApiError, AmmResult, - ConfigIdRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, - PairIdsRequest, PoolIdRequest, ProgramIdRequest, RemoveLiquidityPlanRequest, + ConfigAccountRequest, ConfigIdRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest, + FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, SyncReservesPlanRequest, TokenHoldingsRequest, @@ -85,6 +85,11 @@ pub extern "C" fn amm_config_id(request_json: *const c_char) -> *mut c_char { call::(request_json, api::config_id) } +#[unsafe(no_mangle)] +pub extern "C" fn amm_config_account(request_json: *const c_char) -> *mut c_char { + call::(request_json, api::config_account) +} + #[unsafe(no_mangle)] pub extern "C" fn amm_pair_ids(request_json: *const c_char) -> *mut c_char { call::(request_json, api::pair_ids) diff --git a/modules/amm/ffi/src/lib.rs b/modules/amm/ffi/src/lib.rs index fba637f..b2a09cd 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, create_pool_plan, create_pool_quote, fee_tiers, pair_ids, pool_id, program_id, - resolve_pool, resolve_tokens, swap_exact_in_plan, swap_exact_in_quote, swap_exact_out_plan, - swap_exact_out_quote, swap_pair, AccountRead, AmmApiError, AmmResponse, AmmResult, - ConfigIdRequest, CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, - PairIdsRequest, PoolIdRequest, ProgramIdRequest, ResolvePoolRequest, ResolveTokensRequest, - SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest, - SwapExactOutQuoteRequest, SwapPairRequest, WalletAccount, + config_account, config_id, create_pool_plan, create_pool_quote, fee_tiers, pair_ids, pool_id, + program_id, resolve_pool, resolve_tokens, swap_exact_in_plan, swap_exact_in_quote, + swap_exact_out_plan, swap_exact_out_quote, swap_pair, AccountRead, AmmApiError, AmmResponse, + AmmResult, ConfigAccountRequest, ConfigIdRequest, CreatePoolPlanRequest, + CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest, + ResolvePoolRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, + SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, WalletAccount, }; diff --git a/modules/amm/src/amm_module_impl.cpp b/modules/amm/src/amm_module_impl.cpp index d8aa1f9..7b39486 100644 --- a/modules/amm/src/amm_module_impl.cpp +++ b/modules/amm/src/amm_module_impl.cpp @@ -401,6 +401,25 @@ LogosMap AmmModuleImpl::resolvePoolAccount(const std::string& def_a_hex, return resolved; } +LogosMap AmmModuleImpl::configAccount() { + const std::string amm_program_id = ammProgramId(); + if (amm_program_id.empty()) + return LogosMap{{"status", "error"}, {"error", "config_missing"}}; + + const json config = readConfig(amm_program_id); + if (config.is_null()) + return LogosMap{{"status", "error"}, {"error", "backend_error"}}; + + const FfiResult result = call(amm_config_account, json{ + {"ammProgramId", amm_program_id}, + {"config", config}, + }); + if (!result.ok) + return LogosMap{{"status", "error"}, + {"error", result.error.empty() ? "backend_error" : result.error}}; + return result.value; +} + LogosMap AmmModuleImpl::swapExactInQuote(const std::string& token_in_hex, const std::string& token_out_hex, const nlohmann::json& amount_in, diff --git a/modules/amm/src/amm_module_impl.h b/modules/amm/src/amm_module_impl.h index e93ae4c..9b5e742 100644 --- a/modules/amm/src/amm_module_impl.h +++ b/modules/amm/src/amm_module_impl.h @@ -40,6 +40,13 @@ public: /// ordinary "no pool / no liquidity yet" state (still carries `poolId`). LogosMap resolvePoolAccount(const std::string& def_a_hex, const std::string& def_b_hex); + /// Decodes the singleton AMM config account. On success: `{ status:"ok", error:"", + /// configId, ammProgramId, authority, tokenProgramId, twapOracleProgramId }` (ids + /// base58). `{ status:"error", error:"config_missing" }` when AMM_PROGRAM_BIN is + /// unset/unreadable; `config_unavailable` when the config isn't on-chain yet; or + /// `backend_error` when the backend FFI call fails. + LogosMap configAccount(); + /// Prices a `SwapExactInput` for the (token_in_hex, token_out_hex) pair: /// reads the pool and returns `{ status:"ok", error:"", expectedOutRaw, /// minReceivedRaw, priceImpactBps }`, oriented and computed server-side via