Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions apps/amm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,28 +179,28 @@ Swap view stays disabled (no pool can be resolved).

### Token list config (required for the Swap token picker)

The Swap view's token picker is config-driven: it doesn't derive tokens from
chain state, it reads a flat JSON list from the `TOKENS_CONFIG` environment
variable (absolute path). Each entry needs, at minimum, the token's
`definitionId` and **your own** `holding` account address for that token (the
account the wallet will sign transfers from/to for that token):
The Swap view's token picker is config-driven: it doesn't derive token
definitions from chain state, it reads a flat JSON list from the
`TOKENS_CONFIG` environment variable (absolute path). Each entry needs, at
minimum, a `definitionId`. Source and destination TokenHoldings come from the
connected wallet:

```json
[
{
"symbol": "TKA",
"name": "Token A",
"definitionId": "9qbX…",
"holding": "4T69…",
"decimals": 18
}
]
```

If `TOKENS_CONFIG` is unset, unreadable, or not a valid JSON array, the token
picker stays empty (a `qWarning` naming the exact cause is logged to stderr; no
swap can be started). `definitionId`/`holding` may be given as base58 (as the
wallet/runbook display them) or hex — the app normalizes both to hex.
swap can be started). `definitionId` may be given as base58 (as the
wallet/runbook displays it) or hex. A legacy `holding` value is accepted but is
not used for transaction account selection.

Full command with both variables set (absolute paths, from the repo root):

Expand Down
13 changes: 9 additions & 4 deletions apps/amm/client/src/api/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub(super) fn missing_account_plan(
])?;
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(
Expand Down Expand Up @@ -95,11 +96,15 @@ pub(super) fn missing_account_plan(
),
AccountPlanRow::new(
"user_holding_lp",
None,
holdings.lp.map(|value| value.id),
Some(pair.token_program),
"create",
true,
true,
if holdings.lp.is_some() {
"update"
} else {
"create"
},
holdings.lp.is_none(),
holdings.lp.is_none(),
),
AccountPlanRow::new(
"current_tick",
Expand Down
51 changes: 42 additions & 9 deletions apps/amm/client/src/api/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use token_core::TokenDefinition;

use super::{
config::load_config,
holding::{select_holding, wallet_holdings, SelectedHolding},
holding::{holding_options, wallet_holdings, SelectedHolding},
quote_error::issue,
ContextRequest, TokenIdsRequest, SCHEMA,
};
Expand Down Expand Up @@ -59,6 +59,26 @@ pub(super) fn context(request: ContextRequest) -> Result<Value, String> {
};

let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
let mut program_accounts = holdings.clone();
program_accounts.sort_by_key(|holding| holding.id);
let program_accounts = program_accounts
.into_iter()
.map(|holding| {
json!({
"accountId": holding.id.to_string(),
"address": account_id_hex(holding.id),
"displayAddress": holding.id.to_string(),
"accountType": "TokenHolding",
"definitionId": account_id_hex(holding.definition_id),
"definitionDisplayId": holding.definition_id.to_string(),
"balanceRaw": holding.balance.to_string(),
"state": {
"definitionId": account_id_hex(holding.definition_id),
"balanceRaw": holding.balance.to_string(),
},
})
})
.collect::<Vec<_>>();
let source_map = token_sources(&request, &holdings);
let mut rows = Vec::new();
let mut warnings = Vec::new();
Expand All @@ -85,9 +105,13 @@ pub(super) fn context(request: ContextRequest) -> Result<Value, String> {
}
};

let selected = select_holding(&holdings, token_id);
let mut row = json!({
let options = holding_options(&holdings, token_id);
let total_balance = options.iter().fold(0_u128, |total, holding| {
total.saturating_add(holding.balance)
});
let row = json!({
"definitionId": token_id.to_string(),
"definitionIdHex": account_id_hex(token_id),
"name": name,
"metadataId": metadata_id.map(|id| id.to_string()),
"totalSupplyRaw": total_supply.to_string(),
Expand All @@ -98,17 +122,23 @@ pub(super) fn context(request: ContextRequest) -> Result<Value, String> {
"status": "available",
"code": "available",
"sources": sources,
"balanceRaw": total_balance.to_string(),
"holdings": options.into_iter().map(|holding| json!({
"holdingId": holding.id.to_string(),
"address": account_id_hex(holding.id),
"balanceRaw": holding.balance.to_string(),
})).collect::<Vec<_>>(),
});
if let Some(selected) = selected {
row["holdingId"] = json!(selected.id.to_string());
row["balanceRaw"] = json!(selected.balance.to_string());
}
rows.push(row);
}

rows.sort_by(|left, right| {
let left_holding = left.get("holdingId").is_some();
let right_holding = right.get("holdingId").is_some();
let left_holding = left["holdings"]
.as_array()
.is_some_and(|rows| !rows.is_empty());
let right_holding = right["holdings"]
.as_array()
.is_some_and(|rows| !rows.is_empty());
right_holding.cmp(&left_holding).then_with(|| {
left["definitionId"]
.as_str()
Expand All @@ -129,6 +159,7 @@ pub(super) fn context(request: ContextRequest) -> Result<Value, String> {
"twapOracle": program_id_base58(config.twap_oracle_program_id),
},
"tokens": rows,
"programAccounts": program_accounts,
"feeTiers": fee_tiers(),
"warnings": warnings,
}))
Expand All @@ -143,6 +174,7 @@ fn context_error(request: &ContextRequest, code: &str) -> Value {
"networkFingerprint": request.network_fingerprint,
"walletAvailable": request.wallet_available,
"tokens": [],
"programAccounts": [],
"feeTiers": fee_tiers(),
"warnings": [],
})
Expand Down Expand Up @@ -195,6 +227,7 @@ fn token_sources(
fn unavailable_token_row(token_id: AccountId, sources: Vec<String>, code: &str) -> Value {
json!({
"definitionId": token_id.to_string(),
"definitionIdHex": account_id_hex(token_id),
"name": "",
"metadataId": Value::Null,
"totalSupplyRaw": "0",
Expand Down
35 changes: 28 additions & 7 deletions apps/amm/client/src/api/holding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use nssa_core::{
};
use token_core::TokenHolding;

use crate::account::{decode_account, AccountRead};
use crate::account::{account_id_from_hex, decode_account, parse_base58_id, AccountRead};

#[derive(Clone)]
pub(super) struct SelectedHolding {
Expand Down Expand Up @@ -51,14 +51,35 @@ pub(super) fn decode_fungible_holding(
pub(super) fn select_holding(
holdings: &[SelectedHolding],
definition_id: AccountId,
requested_id: Option<&str>,
) -> Option<SelectedHolding> {
holdings
let options = holding_options(holdings, definition_id);
let Some(requested_id) = requested_id else {
return options.first().cloned();
};
let requested_id = account_id_from_hex(requested_id, "holding id")
.or_else(|_| parse_base58_id(requested_id, "holding id"))
.ok()?;
options
.iter()
.find(|holding| holding.id == requested_id)
.cloned()
}

pub(super) fn holding_options(
holdings: &[SelectedHolding],
definition_id: AccountId,
) -> Vec<SelectedHolding> {
let mut options = holdings
.iter()
.filter(|holding| holding.definition_id == definition_id)
.max_by(|left, right| {
left.balance
.cmp(&right.balance)
.then_with(|| right.id.cmp(&left.id))
})
.cloned()
.collect::<Vec<_>>();
options.sort_by(|left, right| {
right
.balance
.cmp(&left.balance)
.then_with(|| left.id.cmp(&right.id))
});
options
}
Loading
Loading