diff --git a/crates/common/src/add_order.rs b/crates/common/src/add_order.rs index 050ea2c5a..ca0269972 100644 --- a/crates/common/src/add_order.rs +++ b/crates/common/src/add_order.rs @@ -452,6 +452,8 @@ price: 2e18; deployer: deployer_arc.clone(), }; let token1 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), @@ -459,6 +461,8 @@ price: 2e18; symbol: Some("Token1".to_string()), }; let token2 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), @@ -466,6 +470,8 @@ price: 2e18; symbol: Some("Token2".to_string()), }; let token3 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), @@ -551,6 +557,8 @@ _ _: 0 0; deployer: deployer_arc.clone(), }; let token1 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), @@ -558,6 +566,8 @@ _ _: 0 0; symbol: Some("Token1".to_string()), }; let token2 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), @@ -565,6 +575,8 @@ _ _: 0 0; symbol: Some("Token2".to_string()), }; let token3 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), @@ -684,6 +696,8 @@ _ _: 0 0; deployer: deployer_arc.clone(), }; let token1 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), @@ -691,6 +705,8 @@ _ _: 0 0; symbol: Some("Token1".to_string()), }; let token2 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), @@ -698,6 +714,8 @@ _ _: 0 0; symbol: Some("Token2".to_string()), }; let token3 = Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), address: Address::default(), network: network_arc.clone(), decimals: Some(18), diff --git a/crates/settings/src/test.rs b/crates/settings/src/test.rs index f0201b3ab..b4115c13f 100644 --- a/crates/settings/src/test.rs +++ b/crates/settings/src/test.rs @@ -38,6 +38,8 @@ pub fn mock_orderbook() -> Arc { // Helper function to create a mock token pub fn mock_token(name: &str) -> Arc { Arc::new(Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), label: Some(name.into()), address: Address::repeat_byte(0x05), symbol: Some("TKN".into()), diff --git a/crates/settings/src/token.rs b/crates/settings/src/token.rs index d59139df5..c01d17918 100644 --- a/crates/settings/src/token.rs +++ b/crates/settings/src/token.rs @@ -1,7 +1,11 @@ +use crate::yaml::{optional_string, require_hash, require_string, YamlError, YamlParsableHash}; use crate::*; use alloy::primitives::{hex::FromHexError, Address}; use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::sync::RwLock; use std::{collections::HashMap, sync::Arc}; +use strict_yaml_rust::StrictYaml; use thiserror::Error; use typeshare::typeshare; @@ -9,10 +13,14 @@ use typeshare::typeshare; use rain_orderbook_bindings::{impl_all_wasm_traits, wasm_traits::prelude::*}; #[typeshare] -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] #[cfg_attr(target_family = "wasm", derive(Tsify))] +#[serde(default)] pub struct Token { + #[serde(skip)] + pub document: Arc>, + pub key: String, #[typeshare(typescript(type = "Network"))] pub network: Arc, #[typeshare(typescript(type = "string"))] @@ -22,16 +30,139 @@ pub struct Token { pub label: Option, pub symbol: Option, } +impl Token { + pub fn validate_address(value: &str) -> Result { + Address::from_str(value).map_err(ParseTokenConfigSourceError::AddressParseError) + } + pub fn validate_decimals(value: &str) -> Result { + value + .parse::() + .map_err(ParseTokenConfigSourceError::DecimalsParseError) + } + + pub fn update_address(&mut self, address: &str) -> Result { + let mut document = self + .document + .write() + .map_err(|_| YamlError::WriteLockError)?; + + if let StrictYaml::Hash(ref mut document_hash) = *document { + if let Some(StrictYaml::Hash(ref mut tokens)) = + document_hash.get_mut(&StrictYaml::String("tokens".to_string())) + { + if let Some(StrictYaml::Hash(ref mut token)) = + tokens.get_mut(&StrictYaml::String(self.key.to_string())) + { + token[&StrictYaml::String("address".to_string())] = + StrictYaml::String(address.to_string()); + self.address = Token::validate_address(address)?; + } else { + return Err(YamlError::ParseError(format!( + "missing field: {} in tokens", + self.key + ))); + } + } else { + return Err(YamlError::ParseError("missing field: tokens".to_string())); + } + } else { + return Err(YamlError::ParseError("document parse error".to_string())); + } + + Ok(self.clone()) + } +} +impl YamlParsableHash for Token { + fn parse_all_from_yaml( + document: Arc>, + ) -> Result, YamlError> { + let document_read = document.read().map_err(|_| YamlError::ReadLockError)?; + let tokens_hash = require_hash( + &document_read, + Some("tokens"), + Some("missing field: tokens".to_string()), + )?; + + tokens_hash + .into_iter() + .map(|(key_yaml, token_yaml)| { + let token_key = key_yaml.as_str().unwrap_or_default().to_string(); + + let network = Network::parse_from_yaml( + document.clone(), + require_string( + token_yaml, + Some("network"), + Some(format!("network string missing in token: {token_key}")), + )?, + ) + .map_err(|_| { + ParseTokenConfigSourceError::NetworkNotFoundError(token_key.clone()) + })?; + + let address = Token::validate_address(&require_string( + token_yaml, + Some("address"), + Some(format!("address string missing in token: {token_key}")), + )?)?; + + let decimals = optional_string(token_yaml, "decimals") + .map(|d| Token::validate_decimals(&d)) + .transpose()?; + + let label = optional_string(token_yaml, "label"); + let symbol = optional_string(token_yaml, "symbol"); + + let token = Token { + document: document.clone(), + key: token_key.clone(), + network: Arc::new(network), + address, + decimals, + label, + symbol, + }; + + Ok((token_key, token)) + }) + .collect() + } +} + #[cfg(target_family = "wasm")] impl_all_wasm_traits!(Token); +impl Default for Token { + fn default() -> Self { + Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: "".to_string(), + network: Arc::new(Network::dummy()), + address: Address::ZERO, + decimals: None, + label: None, + symbol: None, + } + } +} +impl PartialEq for Token { + fn eq(&self, other: &Self) -> bool { + self.key == other.key + && self.network == other.network + && self.address == other.address + && self.decimals == other.decimals + && self.label == other.label + && self.symbol == other.symbol + } +} + #[derive(Error, Debug, PartialEq)] pub enum ParseTokenConfigSourceError { #[error("Failed to parse address")] AddressParseError(FromHexError), #[error("Failed to parse decimals")] DecimalsParseError(std::num::ParseIntError), - #[error("Network not found for Token: {0}")] + #[error("Network not found for token: {0}")] NetworkNotFoundError(String), } @@ -48,6 +179,8 @@ impl TokenConfigSource { .map(Arc::clone)?; Ok(Token { + document: Arc::new(RwLock::new(StrictYaml::String("".to_string()))), + key: self.network.clone(), network: network_ref, address: self.address, decimals: self.decimals, @@ -58,10 +191,11 @@ impl TokenConfigSource { } #[cfg(test)] -mod token_tests { +mod tests { use self::test::*; use super::*; use alloy::primitives::Address; + use strict_yaml_rust::StrictYamlLoader; fn setup_networks() -> HashMap> { let network = mock_network(); @@ -70,6 +204,11 @@ mod token_tests { networks } + fn get_document(yaml: &str) -> Arc> { + let document = StrictYamlLoader::load_from_str(yaml).unwrap()[0].clone(); + Arc::new(RwLock::new(document)) + } + #[test] fn test_token_creation_success_with_all_fields() { let networks = setup_networks(); @@ -86,6 +225,7 @@ mod token_tests { assert!(token.is_ok()); let token = token.unwrap(); + assert_eq!(token.key, "TestNetwork"); assert_eq!( Arc::as_ptr(&token.network), Arc::as_ptr(networks.get("TestNetwork").unwrap()) @@ -112,6 +252,7 @@ mod token_tests { assert!(token.is_ok()); let token = token.unwrap(); + assert_eq!(token.key, "TestNetwork"); assert_eq!( Arc::as_ptr(&token.network), Arc::as_ptr(networks.get("TestNetwork").unwrap()) @@ -141,4 +282,101 @@ mod token_tests { ParseTokenConfigSourceError::NetworkNotFoundError("InvalidNetwork".to_string()) ); } + + #[test] + fn test_parse_tokens_errors() { + let error = Token::parse_all_from_yaml(get_document( + r#" +test: test +"#, + )) + .unwrap_err(); + assert_eq!( + error, + YamlError::ParseError("missing field: tokens".to_string()) + ); + + let error = Token::parse_all_from_yaml(get_document( + r#" +networks: + mainnet: + rpc: "https://mainnet.infura.io" + chain-id: "1" +tokens: + token1: + address: "0x1234567890123456789012345678901234567890" +"#, + )) + .unwrap_err(); + assert_eq!( + error, + YamlError::ParseError("network string missing in token: token1".to_string()) + ); + + let error = Token::parse_all_from_yaml(get_document( + r#" +networks: + mainnet: + rpc: "https://mainnet.infura.io" + chain-id: "1" +tokens: + token1: + network: "nonexistent" + address: "0x1234567890123456789012345678901234567890" +"#, + )) + .unwrap_err(); + assert_eq!( + error, + YamlError::ParseTokenConfigSourceError( + ParseTokenConfigSourceError::NetworkNotFoundError("token1".to_string()) + ) + ); + + let error = Token::parse_all_from_yaml(get_document( + r#" +networks: + mainnet: + rpc: "https://mainnet.infura.io" + chain-id: "1" +tokens: + token1: + network: "mainnet" +"#, + )) + .unwrap_err(); + assert_eq!( + error, + YamlError::ParseError("address string missing in token: token1".to_string()) + ); + + let error = Token::parse_all_from_yaml(get_document( + r#" +networks: + mainnet: + rpc: "https://mainnet.infura.io" + chain-id: "1" +tokens: + token1: + network: "mainnet" + address: "not_a_valid_address" +"#, + )); + assert!(error.is_err()); + + let error = Token::parse_all_from_yaml(get_document( + r#" +networks: + mainnet: + rpc: "https://mainnet.infura.io" + chain-id: "1" +tokens: + token1: + network: "mainnet" + address: "0x1234567890123456789012345678901234567890" + decimals: "not_a_number" +"#, + )); + assert!(error.is_err()); + } } diff --git a/crates/settings/src/yaml/mod.rs b/crates/settings/src/yaml/mod.rs index 866fe0ace..cf3c0f760 100644 --- a/crates/settings/src/yaml/mod.rs +++ b/crates/settings/src/yaml/mod.rs @@ -1,6 +1,6 @@ pub mod orderbook; -use crate::ParseNetworkConfigSourceError; +use crate::{ParseNetworkConfigSourceError, ParseTokenConfigSourceError}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::sync::{PoisonError, RwLockReadGuard, RwLockWriteGuard}; @@ -10,10 +10,17 @@ use strict_yaml_rust::{ }; use thiserror::Error; -pub trait YamlParsableHash: Sized { +pub trait YamlParsableHash: Sized + Clone { fn parse_all_from_yaml( document: Arc>, ) -> Result, YamlError>; + + fn parse_from_yaml(document: Arc>, key: String) -> Result { + let all = Self::parse_all_from_yaml(document)?; + all.get(&key) + .ok_or_else(|| YamlError::KeyNotFound(key)) + .cloned() + } } pub trait YamlParsableVector: Sized { @@ -50,6 +57,8 @@ pub enum YamlError { WriteLockError, #[error(transparent)] ParseNetworkConfigSourceError(#[from] ParseNetworkConfigSourceError), + #[error(transparent)] + ParseTokenConfigSourceError(#[from] ParseTokenConfigSourceError), } impl PartialEq for YamlError { fn eq(&self, other: &Self) -> bool { @@ -65,6 +74,7 @@ impl PartialEq for YamlError { (Self::ParseNetworkConfigSourceError(a), Self::ParseNetworkConfigSourceError(b)) => { a == b } + (Self::ParseTokenConfigSourceError(a), Self::ParseTokenConfigSourceError(b)) => a == b, _ => false, } } diff --git a/crates/settings/src/yaml/orderbook.rs b/crates/settings/src/yaml/orderbook.rs index 2531c8430..6518ef981 100644 --- a/crates/settings/src/yaml/orderbook.rs +++ b/crates/settings/src/yaml/orderbook.rs @@ -1,5 +1,5 @@ use super::*; -use crate::Network; +use crate::{Network, Token}; use std::sync::{Arc, RwLock}; use strict_yaml_rust::StrictYamlEmitter; @@ -42,10 +42,25 @@ impl OrderbookYaml { .ok_or(YamlError::KeyNotFound(key.to_string()))?; Ok(network.clone()) } + + pub fn get_token_keys(&self) -> Result, YamlError> { + let tokens = Token::parse_all_from_yaml(self.document.clone())?; + Ok(tokens.keys().cloned().collect()) + } + pub fn get_token(&self, key: &str) -> Result { + let tokens = Token::parse_all_from_yaml(self.document.clone())?; + let token = tokens + .get(key) + .ok_or(YamlError::KeyNotFound(key.to_string()))?; + Ok(token.clone()) + } } #[cfg(test)] mod tests { + use std::str::FromStr; + + use alloy::primitives::Address; use url::Url; use super::*; @@ -73,7 +88,7 @@ mod tests { tokens: token1: network: mainnet - address: 0x2345678901abcdef + address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 decimals: 18 label: Wrapped Ether symbol: WETH @@ -151,4 +166,29 @@ mod tests { Url::parse("https://some-random-rpc-address.com").unwrap() ); } + + #[test] + fn test_update_token_address() { + let ob_yaml = OrderbookYaml::new(FULL_YAML.to_string(), false).unwrap(); + + let mut token = ob_yaml.get_token("token1").unwrap(); + assert_eq!( + token.address, + Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap() + ); + + let token = token + .update_address("0x0000000000000000000000000000000000000001") + .unwrap(); + assert_eq!( + token.address, + Address::from_str("0x0000000000000000000000000000000000000001").unwrap() + ); + + let token = ob_yaml.get_token("token1").unwrap(); + assert_eq!( + token.address, + Address::from_str("0x0000000000000000000000000000000000000001").unwrap() + ); + } } diff --git a/packages/orderbook/test/js_api/gui.test.ts b/packages/orderbook/test/js_api/gui.test.ts index a1e5b28ef..dc0792924 100644 --- a/packages/orderbook/test/js_api/gui.test.ts +++ b/packages/orderbook/test/js_api/gui.test.ts @@ -592,7 +592,7 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () describe('state management tests', async () => { let serializedState = - 'H4sIAAAAAAAA_3WNSwrCQBBEExXRW7gWlO6ezG_nEbzCfHokCCNoFh7fgD0uhNSm-lO8unRfxaigFJVUIGTlNWZiYyFkS0mzA60GjQF4mGNDIEpZgfNOF28iOMor4ewbb6x5rLcT9nKAfifT9ckvng54bJ830kw31nkIMWUuS_s_nLqmtTgCtMKt-PS4c8VfciOu4Ww-Bfyqq_0AAAA='; + 'H4sIAAAAAAAA_3WNSwoCQQxEHRXRW7gWlCT9Sc_OI3iF7k5aBmEEnYXHd2HGhWBtXj5F1XnxUa8spXiXSH3zTZxvABlLDImgoagQZ6yx1OjEE5PLXINycoGpD2lpOTtjGUYZxusROztAt7Xp8tCnTns8zJ8XkvMhcuohlyra_u2_4bSYtTIiwFy4MU73m474da6NAU7xDcVGPeL9AAAA'; let gui: DotrainOrderGui; beforeAll(async () => { mockServer