diff --git a/crates/common/src/erc20.rs b/crates/common/src/erc20.rs index 8630afa10f..9a1a226c7c 100644 --- a/crates/common/src/erc20.rs +++ b/crates/common/src/erc20.rs @@ -3,6 +3,7 @@ use alloy::primitives::{Address, U256}; use alloy::providers::{MulticallError, Provider}; use alloy_ethers_typecast::ReadContractParametersBuilderError; use rain_error_decoding::{AbiDecodeFailedErrors, AbiDecodedErrorType}; +use rain_orderbook_app_settings::token::TokenCfg; use rain_orderbook_bindings::provider::{mk_read_provider, ReadProvider, ReadProviderError}; use rain_orderbook_bindings::IERC20::IERC20Instance; use serde::{Deserialize, Serialize}; @@ -21,6 +22,54 @@ pub struct TokenInfo { #[cfg(target_family = "wasm")] impl_wasm_traits!(TokenInfo); +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(target_family = "wasm", derive(Tsify))] +#[serde(rename_all = "camelCase")] +pub struct ExtendedTokenInfo { + pub key: String, + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] + pub address: Address, + pub decimals: u8, + pub name: String, + pub symbol: String, + pub chain_id: u32, + #[cfg_attr(target_family = "wasm", tsify(optional, type = "string"))] + pub logo_uri: Option, +} +#[cfg(target_family = "wasm")] +impl_wasm_traits!(ExtendedTokenInfo); + +impl ExtendedTokenInfo { + pub async fn from_token_cfg(token: &TokenCfg) -> Result { + if let (Some(decimals), Some(label), Some(symbol)) = + (&token.decimals, &token.label, &token.symbol) + { + Ok(ExtendedTokenInfo { + key: token.key.clone(), + address: token.address, + decimals: *decimals, + name: label.clone(), + symbol: symbol.clone(), + chain_id: token.network.chain_id, + logo_uri: token.logo_uri.clone(), + }) + } else { + let erc20 = ERC20::new(token.network.rpcs.clone(), token.address); + let onchain_info = erc20.token_info(None).await?; + + Ok(ExtendedTokenInfo { + key: token.key.clone(), + address: token.address, + decimals: token.decimals.unwrap_or(onchain_info.decimals), + name: token.label.clone().unwrap_or(onchain_info.name), + symbol: token.symbol.clone().unwrap_or(onchain_info.symbol), + chain_id: token.network.chain_id, + logo_uri: token.logo_uri.clone(), + }) + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ERC20 { pub rpcs: Vec, diff --git a/crates/js_api/ARCHITECTURE.md b/crates/js_api/ARCHITECTURE.md index fbf040df26..fb99fd2c74 100644 --- a/crates/js_api/ARCHITECTURE.md +++ b/crates/js_api/ARCHITECTURE.md @@ -130,14 +130,16 @@ - `getAllOrderDetails()` → parse order-level metadata for every merged dotrain. - `getOrderKeys()` → keys from `order_urls`. - `getDeploymentDetails(orderKey)` → deployment name/description map for a specific order. + - `getOrderbookYaml() -> OrderbookYaml` → returns an `OrderbookYaml` instance from the registry's shared settings YAML for querying tokens, networks, orderbooks, etc. - `getGui(orderKey, deploymentKey, serializedState?, stateCallback?)` → merge `settings + order`, optionally restore serialized state, and produce a `DotrainOrderGui` instance. - Errors: `DotrainRegistryError` covers fetch/parse/HTTP/URL issues and wraps `GuiError`. Also returns human-readable messages. - `yaml` (src/yaml/mod.rs) - - Purpose: Wasm-friendly wrapper around orderbook YAML parsing to retrieve an `OrderbookCfg` by contract address. + - Purpose: Wasm-friendly wrapper around orderbook YAML parsing to retrieve configuration objects by address or query token metadata. - Exports: - `OrderbookYaml.new([yamlSources], validate?) -> OrderbookYaml`: parse/merge/optionally validate sources. - `OrderbookYaml.getOrderbookByAddress(address) -> OrderbookCfg`. + - `OrderbookYaml.getTokens() -> TokenInfo[]` (async): returns all tokens from YAML with `chain_id`, `address`, `decimals`, `symbol`, and `name`. Automatically fetches remote tokens from `using-tokens-from` URLs. - Errors: `OrderbookYamlError` with readable messaging, converted to JS. **External Crates & Interactions** diff --git a/crates/js_api/src/gui/mod.rs b/crates/js_api/src/gui/mod.rs index db58896081..819fd2dea9 100644 --- a/crates/js_api/src/gui/mod.rs +++ b/crates/js_api/src/gui/mod.rs @@ -9,7 +9,6 @@ use rain_orderbook_app_settings::{ GuiCfg, GuiDeploymentCfg, GuiFieldDefinitionCfg, GuiPresetCfg, NameAndDescriptionCfg, ParseGuiConfigSourceError, }, - network::NetworkCfg, order::OrderCfg, yaml::{ context::ContextProfile, @@ -17,6 +16,7 @@ use rain_orderbook_app_settings::{ emitter, YamlError, YamlParsable, }, }; +pub use rain_orderbook_common::erc20::ExtendedTokenInfo; use rain_orderbook_common::{ dotrain::{types::patterns::FRONTMATTER_SEPARATOR, RainDocument}, dotrain_order::{DotrainOrder, DotrainOrderError}, @@ -31,7 +31,6 @@ use std::{ }; use strict_yaml_rust::StrictYaml; use thiserror::Error; -use url::Url; use wasm_bindgen_utils::{impl_wasm_traits, prelude::*, wasm_export}; mod deposits; @@ -41,19 +40,6 @@ mod select_tokens; mod state_management; mod validation; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Tsify)] -#[serde(rename_all = "camelCase")] -pub struct TokenInfo { - pub key: String, - #[tsify(type = "string")] - pub address: Address, - pub decimals: u8, - pub name: String, - pub symbol: String, - #[tsify(optional, type = "string")] - pub logo_uri: Option, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[wasm_bindgen] pub struct DotrainOrderGui { @@ -303,53 +289,16 @@ impl DotrainOrderGui { /// ``` #[wasm_export( js_name = "getTokenInfo", - unchecked_return_type = "TokenInfo", - return_description = "Complete token details including address, decimals, name, and symbol" + unchecked_return_type = "ExtendedTokenInfo", + return_description = "Complete token details including address, decimals, name, symbol, and chain_id" )] pub async fn get_token_info( &self, #[wasm_export(param_description = "Token identifier from the YAML tokens section")] key: String, - ) -> Result { + ) -> Result { let token = self.dotrain_order.orderbook_yaml().get_token(&key)?; - - let token_info = if let (Some(decimals), Some(label), Some(symbol)) = - (&token.decimals, &token.label, &token.symbol) - { - TokenInfo { - key: token.key.clone(), - address: token.address, - decimals: *decimals, - name: label.clone(), - symbol: symbol.clone(), - logo_uri: token.logo_uri.clone(), - } - } else { - let order_key = DeploymentCfg::parse_order_key( - self.dotrain_order.dotrain_yaml().documents, - &self.selected_deployment, - )?; - let network_key = OrderCfg::parse_network_key( - self.dotrain_order.dotrain_yaml().documents, - &order_key, - )?; - let rpcs = - NetworkCfg::parse_rpcs(self.dotrain_order.dotrain_yaml().documents, &network_key)?; - - let erc20 = ERC20::new(rpcs, token.address); - let onchain_info = erc20.token_info(None).await?; - - TokenInfo { - key: token.key.clone(), - address: token.address, - decimals: token.decimals.unwrap_or(onchain_info.decimals), - name: token.label.unwrap_or(onchain_info.name), - symbol: token.symbol.unwrap_or(onchain_info.symbol), - logo_uri: token.logo_uri.clone(), - } - }; - - Ok(token_info) + Ok(ExtendedTokenInfo::from_token_cfg(&token).await?) } /// Gets information for all tokens used in the current deployment's order. @@ -376,10 +325,10 @@ impl DotrainOrderGui { /// ``` #[wasm_export( js_name = "getAllTokenInfos", - unchecked_return_type = "TokenInfo[]", + unchecked_return_type = "ExtendedTokenInfo[]", return_description = "Array of complete token information" )] - pub async fn get_all_token_infos(&self) -> Result, GuiError> { + pub async fn get_all_token_infos(&self) -> Result, GuiError> { let select_tokens = self.get_select_tokens()?; let token_keys = match select_tokens.is_empty() { diff --git a/crates/js_api/src/gui/select_tokens.rs b/crates/js_api/src/gui/select_tokens.rs index b464557215..58e4826972 100644 --- a/crates/js_api/src/gui/select_tokens.rs +++ b/crates/js_api/src/gui/select_tokens.rs @@ -276,7 +276,7 @@ impl DotrainOrderGui { /// ``` #[wasm_export( js_name = "getAllTokens", - unchecked_return_type = "TokenInfo[]", + unchecked_return_type = "ExtendedTokenInfo[]", return_description = "Array of token information for the current network" )] pub async fn get_all_tokens( @@ -285,7 +285,7 @@ impl DotrainOrderGui { param_description = "Optional search term to filter tokens by name, symbol, or address" )] search: Option, - ) -> Result, GuiError> { + ) -> Result, GuiError> { let order_key = DeploymentCfg::parse_order_key( self.dotrain_order.dotrain_yaml().documents, &self.selected_deployment, @@ -293,46 +293,17 @@ impl DotrainOrderGui { let network_key = OrderCfg::parse_network_key(self.dotrain_order.dotrain_yaml().documents, &order_key)?; let tokens = self.dotrain_order.orderbook_yaml().get_tokens()?; - let network = self - .dotrain_order - .orderbook_yaml() - .get_network(&network_key)?; let mut fetch_futures = Vec::new(); - let mut results = Vec::new(); for (_, token) in tokens .into_iter() .filter(|(_, token)| token.network.key == network_key) { - if let (Some(decimals), Some(label), Some(symbol)) = - (&token.decimals, &token.label, &token.symbol) - { - results.push(TokenInfo { - key: token.key.clone(), - address: token.address, - decimals: *decimals, - name: label.clone(), - symbol: symbol.clone(), - logo_uri: token.logo_uri.clone(), - }); - } else { - let erc20 = ERC20::new(network.rpcs.clone(), token.address); - fetch_futures.push(async move { - let token_info = erc20.token_info(None).await?; - Ok::(TokenInfo { - key: token.key.clone(), - address: token.address, - decimals: token.decimals.unwrap_or(token_info.decimals), - name: token.label.unwrap_or(token_info.name), - symbol: token.symbol.unwrap_or(token_info.symbol), - logo_uri: token.logo_uri.clone(), - }) - }); - } + fetch_futures.push(async move { ExtendedTokenInfo::from_token_cfg(&token).await }); } - let fetched_results: Vec = futures::stream::iter(fetch_futures) + let mut results: Vec = futures::stream::iter(fetch_futures) .buffer_unordered(MAX_CONCURRENT_FETCHES) .filter_map(|res| async { match res { @@ -342,7 +313,6 @@ impl DotrainOrderGui { }) .collect() .await; - results.extend(fetched_results); results.sort_by(|a, b| { a.address .to_string() diff --git a/crates/js_api/src/registry.rs b/crates/js_api/src/registry.rs index 7fa7d61123..5bcfdebaf1 100644 --- a/crates/js_api/src/registry.rs +++ b/crates/js_api/src/registry.rs @@ -1,4 +1,5 @@ use crate::gui::{DotrainOrderGui, GuiError}; +use crate::yaml::{OrderbookYaml, OrderbookYamlError}; use rain_orderbook_app_settings::gui::NameAndDescriptionCfg; use reqwest; use serde::{Deserialize, Serialize}; @@ -133,6 +134,8 @@ pub enum DotrainRegistryError { UrlParseError(#[from] url::ParseError), #[error(transparent)] GuiError(#[from] GuiError), + #[error(transparent)] + OrderbookYamlError(#[from] OrderbookYamlError), } impl DotrainRegistryError { @@ -162,7 +165,8 @@ impl DotrainRegistryError { DotrainRegistryError::UrlParseError(err) => { format!("Invalid URL format: {}. Please ensure the URL is properly formatted.", err) } - DotrainRegistryError::GuiError(err) => err.to_readable_msg() + DotrainRegistryError::GuiError(err) => err.to_readable_msg(), + DotrainRegistryError::OrderbookYamlError(err) => err.to_readable_msg(), } } } @@ -469,6 +473,32 @@ impl DotrainRegistry { let gui = gui_result.map_err(DotrainRegistryError::GuiError)?; Ok(gui) } + + /// Creates an OrderbookYaml instance from the registry's shared settings. + /// + /// This method provides access to the OrderbookYaml SDK, allowing you to query tokens, + /// networks, orderbooks, and other configuration from the shared settings YAML. + /// + /// ## Examples + /// + /// ```javascript + /// const yamlResult = registry.getOrderbookYaml(); + /// if (yamlResult.error) { + /// console.error("Failed to get OrderbookYaml:", yamlResult.error.readableMsg); + /// return; + /// } + /// const orderbookYaml = yamlResult.value; + /// ``` + #[wasm_export( + js_name = "getOrderbookYaml", + preserve_js_class, + unchecked_return_type = "OrderbookYaml", + return_description = "OrderbookYaml instance from registry settings" + )] + pub fn get_orderbook_yaml(&self) -> Result { + let yaml = OrderbookYaml::new(vec![self.settings.clone()], None)?; + Ok(yaml) + } } impl DotrainRegistry { @@ -1307,5 +1337,106 @@ _ _: 1 1; _ => panic!("Expected OrderKeyNotFound error"), } } + + const MOCK_SETTINGS_WITH_TOKENS: &str = r#"version: 4 +networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + currency: ETH +tokens: + weth: + network: mainnet + address: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + decimals: 18 + label: Wrapped Ether + symbol: WETH + usdc: + network: mainnet + address: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + decimals: 6 + label: USD Coin + symbol: USDC +orderbooks: + mainnet: + address: 0x1234567890123456789012345678901234567890 + network: mainnet +deployers: + mainnet: + address: 0x1234567890123456789012345678901234567890 + network: mainnet +"#; + + const MOCK_DOTRAIN_SIMPLE: &str = r#"gui: + name: Test Order + description: Test description + deployments: + mainnet: + name: Mainnet Order + description: Mainnet deployment + deposits: + - token: weth + presets: + - "0" + fields: + - binding: test-binding + name: Test binding + presets: + - value: "0xbeef" +scenarios: + mainnet: + deployer: mainnet + runs: 1 +orders: + mainnet: + orderbook: mainnet + inputs: + - token: weth + outputs: + - token: usdc +deployments: + mainnet: + scenario: mainnet + order: mainnet +--- +#calculate-io +_ _: 0 0; +#handle-io +:; +"#; + + #[tokio::test] + async fn test_get_orderbook_yaml_returns_valid_instance() { + let server = MockServer::start_async().await; + + let test_registry_content = format!( + "{}/settings.yaml\ntest-order {}/order.rain", + server.url(""), + server.url("") + ); + + server.mock(|when, then| { + when.method("GET").path("/registry.txt"); + then.status(200).body(test_registry_content.clone()); + }); + + server.mock(|when, then| { + when.method("GET").path("/settings.yaml"); + then.status(200).body(MOCK_SETTINGS_WITH_TOKENS); + }); + + server.mock(|when, then| { + when.method("GET").path("/order.rain"); + then.status(200).body(MOCK_DOTRAIN_SIMPLE); + }); + + let registry = DotrainRegistry::new(format!("{}/registry.txt", server.url(""))) + .await + .unwrap(); + + let orderbook_yaml = registry.get_orderbook_yaml(); + assert!(orderbook_yaml.is_ok()); + } } } diff --git a/crates/js_api/src/yaml/mod.rs b/crates/js_api/src/yaml/mod.rs index c45a003493..3f8d8b1309 100644 --- a/crates/js_api/src/yaml/mod.rs +++ b/crates/js_api/src/yaml/mod.rs @@ -3,11 +3,13 @@ use std::str::FromStr; use alloy::{hex::FromHexError, primitives::Address}; use rain_orderbook_app_settings::{ orderbook::OrderbookCfg, + remote_tokens::{ParseRemoteTokensError, RemoteTokensCfg}, yaml::{ orderbook::{OrderbookYaml as OrderbookYamlCfg, OrderbookYamlValidation}, YamlError, YamlParsable, }, }; +use rain_orderbook_common::erc20::ExtendedTokenInfo; use serde::{Deserialize, Serialize}; use thiserror::Error; use wasm_bindgen_utils::prelude::*; @@ -109,6 +111,48 @@ impl OrderbookYaml { Address::from_str(orderbook_address).map_err(OrderbookYamlError::FromHexError)?; Ok(self.yaml.get_orderbook_by_address(address)?) } + + /// Retrieves all tokens from the YAML configuration, including remote tokens. + /// + /// This async function fetches all tokens defined in the YAML configuration. + /// If `using-tokens-from` URLs are configured, it will fetch and merge remote + /// tokens from those URLs before returning the complete token list. + /// + /// ## Examples + /// + /// ```javascript + /// const result = await orderbookYaml.getTokens(); + /// if (result.error) { + /// console.error("Error:", result.error.readableMsg); + /// return; + /// } + /// const tokens = result.value; + /// // Each token has: key, address, decimals, name, symbol, chainId + /// tokens.forEach(token => { + /// console.log(`${token.symbol} on chain ${token.chainId}`); + /// }); + /// ``` + #[wasm_export( + js_name = "getTokens", + unchecked_return_type = "ExtendedTokenInfo[]", + return_description = "Array of token information" + )] + pub async fn get_tokens(&mut self) -> Result, OrderbookYamlError> { + if let Some(remote_tokens_cfg) = self.yaml.get_remote_tokens()? { + let networks = self.yaml.get_networks()?; + let remote_tokens = RemoteTokensCfg::fetch_tokens(&networks, remote_tokens_cfg).await?; + self.yaml.cache.update_remote_tokens(remote_tokens); + } + + let tokens = self.yaml.get_tokens()?; + + let mut token_infos: Vec = Vec::new(); + for token in tokens.values() { + token_infos.push(ExtendedTokenInfo::from_token_cfg(token).await?); + } + + Ok(token_infos) + } } #[derive(Error, Debug)] @@ -117,6 +161,12 @@ pub enum OrderbookYamlError { YamlError(#[from] YamlError), #[error("Invalid address: {0}")] FromHexError(#[from] FromHexError), + #[error("Missing required field: {0}")] + MissingField(String), + #[error(transparent)] + ParseRemoteTokensError(#[from] ParseRemoteTokensError), + #[error(transparent)] + ERC20Error(#[from] rain_orderbook_common::erc20::Error), } impl OrderbookYamlError { @@ -126,6 +176,12 @@ impl OrderbookYamlError { format!("There was an error processing the YAML configuration. Please check the YAML file for any issues. Error: \"{}\"", err), OrderbookYamlError::FromHexError(err) => format!("The provided address is invalid. Please ensure the address is in the correct hexadecimal format. Error: \"{}\"", err), + OrderbookYamlError::MissingField(field) => + format!("A required field is missing from the token configuration: \"{}\". Please ensure all tokens have decimals, label, and symbol defined.", field), + OrderbookYamlError::ParseRemoteTokensError(err) => + format!("Failed to fetch or parse remote tokens. Please check the using-tokens-from URLs are accessible and return valid token data. Error: \"{}\"", err), + OrderbookYamlError::ERC20Error(err) => + format!("Failed to fetch token information from the blockchain. Please check your network connection and RPC settings. Error: \"{}\"", err), } } } @@ -277,4 +333,365 @@ mod tests { } } } + + #[wasm_bindgen_test] + async fn test_get_tokens_local_only() { + let mut orderbook_yaml = OrderbookYaml::new(vec![get_yaml()], None).unwrap(); + let tokens = orderbook_yaml.get_tokens().await.unwrap(); + + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].key, "token1"); + assert_eq!(tokens[0].chain_id, 1); + assert_eq!(tokens[0].decimals, 18); + assert_eq!(tokens[0].symbol, "WETH"); + assert_eq!(tokens[0].name, "Wrapped Ether"); + assert_eq!( + tokens[0].address, + Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap() + ); + } + + pub fn get_yaml_multiple_networks() -> String { + format!( + r#" + version: {spec_version} + networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + label: Ethereum Mainnet + network-id: 1 + currency: ETH + polygon: + rpcs: + - https://polygon-rpc.com + chain-id: 137 + label: Polygon + network-id: 137 + currency: MATIC + tokens: + weth: + network: mainnet + address: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + decimals: 18 + label: Wrapped Ether + symbol: WETH + usdc-polygon: + network: polygon + address: 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 + decimals: 6 + label: USD Coin (PoS) + symbol: USDC + "#, + spec_version = SpecVersion::current() + ) + } + + #[wasm_bindgen_test] + async fn test_get_tokens_multiple_networks() { + let mut orderbook_yaml = + OrderbookYaml::new(vec![get_yaml_multiple_networks()], None).unwrap(); + let tokens = orderbook_yaml.get_tokens().await.unwrap(); + + assert_eq!(tokens.len(), 2); + + let mainnet_token = tokens.iter().find(|t| t.chain_id == 1); + assert!(mainnet_token.is_some()); + let mainnet_token = mainnet_token.unwrap(); + assert_eq!(mainnet_token.symbol, "WETH"); + assert_eq!(mainnet_token.decimals, 18); + + let polygon_token = tokens.iter().find(|t| t.chain_id == 137); + assert!(polygon_token.is_some()); + let polygon_token = polygon_token.unwrap(); + assert_eq!(polygon_token.symbol, "USDC"); + assert_eq!(polygon_token.decimals, 6); + } + + pub fn get_yaml_missing_fields() -> String { + format!( + r#" + version: {spec_version} + networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + tokens: + incomplete: + network: mainnet + address: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + "#, + spec_version = SpecVersion::current() + ) + } + + #[wasm_bindgen_test] + async fn test_get_tokens_missing_fields_tries_rpc() { + let mut orderbook_yaml = OrderbookYaml::new(vec![get_yaml_missing_fields()], None).unwrap(); + let result = orderbook_yaml.get_tokens().await; + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, OrderbookYamlError::ERC20Error(_)), + "Expected ERC20Error when trying to fetch missing token info from RPC, got: {:?}", + err + ); + assert!(err + .to_readable_msg() + .contains("Failed to fetch token information")); + } +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod non_wasm_tests { + use super::*; + use httpmock::MockServer; + use rain_orderbook_app_settings::spec_version::SpecVersion; + use serde_json::json; + + #[tokio::test] + async fn test_get_tokens_from_remote() { + let server = MockServer::start_async().await; + + let remote_response = json!({ + "name": "Remote Tokens", + "timestamp": "2021-01-01T00:00:00.000Z", + "version": { "major": 1, "minor": 0, "patch": 0 }, + "tokens": [ + { + "chainId": 1, + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "name": "USD Coin", + "symbol": "USDC", + "decimals": 6 + } + ] + }); + + server.mock(|when, then| { + when.method("GET").path("/tokens.json"); + then.status(200) + .header("content-type", "application/json") + .body(remote_response.to_string()); + }); + + let yaml = format!( + r#" + version: {spec_version} + networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + using-tokens-from: + - {url}/tokens.json + "#, + spec_version = SpecVersion::current(), + url = server.base_url() + ); + + let mut orderbook_yaml = OrderbookYaml::new(vec![yaml], None).unwrap(); + let tokens = orderbook_yaml.get_tokens().await.unwrap(); + + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].symbol, "USDC"); + assert_eq!(tokens[0].chain_id, 1); + assert_eq!(tokens[0].decimals, 6); + assert_eq!(tokens[0].name, "USD Coin"); + } + + #[tokio::test] + async fn test_get_tokens_mixed_local_and_remote() { + let server = MockServer::start_async().await; + + let remote_response = json!({ + "name": "Remote", + "timestamp": "2021-01-01T00:00:00.000Z", + "version": { "major": 1, "minor": 0, "patch": 0 }, + "tokens": [ + { + "chainId": 1, + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "name": "USD Coin", + "symbol": "USDC", + "decimals": 6 + } + ] + }); + + server.mock(|when, then| { + when.method("GET").path("/tokens.json"); + then.status(200) + .header("content-type", "application/json") + .body(remote_response.to_string()); + }); + + let yaml = format!( + r#" + version: {spec_version} + networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + using-tokens-from: + - {url}/tokens.json + tokens: + weth: + network: mainnet + address: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + decimals: 18 + label: Wrapped Ether + symbol: WETH + "#, + spec_version = SpecVersion::current(), + url = server.base_url() + ); + + let mut orderbook_yaml = OrderbookYaml::new(vec![yaml], None).unwrap(); + let tokens = orderbook_yaml.get_tokens().await.unwrap(); + + assert_eq!(tokens.len(), 2); + assert!(tokens.iter().any(|t| t.symbol == "WETH")); + assert!(tokens.iter().any(|t| t.symbol == "USDC")); + } + + #[tokio::test] + async fn test_get_tokens_multiple_networks_remote() { + let server = MockServer::start_async().await; + + let remote_response = json!({ + "name": "Multi-chain", + "timestamp": "2021-01-01T00:00:00.000Z", + "version": { "major": 1, "minor": 0, "patch": 0 }, + "tokens": [ + { "chainId": 1, "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "name": "USDC", "symbol": "USDC", "decimals": 6 }, + { "chainId": 137, "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", "name": "USDC PoS", "symbol": "USDC", "decimals": 6 } + ] + }); + + server.mock(|when, then| { + when.method("GET").path("/tokens.json"); + then.status(200) + .header("content-type", "application/json") + .body(remote_response.to_string()); + }); + + let yaml = format!( + r#" + version: {spec_version} + networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + polygon: + rpcs: + - https://polygon-rpc.com + chain-id: 137 + using-tokens-from: + - {url}/tokens.json + "#, + spec_version = SpecVersion::current(), + url = server.base_url() + ); + + let mut orderbook_yaml = OrderbookYaml::new(vec![yaml], None).unwrap(); + let tokens = orderbook_yaml.get_tokens().await.unwrap(); + + assert_eq!(tokens.len(), 2); + assert!(tokens.iter().any(|t| t.chain_id == 1)); + assert!(tokens.iter().any(|t| t.chain_id == 137)); + } + + #[tokio::test] + async fn test_get_tokens_remote_fetch_failure() { + let server = MockServer::start_async().await; + + server.mock(|when, then| { + when.method("GET").path("/tokens.json"); + then.status(500).body("Internal Server Error"); + }); + + let yaml = format!( + r#" + version: {spec_version} + networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + using-tokens-from: + - {url}/tokens.json + "#, + spec_version = SpecVersion::current(), + url = server.base_url() + ); + + let mut orderbook_yaml = OrderbookYaml::new(vec![yaml], None).unwrap(); + let result = orderbook_yaml.get_tokens().await; + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_get_tokens_multiple_remote_urls() { + let server = MockServer::start_async().await; + + let response1 = json!({ + "name": "Source 1", + "timestamp": "2021-01-01T00:00:00.000Z", + "version": { "major": 1, "minor": 0, "patch": 0 }, + "tokens": [ + { "chainId": 1, "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "name": "USDC", "symbol": "USDC", "decimals": 6 } + ] + }); + let response2 = json!({ + "name": "Source 2", + "timestamp": "2021-01-01T00:00:00.000Z", + "version": { "major": 1, "minor": 0, "patch": 0 }, + "tokens": [ + { "chainId": 1, "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "name": "USDT", "symbol": "USDT", "decimals": 6 } + ] + }); + + server.mock(|when, then| { + when.method("GET").path("/tokens1.json"); + then.status(200) + .header("content-type", "application/json") + .body(response1.to_string()); + }); + server.mock(|when, then| { + when.method("GET").path("/tokens2.json"); + then.status(200) + .header("content-type", "application/json") + .body(response2.to_string()); + }); + + let yaml = format!( + r#" + version: {spec_version} + networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + using-tokens-from: + - {url}/tokens1.json + - {url}/tokens2.json + "#, + spec_version = SpecVersion::current(), + url = server.base_url() + ); + + let mut orderbook_yaml = OrderbookYaml::new(vec![yaml], None).unwrap(); + let tokens = orderbook_yaml.get_tokens().await.unwrap(); + + assert_eq!(tokens.len(), 2); + assert!(tokens.iter().any(|t| t.symbol == "USDC")); + assert!(tokens.iter().any(|t| t.symbol == "USDT")); + } } diff --git a/packages/orderbook/ARCHITECTURE.md b/packages/orderbook/ARCHITECTURE.md index 2ac9df330e..9ba702c6d0 100644 --- a/packages/orderbook/ARCHITECTURE.md +++ b/packages/orderbook/ARCHITECTURE.md @@ -83,8 +83,8 @@ The package re‑exports the WASM‑bound API from the Rust crates. Representati - High‑level classes (selected) - `RaindexClient` — orderbook queries (orders, trades, vaults, quotes, transactions) across configured networks/subgraphs. - `RaindexOrder`, `RaindexVault`, `RaindexTrade`, `RaindexTransaction`, `RaindexVaultsList`, etc. - - `DotrainOrder`, `DotrainOrderGui`, `DotrainRegistry` — dotrain parsing, GUI orchestration, registry fetching, and deployment calldata. - - `OrderbookYaml` — typed access to networks, tokens, orderbooks, subgraphs, deployers, accounts, metaboards. + - `DotrainOrder`, `DotrainOrderGui`, `DotrainRegistry` — dotrain parsing, GUI orchestration, registry fetching (including `getOrderbookYaml()` for token queries), and deployment calldata. + - `OrderbookYaml` — typed access to networks, tokens (via `getTokens()`), orderbooks, subgraphs, deployers, accounts, metaboards. - `Float` — arbitrary‑precision float utilities used across the API. - Errors & results - Most methods return `WasmEncodedResult` with either `{ value }` or `{ error: { msg, readableMsg } }` for ergonomic, user‑readable error handling in JS. diff --git a/packages/orderbook/README.md b/packages/orderbook/README.md index bf2b1002dd..2af48a1127 100644 --- a/packages/orderbook/README.md +++ b/packages/orderbook/README.md @@ -409,7 +409,21 @@ fixed-limit https://example.com/orders/fixed-limit.rain dca https://example.com/orders/dca.rain ``` -The SDK merges the shared settings YAML with each order’s `.rain` content before you ever build a GUI. +The SDK merges the shared settings YAML with each order's `.rain` content before you ever build a GUI. + +#### Access tokens from registry settings + +Use `getOrderbookYaml()` to access the shared settings as an `OrderbookYaml` instance, then query tokens, networks, or orderbooks: + +```ts +const orderbookYamlResult = registry.getOrderbookYaml(); +if (orderbookYamlResult.error) throw new Error(orderbookYamlResult.error.readableMsg); +const orderbookYaml = orderbookYamlResult.value; + +const tokensResult = await orderbookYaml.getTokens(); +if (tokensResult.error) throw new Error(tokensResult.error.readableMsg); +const tokens = tokensResult.value; // TokenInfo[] with chain_id, address, decimals, symbol, name +``` ### Build a deployment GUI @@ -579,6 +593,7 @@ if (!postTaskResult.error) console.log(postTaskResult.value); - `getOrderHash`, `keccak256`, `keccak256HexString` – deterministic hashing helpers for Rain orders or arbitrary payloads. - `Float` – arbitrary-precision arithmetic with parsing, formatting, comparisons, math ops, fixed-decimal conversions, and helpers like `Float.zero()` or `.formatWithRange(...)`. +- `OrderbookYaml.getTokens()` – async method returning all tokens from YAML configuration with `chain_id`, `address`, `decimals`, `symbol`, and `name`. Automatically fetches remote tokens from `using-tokens-from` URLs. - `RaindexClient.getAllAccounts()` / `getAllVaultTokens()` – introspect accounts and ERC20 metadata defined in your YAML or discovered via subgraphs. - `clearTables`, `getSyncStatus`, `RaindexClient.syncLocalDatabase`, `RaindexClient.setDbCallback` – plug in a persistent cache for offline apps. - `RaindexVaultsList.getWithdrawCalldata()` – multicall builder that withdraws every vault with a balance. diff --git a/packages/orderbook/test/js_api/dotrainRegistry.test.ts b/packages/orderbook/test/js_api/dotrainRegistry.test.ts index 8ecd64c52d..8e47472b55 100644 --- a/packages/orderbook/test/js_api/dotrainRegistry.test.ts +++ b/packages/orderbook/test/js_api/dotrainRegistry.test.ts @@ -340,4 +340,96 @@ fixed-limit http://localhost:8231/fixed-limit.rain`; assert(result.error.readableMsg.includes("order key 'non-existent' was not found")); }); }); + + describe('DotrainRegistry getOrderbookYaml', () => { + const MOCK_SETTINGS_WITH_TOKENS = ` +version: 4 +networks: + mainnet: + rpcs: + - https://mainnet.infura.io + chain-id: 1 + currency: ETH +tokens: + weth: + network: mainnet + address: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + decimals: 18 + label: Wrapped Ether + symbol: WETH + usdc: + network: mainnet + address: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + decimals: 6 + label: USD Coin + symbol: USDC +orderbooks: + mainnet: + address: 0x1234567890123456789012345678901234567890 + network: mainnet +deployers: + mainnet: + address: 0x1234567890123456789012345678901234567890 + network: mainnet +`; + + const MOCK_DOTRAIN_SIMPLE = ` +gui: + name: Test Order + description: Test description + deployments: + mainnet: + name: Mainnet Order + description: Mainnet deployment + deposits: + - token: weth + presets: + - "0" + fields: + - binding: test-binding + name: Test binding + presets: + - value: "0xbeef" +scenarios: + mainnet: + deployer: mainnet + runs: 1 +orders: + mainnet: + orderbook: mainnet + inputs: + - token: weth + outputs: + - token: usdc +deployments: + mainnet: + scenario: mainnet + order: mainnet +--- +#calculate-io +_ _: 0 0; +#handle-io +:; +`; + + it('should return OrderbookYaml instance from settings', async () => { + const registryContent = `http://localhost:8231/settings.yaml +test-order http://localhost:8231/order.rain`; + + await mockServer.forGet('/registry.txt').thenReply(200, registryContent); + await mockServer.forGet('/settings.yaml').thenReply(200, MOCK_SETTINGS_WITH_TOKENS); + await mockServer.forGet('/order.rain').thenReply(200, MOCK_DOTRAIN_SIMPLE); + + const registry = extractWasmEncodedData( + await DotrainRegistry.new('http://localhost:8231/registry.txt') + ); + + const orderbookYamlResult = registry.getOrderbookYaml(); + const orderbookYaml = extractWasmEncodedData(orderbookYamlResult); + + assert.ok(orderbookYaml, 'OrderbookYaml instance should be returned'); + assert.strictEqual(typeof orderbookYaml.getTokens, 'function'); + assert.strictEqual(typeof orderbookYaml.getOrderbookByAddress, 'function'); + }); + }); }); diff --git a/packages/orderbook/test/js_api/orderbookYaml.test.ts b/packages/orderbook/test/js_api/orderbookYaml.test.ts index e473fd6f99..6fd7be7704 100644 --- a/packages/orderbook/test/js_api/orderbookYaml.test.ts +++ b/packages/orderbook/test/js_api/orderbookYaml.test.ts @@ -1,6 +1,7 @@ import assert from 'assert'; -import { describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { OrderbookYaml, OrderbookCfg, WasmEncodedResult } from '../../dist/cjs'; +import { getLocal } from 'mockttp'; const YAML_WITHOUT_ORDERBOOK = ` networks: @@ -188,4 +189,348 @@ orderbooks: expect(orderbookResult.error.msg).toContain('Orderbook yaml error'); }); }); + + describe('getTokens tests', async function () { + const mockServer = getLocal(); + + beforeAll(async () => { + await mockServer.start(8087); + }); + + afterAll(async () => { + await mockServer.stop(); + }); + + beforeEach(async () => { + await mockServer.reset(); + }); + + it('should return local tokens with chainId', async function () { + const orderbookYaml = buildYaml(YAML_WITHOUT_ORDERBOOK); + const tokensResult = await orderbookYaml.getTokens(); + const tokens = extractWasmEncodedData(tokensResult); + + assert.strictEqual(tokens.length, 2); + + const token1 = tokens.find((t: { key: string }) => t.key === 'token1'); + assert.ok(token1, 'token1 should exist'); + assert.strictEqual(token1.chainId, 123); + assert.strictEqual(token1.decimals, 6); + assert.strictEqual(token1.symbol, 'T1'); + assert.strictEqual(token1.name, 'Token 1'); + assert.strictEqual( + token1.address.toLowerCase(), + '0xc2132d05d31c914a87c6611c10748aeb04b58e8f' + ); + + const token2 = tokens.find((t: { key: string }) => t.key === 'token2'); + assert.ok(token2, 'token2 should exist'); + assert.strictEqual(token2.chainId, 123); + assert.strictEqual(token2.decimals, 18); + assert.strictEqual(token2.symbol, 'T2'); + assert.strictEqual(token2.name, 'Token 2'); + }); + + it('should try to fetch missing token fields from RPC and return error on failure', async function () { + const YAML_MISSING_FIELDS = ` +networks: + some-network: + rpcs: + - http://localhost:8087/rpc-url + chain-id: 123 +tokens: + incomplete: + network: some-network + address: 0xc2132d05d31c914a87c6611c10748aeb04b58e8f +`; + // No mock set up - RPC call will fail + // This tests that when token fields are missing, the code attempts to fetch from RPC + // (rather than immediately returning a "missing field" error) + const orderbookYaml = buildYaml(YAML_MISSING_FIELDS); + const tokensResult = await orderbookYaml.getTokens(); + + assert.ok(tokensResult.error, 'Expected error when RPC fetch fails'); + expect(tokensResult.error.readableMsg).toContain('Failed to fetch token information'); + }); + + it('should return tokens with correct chainId for multiple networks', async function () { + const MULTI_NETWORK_YAML = ` +networks: + mainnet: + rpcs: + - http://localhost:8085/rpc-url + chain-id: 1 + polygon: + rpcs: + - http://localhost:8086/rpc-url + chain-id: 137 +tokens: + weth: + network: mainnet + address: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + decimals: 18 + label: Wrapped Ether + symbol: WETH + usdc-polygon: + network: polygon + address: 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 + decimals: 6 + label: USD Coin PoS + symbol: USDC +`; + const orderbookYaml = buildYaml(MULTI_NETWORK_YAML); + const tokensResult = await orderbookYaml.getTokens(); + const tokens = extractWasmEncodedData(tokensResult); + + assert.strictEqual(tokens.length, 2); + + const mainnetToken = tokens.find((t: { chainId: number }) => t.chainId === 1); + assert.ok(mainnetToken, 'mainnet token should exist'); + assert.strictEqual(mainnetToken.symbol, 'WETH'); + assert.strictEqual(mainnetToken.decimals, 18); + + const polygonToken = tokens.find((t: { chainId: number }) => t.chainId === 137); + assert.ok(polygonToken, 'polygon token should exist'); + assert.strictEqual(polygonToken.symbol, 'USDC'); + assert.strictEqual(polygonToken.decimals, 6); + }); + }); + + describe('getTokens with using-tokens-from tests', async function () { + const mockServer = getLocal(); + + beforeAll(async () => { + await mockServer.start(8232); + }); + + afterAll(async () => { + await mockServer.stop(); + }); + + beforeEach(async () => { + await mockServer.reset(); + }); + + it('should fetch and return remote tokens from using-tokens-from', async function () { + const remoteTokensResponse = { + name: 'Remote Tokens', + timestamp: '2021-01-01T00:00:00.000Z', + keywords: [], + version: { major: 1, minor: 0, patch: 0 }, + tokens: [ + { + chainId: 1, + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6 + }, + { + chainId: 1, + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + name: 'Tether USD', + symbol: 'USDT', + decimals: 6 + } + ], + logoURI: 'http://localhost.com' + }; + + await mockServer.forGet('/tokens.json').thenJson(200, remoteTokensResponse); + + const yaml = ` +networks: + mainnet: + rpcs: + - http://localhost:8085/rpc-url + chain-id: 1 +using-tokens-from: + - http://localhost:8232/tokens.json +`; + const orderbookYaml = buildYaml(yaml); + const tokensResult = await orderbookYaml.getTokens(); + const tokens = extractWasmEncodedData(tokensResult); + + assert.strictEqual(tokens.length, 2); + + const usdc = tokens.find((t: { symbol: string }) => t.symbol === 'USDC'); + assert.ok(usdc, 'USDC should exist'); + assert.strictEqual(usdc.decimals, 6); + assert.strictEqual(usdc.chainId, 1); + assert.strictEqual(usdc.name, 'USD Coin'); + + const usdt = tokens.find((t: { symbol: string }) => t.symbol === 'USDT'); + assert.ok(usdt, 'USDT should exist'); + assert.strictEqual(usdt.decimals, 6); + assert.strictEqual(usdt.chainId, 1); + }); + + it('should return both local and remote tokens', async function () { + const remoteTokensResponse = { + name: 'Remote', + timestamp: '2021-01-01T00:00:00.000Z', + keywords: [], + version: { major: 1, minor: 0, patch: 0 }, + tokens: [ + { + chainId: 1, + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6 + } + ], + logoURI: 'http://localhost.com' + }; + + await mockServer.forGet('/tokens.json').thenJson(200, remoteTokensResponse); + + const yaml = ` +networks: + mainnet: + rpcs: + - http://localhost:8085/rpc-url + chain-id: 1 +using-tokens-from: + - http://localhost:8232/tokens.json +tokens: + weth: + network: mainnet + address: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + decimals: 18 + label: Wrapped Ether + symbol: WETH +`; + const orderbookYaml = buildYaml(yaml); + const tokensResult = await orderbookYaml.getTokens(); + const tokens = extractWasmEncodedData(tokensResult); + + assert.strictEqual(tokens.length, 2); + assert.ok( + tokens.find((t: { symbol: string }) => t.symbol === 'WETH'), + 'WETH should exist' + ); + assert.ok( + tokens.find((t: { symbol: string }) => t.symbol === 'USDC'), + 'USDC should exist' + ); + }); + + it('should fetch tokens from multiple using-tokens-from URLs', async function () { + const response1 = { + name: 'Source 1', + timestamp: '2021-01-01T00:00:00.000Z', + keywords: [], + version: { major: 1, minor: 0, patch: 0 }, + tokens: [ + { + chainId: 1, + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + name: 'USDC', + symbol: 'USDC', + decimals: 6 + } + ], + logoURI: 'http://localhost.com' + }; + const response2 = { + name: 'Source 2', + timestamp: '2021-01-01T00:00:00.000Z', + keywords: [], + version: { major: 1, minor: 0, patch: 0 }, + tokens: [ + { + chainId: 1, + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + name: 'USDT', + symbol: 'USDT', + decimals: 6 + } + ], + logoURI: 'http://localhost.com' + }; + + await mockServer.forGet('/tokens1.json').thenJson(200, response1); + await mockServer.forGet('/tokens2.json').thenJson(200, response2); + + const yaml = ` +networks: + mainnet: + rpcs: + - http://localhost:8085/rpc-url + chain-id: 1 +using-tokens-from: + - http://localhost:8232/tokens1.json + - http://localhost:8232/tokens2.json +`; + const orderbookYaml = buildYaml(yaml); + const tokensResult = await orderbookYaml.getTokens(); + const tokens = extractWasmEncodedData(tokensResult); + + assert.strictEqual(tokens.length, 2); + assert.ok( + tokens.find((t: { symbol: string }) => t.symbol === 'USDC'), + 'USDC should exist' + ); + assert.ok( + tokens.find((t: { symbol: string }) => t.symbol === 'USDT'), + 'USDT should exist' + ); + }); + + it('should return tokens with correct chainId from multiple networks', async function () { + const remoteTokensResponse = { + name: 'Multi-chain Tokens', + timestamp: '2021-01-01T00:00:00.000Z', + keywords: [], + version: { major: 1, minor: 0, patch: 0 }, + tokens: [ + { + chainId: 1, + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + name: 'USD Coin', + symbol: 'USDC', + decimals: 6 + }, + { + chainId: 137, + address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + name: 'USD Coin (PoS)', + symbol: 'USDC.e', + decimals: 6 + } + ], + logoURI: 'http://localhost.com' + }; + + await mockServer.forGet('/tokens.json').thenJson(200, remoteTokensResponse); + + const yaml = ` +networks: + mainnet: + rpcs: + - http://localhost:8085/rpc-url + chain-id: 1 + polygon: + rpcs: + - http://localhost:8086/rpc-url + chain-id: 137 +using-tokens-from: + - http://localhost:8232/tokens.json +`; + const orderbookYaml = buildYaml(yaml); + const tokensResult = await orderbookYaml.getTokens(); + const tokens = extractWasmEncodedData(tokensResult); + + assert.strictEqual(tokens.length, 2); + + const mainnetUsdc = tokens.find((t: { chainId: number }) => t.chainId === 1); + assert.ok(mainnetUsdc, 'mainnet token should exist'); + assert.strictEqual(mainnetUsdc.chainId, 1); + + const polygonUsdc = tokens.find((t: { chainId: number }) => t.chainId === 137); + assert.ok(polygonUsdc, 'polygon token should exist'); + assert.strictEqual(polygonUsdc.chainId, 137); + }); + }); }); diff --git a/packages/ui-components/src/__tests__/TokenSelectionModal.test.ts b/packages/ui-components/src/__tests__/TokenSelectionModal.test.ts index 825d0b5b5d..ee7b51ef42 100644 --- a/packages/ui-components/src/__tests__/TokenSelectionModal.test.ts +++ b/packages/ui-components/src/__tests__/TokenSelectionModal.test.ts @@ -3,18 +3,19 @@ import userEvent from '@testing-library/user-event'; import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import TokenSelectionModal from '../lib/components/deployment/TokenSelectionModal.svelte'; import type { ComponentProps } from 'svelte'; -import type { TokenInfo, DotrainOrderGui } from '@rainlanguage/orderbook'; +import type { ExtendedTokenInfo, DotrainOrderGui } from '@rainlanguage/orderbook'; import { useGui } from '$lib/hooks/useGui'; type TokenSelectionModalProps = ComponentProps; -const mockTokens: TokenInfo[] = [ +const mockTokens: ExtendedTokenInfo[] = [ { key: 'token1', address: '0x1234567890123456789012345678901234567890', name: 'Test Token 1', symbol: 'TEST1', decimals: 18, + chainId: 1, logoUri: 'https://example.com/token1-logo.png' }, { @@ -22,7 +23,9 @@ const mockTokens: TokenInfo[] = [ address: '0x0987654321098765432109876543210987654321', name: 'Another Token', symbol: 'ANOTHER', - decimals: 6 + decimals: 6, + chainId: 1, + logoUri: undefined } ]; diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index 5c86f8fe31..f44002b819 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -4,7 +4,7 @@ import ComposedRainlangModal from './ComposedRainlangModal.svelte'; import { type GuiSelectTokensCfg, - type TokenInfo, + type ExtendedTokenInfo, type GuiDepositCfg, type GuiFieldDefinitionCfg, type NameAndDescriptionCfg, @@ -55,7 +55,7 @@ let allFieldDefinitionsWithDefaults: GuiFieldDefinitionCfg[] = []; let allTokensSelected: boolean = false; let showAdvancedOptions: boolean = false; - let allTokenInfos: TokenInfo[] = []; + let allTokenInfos: ExtendedTokenInfo[] = []; let selectTokens: GuiSelectTokensCfg[] | undefined = undefined; let checkingDeployment: boolean = false; let tokenBalances: Map = new Map(); @@ -125,7 +125,7 @@ await handleShareChoices(gui, registry.getCurrentRegistry()); } - async function fetchTokenBalance(tokenInfo: TokenInfo) { + async function fetchTokenBalance(tokenInfo: ExtendedTokenInfo) { if (!$account) return; const balances = tokenBalances; diff --git a/packages/ui-components/src/lib/components/deployment/SelectToken.svelte b/packages/ui-components/src/lib/components/deployment/SelectToken.svelte index 068f9faabe..cb835b276c 100644 --- a/packages/ui-components/src/lib/components/deployment/SelectToken.svelte +++ b/packages/ui-components/src/lib/components/deployment/SelectToken.svelte @@ -1,6 +1,6 @@