Skip to main content

df_lighter_core/client/
network.rs

1//! The two exchanges Lighter runs: Lighter itself, settled on Ethereum with USDC, and Lighter on
2//! Robinhood Chain, with USDG. They share the software, the transaction format and the API, and
3//! nothing else: accounts, keys, nonces, market ids and balances are each venue's own, and a
4//! transaction signed for one is refused by the other. Everything that differs is a method on
5//! [`Network`], so the network is never assumed.
6//!
7//! Sources: each host's `layer1BasicInfo` and `info` (the contract, the collateral token), the
8//! docs' environments and Robinhood Chain pages, and lighter-ts-sdk 1.1.0 for the signing chain
9//! ids (Robinhood's is neither its L1's chain id nor its network id).
10
11use std::{fmt, str::FromStr};
12
13use crate::utils::{Address, ApiKeyIndex, AssetId};
14
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
16pub enum Network {
17    /// Lighter: settled on Ethereum, USDC collateral.
18    #[default]
19    Lighter,
20    /// Lighter on Robinhood Chain: settled on Robinhood Chain, USDG collateral; crypto, US stocks,
21    /// pre-IPO names and metals.
22    Robinhood,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
26#[error("`{0}` is not a Lighter network; expected lighter or robinhood")]
27pub struct UnknownNetwork(pub String);
28
29/// The collateral perps margin in, as each place names it.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct Collateral {
32    pub symbol: &'static str,
33    /// The ERC-20 on the network's L1.
34    pub token: Address,
35    /// The token's decimals on its L1, which a deposit's amount is counted in.
36    pub l1_decimals: u32,
37    /// The asset id the venue knows it by.
38    pub asset: AssetId,
39    /// The decimals the venue counts it in, which a transfer or a withdrawal is signed in.
40    pub decimals: u32,
41}
42
43/// USDC on Ethereum.
44pub const USDC: Collateral = Collateral {
45    symbol: "USDC",
46    token: address("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
47    l1_decimals: 6,
48    asset: AssetId::COLLATERAL,
49    decimals: 6,
50};
51
52/// USDG on Robinhood Chain.
53pub const USDG: Collateral = Collateral {
54    symbol: "USDG",
55    token: address("0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168"),
56    l1_decimals: 6,
57    asset: AssetId::COLLATERAL,
58    decimals: 6,
59};
60
61/// The ZkLighter contract on Ethereum: deposits, secure withdrawals, priority transactions.
62pub const LIGHTER_CONTRACT: Address = address("0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7");
63
64/// The ZkLighter contract on Robinhood Chain.
65pub const ROBINHOOD_CONTRACT: Address = address("0x94bAB9693Ba2f6358507eFfcbd372b0660AFfF9d");
66
67const fn address(text: &str) -> Address {
68    match Address::parse_const(text) {
69        Some(address) => address,
70        None => Address::ZERO,
71    }
72}
73
74impl Network {
75    pub const ALL: [Network; 2] = [Network::Lighter, Network::Robinhood];
76
77    #[must_use]
78    pub const fn name(self) -> &'static str {
79        match self {
80            Network::Lighter => "lighter",
81            Network::Robinhood => "robinhood",
82        }
83    }
84
85    /// The REST API's host; every path is under `/api/v1/`.
86    #[must_use]
87    pub const fn api_url(self) -> &'static str {
88        match self {
89            Network::Lighter => "https://mainnet.zklighter.elliot.ai",
90            Network::Robinhood => "https://api.rh.lighter.xyz",
91        }
92    }
93
94    /// The websocket. `?readonly=true` serves market data in restricted regions.
95    #[must_use]
96    pub const fn ws_url(self) -> &'static str {
97        match self {
98            Network::Lighter => "wss://mainnet.zklighter.elliot.ai/stream",
99            Network::Robinhood => "wss://api.rh.lighter.xyz/stream",
100        }
101    }
102
103    /// The read-only websocket, which takes no transactions.
104    #[must_use]
105    pub const fn ws_readonly_url(self) -> &'static str {
106        match self {
107            Network::Lighter => "wss://mainnet.zklighter.elliot.ai/stream?readonly=true",
108            Network::Robinhood => "wss://api.rh.lighter.xyz/stream?readonly=true",
109        }
110    }
111
112    /// The chain id every transaction is signed for: not a real chain's.
113    #[must_use]
114    pub const fn chain_id(self) -> u32 {
115        match self {
116            Network::Lighter => 304,
117            Network::Robinhood => 466_324,
118        }
119    }
120
121    /// The chain the venue settles on, where deposits are made and secure withdrawals arrive.
122    #[must_use]
123    pub const fn l1_chain_id(self) -> u64 {
124        match self {
125            Network::Lighter => 1,
126            Network::Robinhood => 4663,
127        }
128    }
129
130    #[must_use]
131    pub const fn l1_name(self) -> &'static str {
132        match self {
133            Network::Lighter => "Ethereum",
134            Network::Robinhood => "Robinhood Chain",
135        }
136    }
137
138    /// A public JSON-RPC node on the L1, for reads and to send a deposit through. A program in
139    /// production brings its own.
140    #[must_use]
141    pub const fn l1_rpc_url(self) -> &'static str {
142        match self {
143            Network::Lighter => "https://ethereum-rpc.publicnode.com",
144            Network::Robinhood => "https://rpc.mainnet.chain.robinhood.com",
145        }
146    }
147
148    /// The L1's block explorer, for a link a person follows.
149    #[must_use]
150    pub const fn l1_explorer_url(self) -> &'static str {
151        match self {
152            Network::Lighter => "https://etherscan.io",
153            Network::Robinhood => "https://robinhoodchain.blockscout.com",
154        }
155    }
156
157    /// The ZkLighter contract on the L1.
158    #[must_use]
159    pub const fn contract(self) -> Address {
160        match self {
161            Network::Lighter => LIGHTER_CONTRACT,
162            Network::Robinhood => ROBINHOOD_CONTRACT,
163        }
164    }
165
166    #[must_use]
167    pub const fn collateral(self) -> Collateral {
168        match self {
169            Network::Lighter => USDC,
170            Network::Robinhood => USDG,
171        }
172    }
173
174    /// The explorer's API: blocks, batches, logs and an account's history.
175    #[must_use]
176    pub const fn explorer_api_url(self) -> &'static str {
177        match self {
178            Network::Lighter => "https://explorer.elliot.ai/api",
179            Network::Robinhood => "https://explorerapi.rh.lighter.xyz/api",
180        }
181    }
182
183    /// The venue's own web app, for a link a person follows.
184    #[must_use]
185    pub const fn app_url(self) -> &'static str {
186        match self {
187            Network::Lighter => "https://app.lighter.xyz",
188            Network::Robinhood => "https://lighter.xyz",
189        }
190    }
191
192    /// Whether the slot belongs to Lighter's own web and mobile apps: 0 to 3 on both venues, and
193    /// 157 too on Robinhood Chain. Registering a key there logs the venue's app out.
194    #[must_use]
195    pub const fn reserves_api_key(self, slot: ApiKeyIndex) -> bool {
196        matches!((self, slot.get()), (_, 0..=3) | (Network::Robinhood, 157))
197    }
198
199    /// Whether the venue lists binary options: Lighter does, Robinhood Chain does not.
200    #[must_use]
201    pub const fn has_binary_options(self) -> bool {
202        matches!(self, Network::Lighter)
203    }
204
205    /// Fast withdrawals an L1 address may make an hour.
206    #[must_use]
207    pub const fn fast_withdrawals_per_hour(self) -> u32 {
208        match self {
209            Network::Lighter => 10,
210            Network::Robinhood => 20,
211        }
212    }
213
214    /// Sub-accounts a Standard account may hold: 4 on Lighter, 8 on Robinhood Chain.
215    #[must_use]
216    pub const fn standard_sub_accounts(self) -> u32 {
217        match self {
218            Network::Lighter => 4,
219            Network::Robinhood => 8,
220        }
221    }
222}
223
224impl fmt::Display for Network {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        f.write_str(self.name())
227    }
228}
229
230impl FromStr for Network {
231    type Err = UnknownNetwork;
232
233    fn from_str(text: &str) -> Result<Self, Self::Err> {
234        match text.trim().to_ascii_lowercase().as_str() {
235            "lighter" | "zklighter" | "mainnet" => Ok(Network::Lighter),
236            "robinhood" | "lighter-rh" | "rh" => Ok(Network::Robinhood),
237            _ => Err(UnknownNetwork(text.trim().to_owned())),
238        }
239    }
240}
241
242#[cfg(test)]
243#[path = "tests/network.rs"]
244mod tests;