1use serde::Serialize;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub struct AssetNetwork {
13 pub network: String,
15 pub contract: Option<String>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21pub struct AssetInfo {
22 pub symbol: String,
23 pub networks: Vec<AssetNetwork>,
24}
25
26const NETWORK_CODES: [(&str, &str); 5] = [
30 ("erc20", "ethereum"),
31 ("bep20", "bsc"),
32 ("trc20", "tron"),
33 ("avaxc", "avalanche"),
36 ("avax-c", "avalanche"),
37];
38
39pub fn chain_for_network(network: &str) -> String {
42 let n = network.trim().to_ascii_lowercase();
43 if let Some(chain) = crate::chains::registry::chain(&n) {
44 return chain.id.to_string();
45 }
46 NETWORK_CODES
47 .iter()
48 .find(|(code, _)| *code == n)
49 .map(|(_, chain)| chain.to_string())
50 .unwrap_or(n)
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct TokenAddress {
56 pub symbol: String,
57 pub network: String,
58 pub contract: Option<String>,
59 pub other_networks: usize,
61}
62
63const MIN_CONTRACT_LEN: usize = 20;
66
67pub fn contract_of(raw: &str) -> Option<String> {
69 let raw = raw.trim();
70 (raw.len() >= MIN_CONTRACT_LEN).then(|| raw.to_string())
71}
72
73pub fn token_addresses(base: &str, quote: &str, assets: &[AssetInfo]) -> Vec<TokenAddress> {
78 let find = |s: &str| assets.iter().find(|a| a.symbol.eq_ignore_ascii_case(s));
79 let mut lines = Vec::new();
80 let mut base_networks = Vec::new();
81 if let Some(b) = find(base) {
82 for n in &b.networks {
83 base_networks.push(n.network.clone());
84 lines.push(TokenAddress {
85 symbol: b.symbol.clone(),
86 network: n.network.clone(),
87 contract: n.contract.clone(),
88 other_networks: 0,
89 });
90 }
91 }
92 if let Some(q) = find(quote)
93 && let Some(n) = q
94 .networks
95 .iter()
96 .find(|n| base_networks.contains(&n.network))
97 .or_else(|| q.networks.first())
98 {
99 lines.push(TokenAddress {
100 symbol: q.symbol.clone(),
101 network: n.network.clone(),
102 contract: n.contract.clone(),
103 other_networks: q.networks.len() - 1,
104 });
105 }
106 lines
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn asset(symbol: &str, networks: &[(&str, &str)]) -> AssetInfo {
114 AssetInfo {
115 symbol: symbol.to_string(),
116 networks: networks
117 .iter()
118 .map(|(n, c)| AssetNetwork {
119 network: n.to_string(),
120 contract: contract_of(c),
121 })
122 .collect(),
123 }
124 }
125
126 const USDN: &str = "0xeC747DAf41A4Ca4ebE72356592F8301924132907";
127 const USDT_ETH: &str = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
128
129 #[test]
130 fn test_the_quote_shows_the_network_it_shares_with_the_base() {
131 let assets = [
134 asset("USDN", &[("ETH", USDN)]),
135 asset(
136 "USDT",
137 &[
138 ("TRX", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
139 ("ETH", USDT_ETH),
140 ("BSC", "0x55d398326f99059fF775485246999027B3197955"),
141 ],
142 ),
143 ];
144 let lines = token_addresses("usdn", "USDT", &assets);
145 assert_eq!(lines.len(), 2);
146 assert_eq!(
147 (lines[0].symbol.as_str(), lines[0].network.as_str()),
148 ("USDN", "ETH")
149 );
150 assert_eq!(lines[0].contract.as_deref(), Some(USDN));
151 assert_eq!(lines[1].network, "ETH");
152 assert_eq!(lines[1].contract.as_deref(), Some(USDT_ETH));
153 assert_eq!(lines[1].other_networks, 2);
154 }
155
156 #[test]
157 fn test_venue_network_codes_map_to_scope_chains() {
158 for (code, chain) in [
161 ("ETH", "ethereum"),
162 ("ERC20", "ethereum"),
163 ("ethereum", "ethereum"),
164 ("BSC", "bsc"),
165 ("BEP20", "bsc"),
166 ("TRX", "tron"),
167 ("TRC20", "tron"),
168 ("SOL", "solana"),
169 ("AVAXC", "avalanche"),
170 ("AVAX-C", "avalanche"),
171 ] {
172 assert_eq!(chain_for_network(code), chain, "{code}");
173 }
174 assert_eq!(chain_for_network("SOMECHAIN"), "somechain");
175 }
176
177 #[test]
178 fn test_unlisted_symbols_and_native_coins() {
179 let assets = [
180 asset("BTC", &[("BTC", "btc")]),
181 asset("USDT", &[("TRX", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")]),
182 ];
183 let lines = token_addresses("BTC", "USDT", &assets);
184 assert_eq!(lines[0].contract, None);
186 assert_eq!(lines[1].network, "TRX");
188 assert!(token_addresses("NOPE", "ALSO", &assets).is_empty());
189 }
190}