use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssetNetwork {
pub network: String,
pub contract: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssetInfo {
pub symbol: String,
pub networks: Vec<AssetNetwork>,
}
const NETWORK_CODES: [(&str, &str); 5] = [
("erc20", "ethereum"),
("bep20", "bsc"),
("trc20", "tron"),
("avaxc", "avalanche"),
("avax-c", "avalanche"),
];
pub fn chain_for_network(network: &str) -> String {
let n = network.trim().to_ascii_lowercase();
if let Some(chain) = crate::chains::registry::chain(&n) {
return chain.id.to_string();
}
NETWORK_CODES
.iter()
.find(|(code, _)| *code == n)
.map(|(_, chain)| chain.to_string())
.unwrap_or(n)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TokenAddress {
pub symbol: String,
pub network: String,
pub contract: Option<String>,
pub other_networks: usize,
}
const MIN_CONTRACT_LEN: usize = 20;
pub fn contract_of(raw: &str) -> Option<String> {
let raw = raw.trim();
(raw.len() >= MIN_CONTRACT_LEN).then(|| raw.to_string())
}
pub fn token_addresses(base: &str, quote: &str, assets: &[AssetInfo]) -> Vec<TokenAddress> {
let find = |s: &str| assets.iter().find(|a| a.symbol.eq_ignore_ascii_case(s));
let mut lines = Vec::new();
let mut base_networks = Vec::new();
if let Some(b) = find(base) {
for n in &b.networks {
base_networks.push(n.network.clone());
lines.push(TokenAddress {
symbol: b.symbol.clone(),
network: n.network.clone(),
contract: n.contract.clone(),
other_networks: 0,
});
}
}
if let Some(q) = find(quote)
&& let Some(n) = q
.networks
.iter()
.find(|n| base_networks.contains(&n.network))
.or_else(|| q.networks.first())
{
lines.push(TokenAddress {
symbol: q.symbol.clone(),
network: n.network.clone(),
contract: n.contract.clone(),
other_networks: q.networks.len() - 1,
});
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
fn asset(symbol: &str, networks: &[(&str, &str)]) -> AssetInfo {
AssetInfo {
symbol: symbol.to_string(),
networks: networks
.iter()
.map(|(n, c)| AssetNetwork {
network: n.to_string(),
contract: contract_of(c),
})
.collect(),
}
}
const USDN: &str = "0xeC747DAf41A4Ca4ebE72356592F8301924132907";
const USDT_ETH: &str = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
#[test]
fn test_the_quote_shows_the_network_it_shares_with_the_base() {
let assets = [
asset("USDN", &[("ETH", USDN)]),
asset(
"USDT",
&[
("TRX", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
("ETH", USDT_ETH),
("BSC", "0x55d398326f99059fF775485246999027B3197955"),
],
),
];
let lines = token_addresses("usdn", "USDT", &assets);
assert_eq!(lines.len(), 2);
assert_eq!(
(lines[0].symbol.as_str(), lines[0].network.as_str()),
("USDN", "ETH")
);
assert_eq!(lines[0].contract.as_deref(), Some(USDN));
assert_eq!(lines[1].network, "ETH");
assert_eq!(lines[1].contract.as_deref(), Some(USDT_ETH));
assert_eq!(lines[1].other_networks, 2);
}
#[test]
fn test_venue_network_codes_map_to_scope_chains() {
for (code, chain) in [
("ETH", "ethereum"),
("ERC20", "ethereum"),
("ethereum", "ethereum"),
("BSC", "bsc"),
("BEP20", "bsc"),
("TRX", "tron"),
("TRC20", "tron"),
("SOL", "solana"),
("AVAXC", "avalanche"),
("AVAX-C", "avalanche"),
] {
assert_eq!(chain_for_network(code), chain, "{code}");
}
assert_eq!(chain_for_network("SOMECHAIN"), "somechain");
}
#[test]
fn test_unlisted_symbols_and_native_coins() {
let assets = [
asset("BTC", &[("BTC", "btc")]),
asset("USDT", &[("TRX", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")]),
];
let lines = token_addresses("BTC", "USDT", &assets);
assert_eq!(lines[0].contract, None);
assert_eq!(lines[1].network, "TRX");
assert!(token_addresses("NOPE", "ALSO", &assets).is_empty());
}
}