Skip to main content

scope/market/
assets.rs

1//! Which token a venue's symbol is: the asset's networks and contracts,
2//! from the venue's coin list (the `assets` capability).
3//!
4//! A venue symbol such as `USDN` names one asset on that venue, but not
5//! which token it is. The report shows the contract, so a reader can check
6//! it against the issuer's.
7
8use serde::Serialize;
9
10/// One network of an asset and the token contract on it.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub struct AssetNetwork {
13    /// The venue's network name (e.g. `ETH`).
14    pub network: String,
15    /// The token contract, or `None` for the network's native coin.
16    pub contract: Option<String>,
17}
18
19/// An asset in the venue's coin list.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21pub struct AssetInfo {
22    pub symbol: String,
23    pub networks: Vec<AssetNetwork>,
24}
25
26/// The line the report shows for one token of the pair.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28pub struct TokenAddress {
29    pub symbol: String,
30    pub network: String,
31    pub contract: Option<String>,
32    /// Networks the venue lists for the asset that this line does not show.
33    pub other_networks: usize,
34}
35
36/// The shortest string read as a contract. Venues put a placeholder such
37/// as `btc` in the contract field of a native coin.
38const MIN_CONTRACT_LEN: usize = 20;
39
40/// The contract from a venue's contract field, or `None` for a native coin.
41pub fn contract_of(raw: &str) -> Option<String> {
42    let raw = raw.trim();
43    (raw.len() >= MIN_CONTRACT_LEN).then(|| raw.to_string())
44}
45
46/// The lines for the pair's tokens. The base shows every network the venue
47/// lists. The quote (often listed on many networks) shows the network it
48/// shares with the base, else its first, and counts the others. A symbol
49/// missing from the coin list has no line.
50pub 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        // USDT is on many networks; the line a reader needs is the one on
105        // the base token's network, with the rest counted.
106        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        // A placeholder such as "btc" is a native coin, not a contract.
137        assert_eq!(lines[0].contract, None);
138        // No shared network: the quote's first network.
139        assert_eq!(lines[1].network, "TRX");
140        assert!(token_addresses("NOPE", "ALSO", &assets).is_empty());
141    }
142}