diff --git a/crates/tinywallet-bus/README.md b/crates/tinywallet-bus/README.md index 505e650e..22e0bb00 100644 --- a/crates/tinywallet-bus/README.md +++ b/crates/tinywallet-bus/README.md @@ -47,6 +47,12 @@ argument schemas for all six wallet, seven Web3 and one x402 tools. The implementation crates keep their `tinytools::Tool` adapters and delegate those declarations here. A pre-move fixture pins the established tool JSON. +Contract 1.15 adds `NativeWalletTokenBalances`: catalogued token balances +(today the cluster's USDC SPL mint on Solana, read with +`getTokenAccountsByOwner` / `jsonParsed` and summed over the owner's token +accounts) as `TokenBalanceInfo` rows, kept apart from the native +`BalanceInfo` rows so existing consumers keep one row per network. + Contract 1.7 adds `Web3ConfigureWithEndpoints` alongside the unchanged `Web3Configure(())` member. Hosts that need configured RPC overrides pass the Solana cluster and overridden endpoint keys, never endpoint URLs. The module diff --git a/crates/tinywallet-bus/src/lib_tests.rs b/crates/tinywallet-bus/src/lib_tests.rs index ca4fb307..82402dce 100644 --- a/crates/tinywallet-bus/src/lib_tests.rs +++ b/crates/tinywallet-bus/src/lib_tests.rs @@ -110,7 +110,7 @@ fn network_request_keeps_legacy_json_and_omits_unset_endpoint_url() { #[test] fn the_contract_accepts_its_own_version_and_newer_minors() { - assert_eq!(CONTRACT_VERSION, (1, 14)); + assert_eq!(CONTRACT_VERSION, (1, 15)); assert!(is_compatible(CONTRACT_VERSION)); assert!(is_compatible((CONTRACT_VERSION.0, CONTRACT_VERSION.1 + 1))); assert!(!is_compatible((CONTRACT_VERSION.0 + 1, 0))); @@ -146,7 +146,7 @@ fn the_method_list_is_sorted_complete_and_free_of_duplicates() { METHODS, "sorted dispatch order, no repeats" ); - assert_eq!(METHODS.len(), 38); + assert_eq!(METHODS.len(), 39); for member in [ names::methods::ATTACH_SIGNATURE, names::methods::BUILD_UNSIGNED, @@ -174,6 +174,7 @@ fn the_method_list_is_sorted_complete_and_free_of_duplicates() { names::methods::NATIVE_WALLET_SUPPORTED_ASSETS, names::methods::NATIVE_WALLET_CHAIN_STATUS, names::methods::NATIVE_WALLET_BALANCES, + names::methods::NATIVE_WALLET_TOKEN_BALANCES, names::methods::NATIVE_WALLET_PREPARE_TRANSFER, names::methods::NATIVE_WALLET_EXECUTE_PREPARED, names::methods::NATIVE_WALLET_TX_STATUS, @@ -284,13 +285,14 @@ fn native_wallet_members_are_additive_and_use_one_typed_argument() { methods::NATIVE_WALLET_SUPPORTED_ASSETS, methods::NATIVE_WALLET_CHAIN_STATUS, methods::NATIVE_WALLET_BALANCES, + methods::NATIVE_WALLET_TOKEN_BALANCES, methods::NATIVE_WALLET_PREPARE_TRANSFER, methods::NATIVE_WALLET_EXECUTE_PREPARED, methods::NATIVE_WALLET_TX_STATUS, methods::NATIVE_WALLET_TX_RECEIPT, methods::NATIVE_WALLET_LOOKUP_TX, ]; - assert_eq!(METHODS.len(), 38); + assert_eq!(METHODS.len(), 39); assert!(native_members.iter().all(|member| METHODS.contains(member))); assert_eq!(HOST_METHODS.len(), 6); assert!(HOST_METHODS.contains(&host_methods::AUTHORIZE_NATIVE_WALLET_EXECUTION)); @@ -415,3 +417,29 @@ fn every_confidential_member_is_a_member() { assert!(CONFIDENTIAL_METHODS.contains(&names::methods::GENERATE_MNEMONIC)); assert!(CONFIDENTIAL_METHODS.contains(&names::methods::DERIVE_MNEMONIC_SEED)); } + +#[cfg(feature = "wire")] +#[test] +fn token_balance_rows_serialize_in_camel_case_with_the_mint() { + use crate::wire; + let row = wire::TokenBalanceInfo { + chain: wire::WalletChain::Solana, + evm_network: None, + address: "Owner".into(), + asset_symbol: "USDC".into(), + token_address: "Mint".into(), + decimals: 6, + raw: "1500000".into(), + formatted: "1.500000".into(), + provider_status: wire::ProviderStatus::Ready, + }; + let value = serde_json::to_value(&row).unwrap(); + assert_eq!(value["tokenAddress"], "Mint"); + assert_eq!(value["assetSymbol"], "USDC"); + assert_eq!(value["providerStatus"], "ready"); + assert!(value.get("evmNetwork").is_none()); + assert_eq!( + serde_json::from_value::(value).unwrap(), + row + ); +} diff --git a/crates/tinywallet-bus/src/names/mod.rs b/crates/tinywallet-bus/src/names/mod.rs index 71ac08b4..2d6acdad 100644 --- a/crates/tinywallet-bus/src/names/mod.rs +++ b/crates/tinywallet-bus/src/names/mod.rs @@ -94,6 +94,9 @@ pub mod methods { pub const NATIVE_WALLET_BALANCES: &str = "NativeWalletBalances"; /// Prepare a native wallet transfer quote. pub const NATIVE_WALLET_PREPARE_TRANSFER: &str = "NativeWalletPrepareTransfer"; + /// Read catalogued token balances (SPL mints on Solana) for the configured + /// accounts. Contract 1.15. + pub const NATIVE_WALLET_TOKEN_BALANCES: &str = "NativeWalletTokenBalances"; /// Execute a prepared native wallet transfer. pub const NATIVE_WALLET_EXECUTE_PREPARED: &str = "NativeWalletExecutePrepared"; /// Read native wallet transaction status. @@ -141,6 +144,7 @@ pub const METHODS: &[&str] = &[ methods::NATIVE_WALLET_NETWORK_DEFAULTS, methods::NATIVE_WALLET_PREPARE_TRANSFER, methods::NATIVE_WALLET_SUPPORTED_ASSETS, + methods::NATIVE_WALLET_TOKEN_BALANCES, methods::NATIVE_WALLET_TX_RECEIPT, methods::NATIVE_WALLET_TX_STATUS, methods::SIGN_MESSAGE, diff --git a/crates/tinywallet-bus/src/version/mod.rs b/crates/tinywallet-bus/src/version/mod.rs index 3c97b461..62fb4fd2 100644 --- a/crates/tinywallet-bus/src/version/mod.rs +++ b/crates/tinywallet-bus/src/version/mod.rs @@ -1,7 +1,7 @@ //! The `TinyWallet` `TinyBus` contract version and its binding rule. /// The wire contract version this crate defines. -pub const CONTRACT_VERSION: (u32, u32) = (1, 14); +pub const CONTRACT_VERSION: (u32, u32) = (1, 15); /// Returns whether a host using [`CONTRACT_VERSION`] can bind to `module`. /// diff --git a/crates/tinywallet-bus/src/wire/mod.rs b/crates/tinywallet-bus/src/wire/mod.rs index f63ee511..abae3c28 100644 --- a/crates/tinywallet-bus/src/wire/mod.rs +++ b/crates/tinywallet-bus/src/wire/mod.rs @@ -62,8 +62,9 @@ pub use wallet::{ NativeWalletExecuteRequest, NativeWalletExecutionIntent, NativeWalletPrepareTransferRequest, NativeWalletServiceHandle, NativeWalletServiceRequest, NativeWalletTransactionRequest, PrepareTransferParams, PreparedKind, PreparedStatus, PreparedTransaction, ProviderStatus, - RpcEndpointKey, RpcSource, SolanaCluster, SupportedAsset, TxLookupInfo, TxReceiptInfo, TxState, - TxStatusInfo, WalletAssetDefinition, WalletChain, WalletNetworkDefaults, + RpcEndpointKey, RpcSource, SolanaCluster, SupportedAsset, TokenBalanceInfo, TxLookupInfo, + TxReceiptInfo, TxState, TxStatusInfo, WalletAssetDefinition, WalletChain, + WalletNetworkDefaults, }; use crate::Chain; diff --git a/crates/tinywallet-bus/src/wire/wallet.rs b/crates/tinywallet-bus/src/wire/wallet.rs index 0dcd1ffc..6a481a46 100644 --- a/crates/tinywallet-bus/src/wire/wallet.rs +++ b/crates/tinywallet-bus/src/wire/wallet.rs @@ -465,6 +465,33 @@ pub struct BalanceInfo { pub provider_status: ProviderStatus, } +/// A token balance row: one catalogued token (an SPL mint on Solana) held by +/// one wallet account. Native balances stay in [`BalanceInfo`]. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct TokenBalanceInfo { + /// Chain family. + pub chain: WalletChain, + /// EVM network for EVM rows. + #[serde(skip_serializing_if = "Option::is_none")] + pub evm_network: Option, + /// Account (owner) address. + pub address: String, + /// Asset symbol from the catalog. + pub asset_symbol: String, + /// Token contract or mint address. + pub token_address: String, + /// Smallest-unit decimal places. + pub decimals: u8, + /// Raw smallest-unit amount, summed over every token account the owner + /// holds for this mint. + pub raw: String, + /// Human-readable amount. + pub formatted: String, + /// Provider state; `missing` rows read as zero. + pub provider_status: ProviderStatus, +} + /// What a prepared transfer moves. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] diff --git a/crates/tinywallet-module/src/service/mod.rs b/crates/tinywallet-module/src/service/mod.rs index cd8f8e66..bbc5e206 100644 --- a/crates/tinywallet-module/src/service/mod.rs +++ b/crates/tinywallet-module/src/service/mod.rs @@ -447,6 +447,26 @@ impl Wallet { .map_err(native_wallet_operation_failed) } + /// Read catalogued token balances (SPL mints on Solana) for the + /// configured accounts. Contract 1.15. + async fn native_wallet_token_balances( + &self, + request: tinywallet_bus::wire::NativeWalletServiceRequest, + ) -> BusResult> { + let active = self.native.read().await; + let Some(active) = active.as_ref() else { + return Err(host_unavailable()); + }; + active + .require_handle(&request.service) + .map_err(native_wallet_operation_failed)?; + active + .engine + .token_balances() + .await + .map_err(native_wallet_operation_failed) + } + /// Validate and store an owner-scoped transfer quote inside the module. async fn native_wallet_prepare_transfer( &self, @@ -1537,6 +1557,7 @@ mod exports { "NativeWalletSupportedAssets", "NativeWalletChainStatus", "NativeWalletBalances", + "NativeWalletTokenBalances", "NativeWalletPrepareTransfer", "NativeWalletExecutePrepared", "NativeWalletTxStatus", diff --git a/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs b/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs index 492c7946..6d7999e4 100644 --- a/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs +++ b/crates/tinywallet-module/src/service/web3_native_wallet_tests.rs @@ -108,6 +108,19 @@ async fn native_wallet_configuration_uses_opaque_endpoint_references_and_invalid .await .unwrap(); assert_eq!(balances.len(), 6); + let tokens = wallet + .native_wallet_token_balances(NativeWalletServiceRequest { + service: old.clone(), + }) + .await + .unwrap(); + // The devnet catalog's USDC mint, read for the Solana account. + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].asset_symbol, "USDC"); + assert_eq!( + tokens[0].token_address, + "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" + ); // A provider read proves the opaque reference is carried to the host // callback while the URL stays inside the host. diff --git a/crates/tinywallet-module/tests/lifecycle_e2e.rs b/crates/tinywallet-module/tests/lifecycle_e2e.rs index 944b3300..8745ffa6 100644 --- a/crates/tinywallet-module/tests/lifecycle_e2e.rs +++ b/crates/tinywallet-module/tests/lifecycle_e2e.rs @@ -82,6 +82,7 @@ const EXPECTED_METHODS: &[&str] = &[ "NativeWalletSupportedAssets", "NativeWalletChainStatus", "NativeWalletBalances", + "NativeWalletTokenBalances", "NativeWalletPrepareTransfer", "NativeWalletExecutePrepared", "NativeWalletTxStatus", diff --git a/crates/tinywallet-module/tests/module_e2e.rs b/crates/tinywallet-module/tests/module_e2e.rs index 3346873e..37892958 100644 --- a/crates/tinywallet-module/tests/module_e2e.rs +++ b/crates/tinywallet-module/tests/module_e2e.rs @@ -61,6 +61,7 @@ const EXPECTED_METHODS: &[&str] = &[ "NativeWalletSupportedAssets", "NativeWalletChainStatus", "NativeWalletBalances", + "NativeWalletTokenBalances", "NativeWalletPrepareTransfer", "NativeWalletExecutePrepared", "NativeWalletTxStatus", diff --git a/crates/tinywallet-web3/src/crypto/chains/solana/mod.rs b/crates/tinywallet-web3/src/crypto/chains/solana/mod.rs index fb26e679..0be29f75 100644 --- a/crates/tinywallet-web3/src/crypto/chains/solana/mod.rs +++ b/crates/tinywallet-web3/src/crypto/chains/solana/mod.rs @@ -79,6 +79,60 @@ pub(crate) async fn native_balance(engine: &WalletEngine, address: &str) -> Resu Ok(u128::from(result.value)) } +/// Balance of one SPL `mint` held by `owner`, in the mint's atomic units. +/// +/// Reads `getTokenAccountsByOwner` filtered to the mint with the `jsonParsed` +/// encoding and sums `tokenAmount.amount` over every token account returned +/// (the associated account and any others). No accounts is a zero balance. +pub(crate) async fn token_balance( + engine: &WalletEngine, + owner: &str, + mint: &str, +) -> Result { + validate_solana_address(owner)?; + validate_solana_address(mint)?; + let result: serde_json::Value = engine + .rpc_call( + WalletChain::Solana, + "getTokenAccountsByOwner", + json!([owner, {"mint": mint}, {"encoding": "jsonParsed"}]), + ) + .await?; + sum_parsed_token_amounts(&result, mint) +} + +/// Sum `tokenAmount.amount` over a `getTokenAccountsByOwner` (`jsonParsed`) +/// result, refusing an account for another mint or an unparsable amount. +pub(crate) fn sum_parsed_token_amounts( + result: &serde_json::Value, + mint: &str, +) -> Result { + let accounts = result + .get("value") + .and_then(serde_json::Value::as_array) + .ok_or_else(|| "getTokenAccountsByOwner returned no account list".to_string())?; + let mut total: u128 = 0; + for entry in accounts { + let info = entry + .pointer("/account/data/parsed/info") + .ok_or_else(|| "getTokenAccountsByOwner returned an unparsed account".to_string())?; + if info.get("mint").and_then(serde_json::Value::as_str) != Some(mint) { + return Err("getTokenAccountsByOwner returned an account for another mint".into()); + } + let amount = info + .pointer("/tokenAmount/amount") + .and_then(serde_json::Value::as_str) + .and_then(|amount| amount.parse::().ok()) + .ok_or_else(|| { + "getTokenAccountsByOwner returned an invalid token amount".to_string() + })?; + total = total + .checked_add(amount) + .ok_or_else(|| "token balance overflow".to_string())?; + } + Ok(total) +} + /// The wallet's Solana public key, as the signer derives it. async fn signer_pubkey(engine: &WalletEngine) -> Result<[u8; 32], String> { let account = engine.signer.derive_account(WalletChain::Solana).await?; diff --git a/crates/tinywallet-web3/src/crypto/execution/mod.rs b/crates/tinywallet-web3/src/crypto/execution/mod.rs index 9e8ed550..e6bef5b8 100644 --- a/crates/tinywallet-web3/src/crypto/execution/mod.rs +++ b/crates/tinywallet-web3/src/crypto/execution/mod.rs @@ -36,7 +36,7 @@ pub(crate) use types::PreparedTransferQuote; pub use types::{ BalanceInfo, ChainStatus, ExecutePreparedParams, ExecutionResult, PrepareTransferParams, PreparedKind, PreparedStatus, PreparedTransaction, ProviderStatus, RawBroadcastResult, - SupportedAsset, TxLookupInfo, TxReceiptInfo, TxState, TxStatusInfo, + SupportedAsset, TokenBalanceInfo, TxLookupInfo, TxReceiptInfo, TxState, TxStatusInfo, }; pub use validate::{hex_to_bytes, hex_to_u128, u128_to_hex}; diff --git a/crates/tinywallet-web3/src/crypto/execution/mod_tests.rs b/crates/tinywallet-web3/src/crypto/execution/mod_tests.rs index 6999d5f5..fb9b08bc 100644 --- a/crates/tinywallet-web3/src/crypto/execution/mod_tests.rs +++ b/crates/tinywallet-web3/src/crypto/execution/mod_tests.rs @@ -893,3 +893,109 @@ async fn an_erc20_transfer_pays_the_contract_zero_with_the_recipient_in_calldata other => panic!("expected an EVM spec, got {other:?}"), } } + +const MAINNET_USDC: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; +const DEVNET_USDC: &str = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"; + +fn parsed_token_account(mint: &str, amount: &str) -> Value { + json!({ + "pubkey": "TokenAccount1111111111111111111111111111111", + "account": { + "data": { + "program": "spl-token", + "parsed": { + "type": "account", + "info": { + "mint": mint, + "owner": sample_address(WalletChain::Solana), + "tokenAmount": {"amount": amount, "decimals": 6, "uiAmountString": "x"} + } + } + } + } + }) +} + +#[tokio::test] +async fn token_balances_sum_every_spl_account_of_the_catalog_usdc_mint() { + let rig = Rig::new(); + rig.transport.on_rpc( + "getTokenAccountsByOwner", + json!({ + "context": {"slot": 1}, + "value": [ + parsed_token_account(MAINNET_USDC, "1500000"), + parsed_token_account(MAINNET_USDC, "250000"), + ] + }), + ); + + let rows = rig.engine.token_balances().await.unwrap(); + + assert_eq!(rows.len(), 1, "{rows:?}"); + let usdc = &rows[0]; + assert_eq!(usdc.chain, WalletChain::Solana); + assert_eq!(usdc.asset_symbol, "USDC"); + assert_eq!(usdc.token_address, MAINNET_USDC); + assert_eq!(usdc.address, sample_address(WalletChain::Solana)); + assert_eq!(usdc.raw, "1750000"); + assert_eq!(usdc.formatted, "1.750000"); + assert_eq!(usdc.provider_status, ProviderStatus::Ready); + + let (method, params) = rig + .transport + .rpc_calls() + .into_iter() + .find(|(method, _)| method == "getTokenAccountsByOwner") + .unwrap(); + assert_eq!(method, "getTokenAccountsByOwner"); + assert_eq!(params[0], sample_address(WalletChain::Solana)); + assert_eq!(params[1], json!({"mint": MAINNET_USDC})); + assert_eq!(params[2], json!({"encoding": "jsonParsed"})); +} + +#[tokio::test] +async fn token_balances_use_the_devnet_mint_on_devnet_and_read_no_accounts_as_zero() { + let rig = Rig::new(); + rig.endpoints + .set_cluster(tinywallet_bus::wire::SolanaCluster::Devnet); + rig.transport.on_rpc( + "getTokenAccountsByOwner", + json!({"context": {"slot": 1}, "value": []}), + ); + + let rows = rig.engine.token_balances().await.unwrap(); + + assert_eq!(rows[0].token_address, DEVNET_USDC); + assert_eq!(rows[0].raw, "0"); + assert_eq!(rows[0].provider_status, ProviderStatus::Ready); +} + +#[tokio::test] +async fn token_balances_mark_an_unreadable_or_foreign_mint_answer_missing() { + let rig = Rig::new(); + // Nothing scripted: the read fails. + let rows = rig.engine.token_balances().await.unwrap(); + assert_eq!(rows[0].raw, "0"); + assert_eq!(rows[0].provider_status, ProviderStatus::Missing); + + // An account for another mint is not counted as USDC. + let rig = Rig::new(); + rig.transport.on_rpc( + "getTokenAccountsByOwner", + json!({"value": [parsed_token_account(DEVNET_USDC, "9")]}), + ); + let rows = rig.engine.token_balances().await.unwrap(); + assert_eq!(rows[0].raw, "0"); + assert_eq!(rows[0].provider_status, ProviderStatus::Missing); +} + +#[tokio::test] +async fn token_balances_need_a_configured_wallet() { + let rig = Rig::new(); + rig.accounts.set(Ok(FakeWalletAccounts::unconfigured())); + assert_eq!( + rig.engine.token_balances().await.unwrap_err(), + WALLET_NOT_CONFIGURED_MESSAGE + ); +} diff --git a/crates/tinywallet-web3/src/crypto/execution/queries.rs b/crates/tinywallet-web3/src/crypto/execution/queries.rs index 52a02a39..b80ddb6e 100644 --- a/crates/tinywallet-web3/src/crypto/execution/queries.rs +++ b/crates/tinywallet-web3/src/crypto/execution/queries.rs @@ -12,7 +12,7 @@ use crate::crypto::wallet::{WalletAccount, WalletChain, WalletEngine}; use crate::quote::WALLET_NOT_CONFIGURED_MESSAGE; use super::LOG_PREFIX; -use super::types::{BalanceInfo, ChainStatus, ProviderStatus, SupportedAsset}; +use super::types::{BalanceInfo, ChainStatus, ProviderStatus, SupportedAsset, TokenBalanceInfo}; use super::validate::format_amount; /// EVM networks surfaced as their own native-balance rows. The single derived @@ -180,6 +180,64 @@ impl WalletEngine { Ok(out) } + /// Live balances of the catalogued tokens: today every SPL mint in the + /// Solana catalog for the configured cluster (USDC; devnet or mainnet + /// mint). One row per token, a provider failure yields a zero row marked + /// [`ProviderStatus::Missing`] rather than failing the read. + /// + /// # Errors + /// + /// [`WALLET_NOT_CONFIGURED_MESSAGE`] when the wallet is not set up, or the + /// host's error if the status cannot be read. + pub async fn token_balances(&self) -> Result, String> { + let status = self.accounts.status().await?; + if !status.configured { + return Err(WALLET_NOT_CONFIGURED_MESSAGE.to_string()); + } + let cluster = self.endpoints.solana_cluster(); + let mut out = Vec::new(); + for account in status + .accounts + .iter() + .filter(|account| account.chain == WalletChain::Solana) + { + for asset in asset_catalog(WalletChain::Solana, cluster) { + let Some(mint) = asset.contract_address.clone() else { + continue; + }; + let (raw, provider_status) = match solana::token_balance( + self, + &account.address, + &mint, + ) + .await + { + Ok(balance) => (balance, ProviderStatus::Ready), + Err(error) => { + warn!( + "{LOG_PREFIX} token_balances symbol={} falling back to zero: {error}", + asset.symbol + ); + (0, ProviderStatus::Missing) + } + }; + out.push(TokenBalanceInfo { + chain: WalletChain::Solana, + evm_network: None, + address: account.address.clone(), + asset_symbol: asset.symbol, + token_address: mint, + decimals: asset.decimals, + raw: raw.to_string(), + formatted: format_amount(raw, asset.decimals), + provider_status, + }); + } + } + debug!("{LOG_PREFIX} token_balances returned rows={}", out.len()); + Ok(out) + } + /// Read one native balance, falling back to a zero row on failure. async fn balance_row( &self, diff --git a/crates/tinywallet-web3/src/crypto/execution/types.rs b/crates/tinywallet-web3/src/crypto/execution/types.rs index b1d625b0..0cbd4a2f 100644 --- a/crates/tinywallet-web3/src/crypto/execution/types.rs +++ b/crates/tinywallet-web3/src/crypto/execution/types.rs @@ -7,7 +7,7 @@ use crate::quote::Quoted; pub use tinywallet_bus::wire::{ BalanceInfo, ChainStatus, ExecutePreparedParams, ExecutionResult, PrepareTransferParams, PreparedKind, PreparedStatus, PreparedTransaction, ProviderStatus, SupportedAsset, - TxLookupInfo, TxReceiptInfo, TxState, TxStatusInfo, + TokenBalanceInfo, TxLookupInfo, TxReceiptInfo, TxState, TxStatusInfo, }; /// The owner-bound record held in the implementation's quote store. diff --git a/docs/specs/native-wallet-engine-module.md b/docs/specs/native-wallet-engine-module.md index 87c1b07a..ffc612c7 100644 --- a/docs/specs/native-wallet-engine-module.md +++ b/docs/specs/native-wallet-engine-module.md @@ -23,6 +23,7 @@ The native service exposes the current engine surface through typed requests: - configure and close one opaque, host-owned-user-scoped service handle; - list network defaults and supported assets; - query chain readiness and native balances; +- query catalogued token balances (SPL mints on Solana; contract 1.15); - prepare and execute a transfer; - read transaction status, receipt, and raw transaction by chain and hash.