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
26#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28pub struct TokenAddress {
29 pub symbol: String,
30 pub network: String,
31 pub contract: Option<String>,
32 pub other_networks: usize,
34}
35
36const MIN_CONTRACT_LEN: usize = 20;
39
40pub fn contract_of(raw: &str) -> Option<String> {
42 let raw = raw.trim();
43 (raw.len() >= MIN_CONTRACT_LEN).then(|| raw.to_string())
44}
45
46pub fn token_addresses(base: &str, quote: &str, assets: &[AssetInfo]) -> Vec<TokenAddress> {
51 let find = |s: &str| assets.iter().find(|a| a.symbol.eq_ignore_ascii_case(s));
52 let mut lines = Vec::new();
53 let mut base_networks = Vec::new();
54 if let Some(b) = find(base) {
55 for n in &b.networks {
56 base_networks.push(n.network.clone());
57 lines.push(TokenAddress {
58 symbol: b.symbol.clone(),
59 network: n.network.clone(),
60 contract: n.contract.clone(),
61 other_networks: 0,
62 });
63 }
64 }
65 if let Some(q) = find(quote)
66 && let Some(n) = q
67 .networks
68 .iter()
69 .find(|n| base_networks.contains(&n.network))
70 .or_else(|| q.networks.first())
71 {
72 lines.push(TokenAddress {
73 symbol: q.symbol.clone(),
74 network: n.network.clone(),
75 contract: n.contract.clone(),
76 other_networks: q.networks.len() - 1,
77 });
78 }
79 lines
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 fn asset(symbol: &str, networks: &[(&str, &str)]) -> AssetInfo {
87 AssetInfo {
88 symbol: symbol.to_string(),
89 networks: networks
90 .iter()
91 .map(|(n, c)| AssetNetwork {
92 network: n.to_string(),
93 contract: contract_of(c),
94 })
95 .collect(),
96 }
97 }
98
99 const USDN: &str = "0xeC747DAf41A4Ca4ebE72356592F8301924132907";
100 const USDT_ETH: &str = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
101
102 #[test]
103 fn test_the_quote_shows_the_network_it_shares_with_the_base() {
104 let assets = [
107 asset("USDN", &[("ETH", USDN)]),
108 asset(
109 "USDT",
110 &[
111 ("TRX", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
112 ("ETH", USDT_ETH),
113 ("BSC", "0x55d398326f99059fF775485246999027B3197955"),
114 ],
115 ),
116 ];
117 let lines = token_addresses("usdn", "USDT", &assets);
118 assert_eq!(lines.len(), 2);
119 assert_eq!(
120 (lines[0].symbol.as_str(), lines[0].network.as_str()),
121 ("USDN", "ETH")
122 );
123 assert_eq!(lines[0].contract.as_deref(), Some(USDN));
124 assert_eq!(lines[1].network, "ETH");
125 assert_eq!(lines[1].contract.as_deref(), Some(USDT_ETH));
126 assert_eq!(lines[1].other_networks, 2);
127 }
128
129 #[test]
130 fn test_unlisted_symbols_and_native_coins() {
131 let assets = [
132 asset("BTC", &[("BTC", "btc")]),
133 asset("USDT", &[("TRX", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")]),
134 ];
135 let lines = token_addresses("BTC", "USDT", &assets);
136 assert_eq!(lines[0].contract, None);
138 assert_eq!(lines[1].network, "TRX");
140 assert!(token_addresses("NOPE", "ALSO", &assets).is_empty());
141 }
142}