mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(modules/amm): add tokenHoldings — list the wallet's token holdings
A thin source for the account selector: an amm_ffi op that decodes the wallet's
fungible TokenHoldings (owned by the configured token program) into
[{ accountId (hex), accountType:\"TokenHolding\", definitionId (base58),
definitionIdHex (hex), balanceRaw }] — one row per holding account, every token,
including zero-balance holdings; narrowing to a specific token is the selector's
job. Exposed via AmmModuleImpl::tokenHoldings and the AmmUiBackend tokenHoldings()
slot, both gated on wallet-open.
This commit is contained in:
@@ -279,6 +279,13 @@ QVariantMap AmmUiBackend::liquidityQuote(QVariantMap request)
|
||||
return m_logos->amm_module.liquidityQuote(request);
|
||||
}
|
||||
|
||||
QVariantList AmmUiBackend::tokenHoldings()
|
||||
{
|
||||
// Read-only list of the wallet's token holdings for the account selector. Gated
|
||||
// by this app's wallet-open state (a closed wallet has nothing to list).
|
||||
return m_logos->amm_module.tokenHoldings(isWalletOpen());
|
||||
}
|
||||
|
||||
QVariantMap AmmUiBackend::createPool(QVariantMap request)
|
||||
{
|
||||
// Same connected-state submit guard as the swaps — this app's lock is
|
||||
|
||||
@@ -77,6 +77,8 @@ public slots:
|
||||
// accounts here.
|
||||
QVariantMap liquidityQuote(QVariantMap request) override;
|
||||
QVariantMap createPool(QVariantMap request) override;
|
||||
// Lists the wallet's fungible token holdings for the account selector.
|
||||
QVariantList tokenHoldings() override;
|
||||
|
||||
private:
|
||||
void syncWalletState();
|
||||
|
||||
@@ -114,4 +114,9 @@ class AmmUiBackend
|
||||
// invalid_account_id, bad_amount, bad_fee_bps_amount, invalid_fee_tier,
|
||||
// wallet_submission_failed, backend_error).
|
||||
SLOT(QVariantMap createPool(QVariantMap request))
|
||||
// Lists the connected wallet's fungible token holdings for the account
|
||||
// selector: [{ accountId, accountType:"TokenHolding", definitionId,
|
||||
// definitionIdHex, balanceRaw }] — one row per holding account, every token,
|
||||
// including zero-balance holdings. The selector narrows to a token by id.
|
||||
SLOT(QVariantList tokenHoldings())
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ char *amm_liquidity_quote(const char *request_json);
|
||||
|
||||
char *amm_create_pool_plan(const char *request_json);
|
||||
|
||||
char *amm_token_holdings(const char *request_json);
|
||||
|
||||
char *amm_program_id(const char *request_json);
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ mod quote;
|
||||
mod quote_error;
|
||||
mod request;
|
||||
mod swap;
|
||||
mod token_holdings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -25,7 +26,7 @@ pub use request::{
|
||||
ConfigIdRequest, ContextRequest, CreatePoolPlanRequest, LiquidityQuoteRequest, PairIdsRequest,
|
||||
PairSnapshot, PlanRequest, PoolIdRequest, PositionRequest, ProgramIdRequest, QuoteRequest,
|
||||
ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
|
||||
SwapExactOutQuoteRequest, SwapPairRequest, TokenIdsRequest,
|
||||
SwapExactOutQuoteRequest, SwapPairRequest, TokenHoldingsRequest, TokenIdsRequest,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -140,6 +141,11 @@ pub fn create_pool_plan(request: CreatePoolPlanRequest) -> AmmResult {
|
||||
liquidity::create_pool_plan(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Lists the wallet's fungible token holdings for the account selector.
|
||||
pub fn token_holdings(request: TokenHoldingsRequest) -> AmmResult {
|
||||
token_holdings::token_holdings(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Derives the AMM `ProgramId` (Image ID) from a deployed program binary.
|
||||
pub fn program_id(request: ProgramIdRequest) -> AmmResult {
|
||||
swap::program_id(request).map_err(Into::into)
|
||||
|
||||
@@ -168,6 +168,17 @@ pub struct CreatePoolPlanRequest {
|
||||
pub user_holding_lp_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenHoldingsRequest {
|
||||
pub amm_program_id: String,
|
||||
/// AMM config account read — decoded for the `token_program_id` that identifies
|
||||
/// which wallet accounts are token holdings.
|
||||
pub config: AccountRead,
|
||||
#[serde(default)]
|
||||
pub wallet_accounts: Vec<AccountRead>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProgramIdRequest {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//! Lists the wallet's fungible token holdings for the account selector.
|
||||
//!
|
||||
//! A flat, decoded view of every `TokenHolding` the wallet owns (owned by the
|
||||
//! configured token program), each row carrying the id in **both** encodings so
|
||||
//! the swap view (hex ids) and the liquidity view (base58 ids) can each filter by
|
||||
//! their own token id via the selector's `stateField`. Pure over the wallet reads +
|
||||
//! config — no chain access of its own.
|
||||
//!
|
||||
//! Thin stopgap: token holdings are wallet/token-program data, not AMM data. This
|
||||
//! lives here only because `amm_ffi` is the one place wired to decode `TokenHolding`
|
||||
//! (via `token_core`); it should move to a dedicated token-program logos module once
|
||||
//! one exists.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{config::load_config, holding::wallet_holdings, TokenHoldingsRequest};
|
||||
use crate::account::{account_id_hex, parse_program_id};
|
||||
|
||||
pub(super) fn token_holdings(request: TokenHoldingsRequest) -> Result<Value, String> {
|
||||
let amm_program = parse_program_id(&request.amm_program_id)?;
|
||||
let config = load_config(amm_program, &request.config)?;
|
||||
let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
|
||||
let rows = holdings
|
||||
.into_iter()
|
||||
.map(|holding| {
|
||||
json!({
|
||||
"accountId": account_id_hex(holding.id),
|
||||
"accountType": "TokenHolding",
|
||||
// Both encodings: the swap view filters on definitionIdHex, the
|
||||
// liquidity view on the base58 definitionId.
|
||||
"definitionId": holding.definition_id.to_string(),
|
||||
"definitionIdHex": account_id_hex(holding.definition_id),
|
||||
"balanceRaw": holding.balance.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({ "holdings": rows }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use amm_core::{compute_config_pda, AmmConfig};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, Data},
|
||||
program::ProgramId,
|
||||
};
|
||||
use token_core::TokenHolding;
|
||||
|
||||
use super::*;
|
||||
use crate::account::{account_read, AccountRead};
|
||||
|
||||
fn token_program() -> ProgramId {
|
||||
parse_program_id(&"01".repeat(32)).unwrap()
|
||||
}
|
||||
|
||||
fn holding_read(id: AccountId, definition: AccountId, balance: u128) -> AccountRead {
|
||||
let account = Account {
|
||||
program_owner: token_program(),
|
||||
data: (&TokenHolding::Fungible {
|
||||
definition_id: definition,
|
||||
balance,
|
||||
})
|
||||
.into(),
|
||||
..Account::default()
|
||||
};
|
||||
account_read(id, &account)
|
||||
}
|
||||
|
||||
fn config_read(amm: ProgramId) -> AccountRead {
|
||||
let account = Account {
|
||||
program_owner: amm,
|
||||
data: Data::from(&AmmConfig {
|
||||
token_program_id: token_program(),
|
||||
twap_oracle_program_id: parse_program_id(&"02".repeat(32)).unwrap(),
|
||||
authority: AccountId::new([0x09; 32]),
|
||||
}),
|
||||
..Account::default()
|
||||
};
|
||||
account_read(compute_config_pda(amm), &account)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_wallet_token_holdings_with_both_id_encodings() {
|
||||
let amm = parse_program_id(&"00".repeat(32)).unwrap();
|
||||
let def = AccountId::new([0xAA; 32]);
|
||||
let holding_id = AccountId::new([0x01; 32]);
|
||||
|
||||
let value = token_holdings(TokenHoldingsRequest {
|
||||
amm_program_id: "00".repeat(32),
|
||||
config: config_read(amm),
|
||||
wallet_accounts: vec![holding_read(holding_id, def, 500)],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let rows = value["holdings"].as_array().unwrap();
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0]["accountId"], account_id_hex(holding_id));
|
||||
assert_eq!(rows[0]["accountType"], "TokenHolding");
|
||||
assert_eq!(rows[0]["definitionId"], def.to_string());
|
||||
assert_eq!(rows[0]["definitionIdHex"], account_id_hex(def));
|
||||
assert_eq!(rows[0]["balanceRaw"], "500");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_non_token_accounts_and_fails_closed_on_bad_config() {
|
||||
let amm = parse_program_id(&"00".repeat(32)).unwrap();
|
||||
// A non-token-program account is not a holding.
|
||||
let foreign = Account {
|
||||
program_owner: parse_program_id(&"ee".repeat(32)).unwrap(),
|
||||
..Account::default()
|
||||
};
|
||||
let value = token_holdings(TokenHoldingsRequest {
|
||||
amm_program_id: "00".repeat(32),
|
||||
config: config_read(amm),
|
||||
wallet_accounts: vec![account_read(AccountId::new([0x02; 32]), &foreign)],
|
||||
})
|
||||
.unwrap();
|
||||
assert!(value["holdings"].as_array().unwrap().is_empty());
|
||||
|
||||
// A read-failed config fails closed as Err.
|
||||
assert!(token_holdings(TokenHoldingsRequest {
|
||||
amm_program_id: "00".repeat(32),
|
||||
config: AccountRead {
|
||||
id: String::new(),
|
||||
status: String::from("read_failed"),
|
||||
account: None,
|
||||
},
|
||||
wallet_accounts: vec![],
|
||||
})
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ use crate::api::{
|
||||
self, AmmApiError, AmmResult, ConfigIdRequest, ContextRequest, CreatePoolPlanRequest,
|
||||
LiquidityQuoteRequest, PairIdsRequest, PlanRequest, PoolIdRequest, ProgramIdRequest,
|
||||
QuoteRequest, ResolvePoolRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest,
|
||||
SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, TokenIdsRequest,
|
||||
SwapExactOutPlanRequest, SwapExactOutQuoteRequest, SwapPairRequest, TokenHoldingsRequest,
|
||||
TokenIdsRequest,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -153,6 +154,11 @@ pub extern "C" fn amm_create_pool_plan(request_json: *const c_char) -> *mut c_ch
|
||||
call::<CreatePoolPlanRequest>(request_json, api::create_pool_plan)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn amm_token_holdings(request_json: *const c_char) -> *mut c_char {
|
||||
call::<TokenHoldingsRequest>(request_json, api::token_holdings)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn amm_program_id(request_json: *const c_char) -> *mut c_char {
|
||||
call::<ProgramIdRequest>(request_json, api::program_id)
|
||||
|
||||
@@ -919,6 +919,37 @@ LogosList AmmModuleImpl::tokenList() {
|
||||
return out;
|
||||
}
|
||||
|
||||
LogosList AmmModuleImpl::tokenHoldings(bool wallet_open) {
|
||||
const std::string amm_program_id = ammProgramId();
|
||||
if (amm_program_id.empty())
|
||||
return LogosList::array();
|
||||
|
||||
// The config gives the token_program_id that identifies which wallet accounts
|
||||
// are token holdings (decoded by the FFI op).
|
||||
const FfiResult configResult =
|
||||
call(amm_config_id, json{{"ammProgramId", amm_program_id}});
|
||||
if (!configResult.ok)
|
||||
return LogosList::array();
|
||||
const json config = readPublicAccount(jStr(configResult.value, "configId"));
|
||||
|
||||
// Fresh wallet read each call — the selector wants current holdings/balances.
|
||||
const json wallet_accounts = walletAccountReads(wallet_open, /*refresh=*/true);
|
||||
|
||||
const FfiResult result = call(amm_token_holdings, json{
|
||||
{"ammProgramId", amm_program_id},
|
||||
{"config", config},
|
||||
{"walletAccounts", wallet_accounts},
|
||||
});
|
||||
if (!result.ok)
|
||||
return LogosList::array();
|
||||
|
||||
LogosList out = LogosList::array();
|
||||
const auto it = result.value.find("holdings");
|
||||
if (it != result.value.end() && it->is_array())
|
||||
for (const auto& holding : *it) out.push_back(holding);
|
||||
return out;
|
||||
}
|
||||
|
||||
nlohmann::json AmmModuleImpl::buildQuoteInput(const LogosMap& request,
|
||||
const Network& net,
|
||||
bool wallet_open,
|
||||
|
||||
@@ -134,6 +134,16 @@ public:
|
||||
/// a code so the create-pool UI can tell the user why.
|
||||
LogosMap createPool(const LogosMap& request);
|
||||
|
||||
/// Lists the connected wallet's fungible token holdings for the account
|
||||
/// selector: `[{ accountId (hex), accountType:"TokenHolding", definitionId
|
||||
/// (base58), definitionIdHex (hex), balanceRaw }]` — one row per holding
|
||||
/// account, every token, including zero-balance holdings. Narrowing to a
|
||||
/// specific token is the selector's job. `wallet_open` gates the wallet read;
|
||||
/// an empty list on a closed wallet, unset AMM_PROGRAM_BIN, or a decode failure.
|
||||
/// (Thin stopgap — token-holding listing is wallet/token data; see
|
||||
/// token_holdings.rs.)
|
||||
LogosList tokenHoldings(bool wallet_open);
|
||||
|
||||
/// Reads the token list config at TOKENS_CONFIG (a JSON array of
|
||||
/// { symbol, name, definitionId, holding, decimals }) and returns it,
|
||||
/// normalizing definitionId/holding to lowercase hex. Empty list if
|
||||
|
||||
Reference in New Issue
Block a user