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.
This commit is contained in:
r4bbit
2026-08-13 21:30:14 +02:00
parent 92f55652a8
commit 56c80ce29d
13 changed files with 135 additions and 16 deletions
+5
View File
@@ -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)
+1
View File
@@ -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;
+4
View File
@@ -49,6 +49,10 @@ class AmmUiBackend
// `{ status:"error", error:<code> }` — `no_pool` for the ordinary "no pool /
// no liquidity yet" state, else no_program_bin / amm_not_initialized / bad_config.
SLOT(QVariantMap resolvePoolAccount(QString defAHex, QString defBHex))
// 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
+2
View File
@@ -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);
+4
View File
@@ -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) {
+24 -2
View File
@@ -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<Value, String> {
let amm_program = parse_program_id(&request.amm_program_id)?;
@@ -13,6 +15,26 @@ pub(super) fn config_id(request: ConfigIdRequest) -> Result<Value, String> {
}))
}
/// 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<Value, String> {
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<AmmConfig, String> {
let (id, account) = decode_account(read)?;
if id != compute_config_pda(amm_program)
+8 -3
View File
@@ -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)
+10
View File
@@ -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.
+37 -2
View File
@@ -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]);
+7 -2
View File
@@ -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::<ConfigIdRequest>(request_json, api::config_id)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_config_account(request_json: *const c_char) -> *mut c_char {
call::<ConfigAccountRequest>(request_json, api::config_account)
}
#[unsafe(no_mangle)]
pub extern "C" fn amm_pair_ids(request_json: *const c_char) -> *mut c_char {
call::<PairIdsRequest>(request_json, api::pair_ids)
+7 -7
View File
@@ -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,
};
+19
View File
@@ -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,
+7
View File
@@ -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