mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-27 07:01:14 +00:00
refactor(amm): remove the dead Network/context machinery
The liquidity token surface moved app-side (resolveTokens + custom tokens), leaving the whole newPositionContext path dormant. Delete it end to end and point the swap methods at the same lean program-id helper everything else uses.
This commit is contained in:
@@ -1,143 +1,20 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use amm_core::{
|
||||
FEE_TIER_BPS_1, FEE_TIER_BPS_100, FEE_TIER_BPS_30, FEE_TIER_BPS_5, MINIMUM_LIQUIDITY,
|
||||
};
|
||||
use nssa_core::{account::AccountId, program::ProgramId};
|
||||
use serde_json::{json, Value};
|
||||
use token_core::TokenDefinition;
|
||||
|
||||
use super::{
|
||||
config::load_config,
|
||||
holding::{select_holding, wallet_holdings, SelectedHolding},
|
||||
quote_error::issue,
|
||||
ContextRequest, ResolveTokensRequest, TokenIdsRequest,
|
||||
};
|
||||
use crate::account::{
|
||||
account_id_from_hex, account_id_hex, decode_account, parse_base58_id, parse_program_id,
|
||||
program_id_base58, AccountRead,
|
||||
holding::{select_holding, wallet_holdings},
|
||||
ResolveTokensRequest,
|
||||
};
|
||||
use crate::account::{account_id_from_hex, decode_account, parse_program_id, AccountRead};
|
||||
|
||||
pub(super) fn token_ids(request: TokenIdsRequest) -> 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(manifest_error("config_unavailable"));
|
||||
};
|
||||
|
||||
let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
|
||||
let mut token_ids = BTreeSet::new();
|
||||
for id in &request.configured_token_ids {
|
||||
if let Ok(id) = account_id_from_hex(id, "configured token id") {
|
||||
token_ids.insert(id);
|
||||
}
|
||||
}
|
||||
for id in request
|
||||
.recent_token_ids
|
||||
.iter()
|
||||
.chain(&request.resolved_token_ids)
|
||||
{
|
||||
if let Ok(id) = parse_base58_id(id, "token id") {
|
||||
token_ids.insert(id);
|
||||
}
|
||||
}
|
||||
token_ids.extend(holdings.into_iter().map(|holding| holding.definition_id));
|
||||
|
||||
Ok(json!({
|
||||
"status": "ok",
|
||||
"tokenIds": token_ids.into_iter().map(account_id_hex).collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn manifest_error(code: &str) -> Value {
|
||||
json!({ "status": "error", "code": code, "tokenIds": [] })
|
||||
}
|
||||
|
||||
pub(super) fn context(request: ContextRequest) -> 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(context_error(&request, "config_unavailable"));
|
||||
};
|
||||
|
||||
let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
|
||||
let source_map = token_sources(&request, &holdings);
|
||||
let mut rows = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
for (token_id, sources) in source_map {
|
||||
let read = request
|
||||
.token_definitions
|
||||
.iter()
|
||||
.find(|read| account_id_from_hex(&read.id, "token definition id") == Ok(token_id));
|
||||
let (name, total_supply, metadata_id) =
|
||||
match fungible_definition(read, token_id, config.token_program_id) {
|
||||
Ok(definition) => definition,
|
||||
Err(error) => {
|
||||
rows.push(unavailable_token_row(token_id, sources, error.code));
|
||||
if error.warn {
|
||||
warnings.push(issue(
|
||||
error.code,
|
||||
"Token definition could not be read.",
|
||||
&[],
|
||||
json!({ "tokenId": token_id.to_string() }),
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let selected = select_holding(&holdings, token_id);
|
||||
let mut row = json!({
|
||||
"definitionId": token_id.to_string(),
|
||||
"name": name,
|
||||
"metadataId": metadata_id.map(|id| id.to_string()),
|
||||
"totalSupplyRaw": total_supply.to_string(),
|
||||
"ownerProgramId": program_id_base58(config.token_program_id),
|
||||
"public": true,
|
||||
"fungible": true,
|
||||
"selectable": true,
|
||||
"status": "available",
|
||||
"code": "available",
|
||||
"sources": sources,
|
||||
});
|
||||
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();
|
||||
right_holding.cmp(&left_holding).then_with(|| {
|
||||
left["definitionId"]
|
||||
.as_str()
|
||||
.cmp(&right["definitionId"].as_str())
|
||||
})
|
||||
});
|
||||
|
||||
Ok(json!({
|
||||
"status": if request.wallet_available { "ready" } else { "no_wallet" },
|
||||
"networkId": request.network_id,
|
||||
"networkFingerprint": request.network_fingerprint,
|
||||
"walletAvailable": request.wallet_available,
|
||||
"minimumLiquidityRaw": MINIMUM_LIQUIDITY.to_string(),
|
||||
"programIds": {
|
||||
"amm": program_id_base58(amm_program),
|
||||
"token": program_id_base58(config.token_program_id),
|
||||
"twapOracle": program_id_base58(config.twap_oracle_program_id),
|
||||
},
|
||||
"tokens": rows,
|
||||
"feeTiers": fee_tiers(),
|
||||
"warnings": warnings,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Resolves an explicit, app-provided set of token ids into selector rows — the lean,
|
||||
/// stateless successor to `context`. The app owns the id set (its configured tokens plus any
|
||||
/// custom/pasted ids it remembers), so there is no network envelope and no process-cached wallet
|
||||
/// state here: the module reads the definitions + wallet fresh and passes them in, exactly like
|
||||
/// `context` did per token.
|
||||
/// Resolves an explicit, app-provided set of token ids into selector rows. The app owns the
|
||||
/// id set (its configured tokens plus any custom/pasted ids it remembers), so there is no
|
||||
/// network envelope, no status, and no process-cached wallet state here: the module reads the
|
||||
/// definitions + wallet fresh and passes them in.
|
||||
///
|
||||
/// `token_ids` are hex (the module normalizes base58→hex at the boundary); `token_definitions`
|
||||
/// are the corresponding read accounts, keyed by hex id. Every returned row has the same shape —
|
||||
@@ -154,7 +31,7 @@ pub(super) fn resolve_tokens(request: ResolveTokensRequest) -> Result<Value, Str
|
||||
let holdings = wallet_holdings(&request.wallet_accounts, config.token_program_id);
|
||||
|
||||
// De-duplicate the requested ids, dropping any malformed ones. Order is irrelevant —
|
||||
// the rows are sorted below (held first, then by id) like `context`.
|
||||
// the rows are sorted below (held first, then by id).
|
||||
let mut token_ids = BTreeSet::new();
|
||||
for id in &request.token_ids {
|
||||
if let Ok(id) = account_id_from_hex(id, "token id") {
|
||||
@@ -169,7 +46,7 @@ pub(super) fn resolve_tokens(request: ResolveTokensRequest) -> Result<Value, Str
|
||||
.iter()
|
||||
.find(|read| account_id_from_hex(&read.id, "token definition id") == Ok(token_id));
|
||||
// Only readable, fungible definitions become rows; anything else is omitted.
|
||||
let Ok((name, total_supply, _metadata_id)) =
|
||||
let Some((name, total_supply)) =
|
||||
fungible_definition(read, token_id, config.token_program_id)
|
||||
else {
|
||||
continue;
|
||||
@@ -201,120 +78,21 @@ pub(super) fn resolve_tokens(request: ResolveTokensRequest) -> Result<Value, Str
|
||||
Ok(json!({ "status": "ok", "tokens": rows }))
|
||||
}
|
||||
|
||||
fn context_error(request: &ContextRequest, code: &str) -> Value {
|
||||
json!({
|
||||
"status": "error",
|
||||
"code": code,
|
||||
"networkId": request.network_id,
|
||||
"networkFingerprint": request.network_fingerprint,
|
||||
"walletAvailable": request.wallet_available,
|
||||
"tokens": [],
|
||||
"feeTiers": fee_tiers(),
|
||||
"warnings": [],
|
||||
})
|
||||
}
|
||||
|
||||
fn fee_tiers() -> Value {
|
||||
json!([
|
||||
{ "feeBps": FEE_TIER_BPS_1, "label": "0.01%", "enabled": true },
|
||||
{ "feeBps": FEE_TIER_BPS_5, "label": "0.05%", "enabled": true },
|
||||
{ "feeBps": FEE_TIER_BPS_30, "label": "0.30%", "enabled": true },
|
||||
{ "feeBps": FEE_TIER_BPS_100, "label": "1.00%", "enabled": true },
|
||||
])
|
||||
}
|
||||
|
||||
fn token_sources(
|
||||
request: &ContextRequest,
|
||||
holdings: &[SelectedHolding],
|
||||
) -> BTreeMap<AccountId, Vec<String>> {
|
||||
let mut sources: BTreeMap<AccountId, BTreeSet<String>> = BTreeMap::new();
|
||||
for id in &request.configured_token_ids {
|
||||
if let Ok(id) = account_id_from_hex(id, "configured token id") {
|
||||
sources
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.insert(String::from("config"));
|
||||
}
|
||||
}
|
||||
for (ids, source) in [
|
||||
(&request.recent_token_ids, "recent"),
|
||||
(&request.resolved_token_ids, "resolved"),
|
||||
] {
|
||||
for id in ids {
|
||||
if let Ok(id) = parse_base58_id(id, "token id") {
|
||||
sources.entry(id).or_default().insert(String::from(source));
|
||||
}
|
||||
}
|
||||
}
|
||||
for holding in holdings {
|
||||
sources
|
||||
.entry(holding.definition_id)
|
||||
.or_default()
|
||||
.insert(String::from("holding"));
|
||||
}
|
||||
sources
|
||||
.into_iter()
|
||||
.map(|(id, values)| (id, values.into_iter().collect()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unavailable_token_row(token_id: AccountId, sources: Vec<String>, code: &str) -> Value {
|
||||
json!({
|
||||
"definitionId": token_id.to_string(),
|
||||
"name": "",
|
||||
"metadataId": Value::Null,
|
||||
"totalSupplyRaw": "0",
|
||||
"selectable": false,
|
||||
"status": code,
|
||||
"code": code,
|
||||
"sources": sources,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) struct DefinitionError {
|
||||
pub(super) code: &'static str,
|
||||
pub(super) warn: bool,
|
||||
}
|
||||
|
||||
pub(super) fn fungible_definition(
|
||||
/// Decodes a token definition read as a fungible `(name, total_supply)`, or `None` when it is
|
||||
/// unreadable, owned by a different program, its id mismatches, or it isn't fungible.
|
||||
fn fungible_definition(
|
||||
read: Option<&AccountRead>,
|
||||
token_id: AccountId,
|
||||
token_program: ProgramId,
|
||||
) -> Result<(String, u128, Option<AccountId>), DefinitionError> {
|
||||
let Some(read) = read else {
|
||||
return Err(DefinitionError {
|
||||
code: "token_definition_unreadable",
|
||||
warn: true,
|
||||
});
|
||||
};
|
||||
let Ok((id, account)) = decode_account(read) else {
|
||||
return Err(DefinitionError {
|
||||
code: "token_definition_unreadable",
|
||||
warn: true,
|
||||
});
|
||||
};
|
||||
if id != token_id {
|
||||
return Err(DefinitionError {
|
||||
code: "token_definition_unreadable",
|
||||
warn: false,
|
||||
});
|
||||
}
|
||||
if account.program_owner != token_program {
|
||||
return Err(DefinitionError {
|
||||
code: "token_program_mismatch",
|
||||
warn: false,
|
||||
});
|
||||
) -> Option<(String, u128)> {
|
||||
let (id, account) = decode_account(read?).ok()?;
|
||||
if id != token_id || account.program_owner != token_program {
|
||||
return None;
|
||||
}
|
||||
match TokenDefinition::try_from(&account.data) {
|
||||
Ok(TokenDefinition::Fungible {
|
||||
name,
|
||||
total_supply,
|
||||
metadata_id,
|
||||
..
|
||||
}) => Ok((name, total_supply, metadata_id)),
|
||||
_ => Err(DefinitionError {
|
||||
code: "token_not_fungible",
|
||||
warn: false,
|
||||
}),
|
||||
name, total_supply, ..
|
||||
}) => Some((name, total_supply)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ mod holding;
|
||||
mod liquidity;
|
||||
mod pair;
|
||||
mod quote;
|
||||
mod quote_error;
|
||||
mod request;
|
||||
mod swap;
|
||||
mod token_holdings;
|
||||
@@ -18,12 +17,11 @@ mod tests;
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
pub use request::{
|
||||
AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, ContextRequest,
|
||||
CreatePoolPlanRequest, CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest,
|
||||
ProgramIdRequest, RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest,
|
||||
AddLiquidityPlanRequest, AddLiquidityQuoteRequest, ConfigIdRequest, CreatePoolPlanRequest,
|
||||
CreatePoolQuoteRequest, FeeTiersRequest, PairIdsRequest, PoolIdRequest, ProgramIdRequest,
|
||||
RemoveLiquidityPlanRequest, RemoveLiquidityQuoteRequest, ResolvePoolRequest,
|
||||
ResolveTokensRequest, SwapExactInPlanRequest, SwapExactInQuoteRequest, SwapExactOutPlanRequest,
|
||||
SwapExactOutQuoteRequest, SwapPairRequest, SyncReservesPlanRequest, TokenHoldingsRequest,
|
||||
TokenIdsRequest,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -68,22 +66,12 @@ pub fn config_id(request: ConfigIdRequest) -> AmmResult {
|
||||
config::config_id(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Discovers token definition IDs available to the active wallet and app.
|
||||
pub fn token_ids(request: TokenIdsRequest) -> AmmResult {
|
||||
context::token_ids(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)
|
||||
}
|
||||
|
||||
/// Builds network, token, holding, and fee-tier context.
|
||||
pub fn context(request: ContextRequest) -> AmmResult {
|
||||
context::context(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Resolves an app-provided set of token ids into selector rows (lean successor to `context`).
|
||||
/// Resolves an app-provided set of token ids into liquidity selector rows.
|
||||
pub fn resolve_tokens(request: ResolveTokensRequest) -> AmmResult {
|
||||
context::resolve_tokens(request).map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub(super) fn issue(code: &str, message: &str, fields: &[&str], details: Value) -> Value {
|
||||
json!({
|
||||
"code": code,
|
||||
"message": message,
|
||||
"details": details,
|
||||
"recoverable": true,
|
||||
"blockingFields": fields,
|
||||
})
|
||||
}
|
||||
@@ -8,45 +8,9 @@ pub struct ConfigIdRequest {
|
||||
pub amm_program_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenIdsRequest {
|
||||
pub amm_program_id: String,
|
||||
pub config: AccountRead,
|
||||
#[serde(default)]
|
||||
pub wallet_accounts: Vec<AccountRead>,
|
||||
#[serde(default)]
|
||||
pub configured_token_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub recent_token_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub resolved_token_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContextRequest {
|
||||
pub network_id: String,
|
||||
pub network_fingerprint: String,
|
||||
pub amm_program_id: String,
|
||||
pub wallet_available: bool,
|
||||
pub config: AccountRead,
|
||||
#[serde(default)]
|
||||
pub wallet_accounts: Vec<AccountRead>,
|
||||
#[serde(default)]
|
||||
pub token_definitions: Vec<AccountRead>,
|
||||
#[serde(default)]
|
||||
pub configured_token_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub recent_token_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub resolved_token_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Resolves an app-provided set of token ids into selector rows (the lean successor to
|
||||
/// `ContextRequest`). `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.
|
||||
/// 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.
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResolveTokensRequest {
|
||||
|
||||
@@ -14,13 +14,12 @@ use token_core::{TokenDefinition, TokenHolding};
|
||||
use twap_oracle_core::compute_current_tick_account_pda;
|
||||
|
||||
use super::{
|
||||
context::{context, resolve_tokens, token_ids},
|
||||
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},
|
||||
ContextRequest, PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest,
|
||||
SwapExactOutPlanRequest, TokenIdsRequest,
|
||||
PairIdsRequest, ResolveTokensRequest, SwapExactInPlanRequest, SwapExactOutPlanRequest,
|
||||
};
|
||||
use crate::{
|
||||
account::{account_id_hex, account_read, decode_account, program_id_bytes},
|
||||
@@ -190,107 +189,6 @@ fn pair_manifest_reports_unavailable_config_as_domain_error() {
|
||||
assert_eq!(result["code"], "config_unavailable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_manifest_includes_compatible_wallet_holdings() {
|
||||
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||
let configured = AccountId::new([1; 32]);
|
||||
let held = AccountId::new([2; 32]);
|
||||
let recent = AccountId::new([3; 32]);
|
||||
let resolved = AccountId::new([4; 32]);
|
||||
|
||||
let value = token_ids(TokenIdsRequest {
|
||||
amm_program_id: amm_program_id(),
|
||||
config: account_read(config_id, &config_account()),
|
||||
wallet_accounts: vec![account_read(
|
||||
AccountId::new([5; 32]),
|
||||
&token_holding(held, 9),
|
||||
)],
|
||||
configured_token_ids: vec![account_id_hex(configured)],
|
||||
recent_token_ids: vec![recent.to_string()],
|
||||
resolved_token_ids: vec![resolved.to_string()],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
value["tokenIds"],
|
||||
json!([
|
||||
account_id_hex(configured),
|
||||
account_id_hex(held),
|
||||
account_id_hex(recent),
|
||||
account_id_hex(resolved),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_program_holdings_do_not_contribute_token_candidates() {
|
||||
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||
let config = config_account();
|
||||
let definition = AccountId::new([2; 32]);
|
||||
let wrong_owner_holding = account(
|
||||
[99; 8],
|
||||
Data::from(&TokenHolding::Fungible {
|
||||
definition_id: definition,
|
||||
balance: 9,
|
||||
}),
|
||||
);
|
||||
let wallet_accounts = vec![account_read(AccountId::new([3; 32]), &wrong_owner_holding)];
|
||||
|
||||
let manifest = token_ids(TokenIdsRequest {
|
||||
amm_program_id: amm_program_id(),
|
||||
config: account_read(config_id, &config),
|
||||
wallet_accounts: wallet_accounts.clone(),
|
||||
configured_token_ids: Vec::new(),
|
||||
recent_token_ids: Vec::new(),
|
||||
resolved_token_ids: Vec::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(manifest["tokenIds"], json!([]));
|
||||
|
||||
let value = context(ContextRequest {
|
||||
network_id: String::from("testnet"),
|
||||
network_fingerprint: String::from("block10:abc"),
|
||||
amm_program_id: amm_program_id(),
|
||||
wallet_available: true,
|
||||
config: account_read(config_id, &config),
|
||||
wallet_accounts,
|
||||
token_definitions: vec![account_read(
|
||||
definition,
|
||||
&token_definition("Token", 1_000_000),
|
||||
)],
|
||||
configured_token_ids: Vec::new(),
|
||||
recent_token_ids: Vec::new(),
|
||||
resolved_token_ids: Vec::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(value["tokens"], json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_selects_tokens_without_holdings() {
|
||||
let token_id = AccountId::new([3; 32]);
|
||||
let config_id = compute_config_pda(AMM_PROGRAM);
|
||||
let value = context(ContextRequest {
|
||||
network_id: String::from("testnet"),
|
||||
network_fingerprint: String::from("block10:abc"),
|
||||
amm_program_id: amm_program_id(),
|
||||
wallet_available: true,
|
||||
config: account_read(config_id, &config_account()),
|
||||
wallet_accounts: Vec::new(),
|
||||
token_definitions: vec![account_read(
|
||||
token_id,
|
||||
&token_definition("Token", 1_000_000),
|
||||
)],
|
||||
configured_token_ids: vec![account_id_hex(token_id)],
|
||||
recent_token_ids: Vec::new(),
|
||||
resolved_token_ids: Vec::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(value["tokens"][0]["selectable"], true);
|
||||
assert_eq!(value["tokens"][0]["sources"], json!(["config"]));
|
||||
assert!(value["tokens"][0].get("holdingId").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_tokens_returns_lean_rows_held_first_and_omits_unresolvable() {
|
||||
let held = AccountId::new([2; 32]);
|
||||
|
||||
Reference in New Issue
Block a user