df_hyperliquid_core/transfer/
arbitrum.rs1use crate::client::Network;
6use crate::utils::{Address, Dec, Json, Round, hex};
7use serde_json::Value;
8
9const fn address(text: &str) -> Address {
10 match Address::parse_const(text) {
11 Some(address) => address,
12 None => Address::ZERO,
13 }
14}
15
16pub const MAINNET_BRIDGE: Address = address("0x2df1c51e09aecf9cacb7bc98cb1742757f163df7");
18pub const TESTNET_BRIDGE: Address = address("0x08cfc1B6b2dCF36A1480b99353A354AA8AC56f89");
20
21pub const MAINNET_USDC: Address = address("0xaf88d065e77c8cC2239327C5EDb3A432268e5831");
23pub const TESTNET_USDC: Address = address("0x1baAbB04529D43a73232B713C0FE471f7c7334d5");
25
26#[must_use]
28pub const fn bridge(network: Network) -> Address {
29 match network {
30 Network::Mainnet => MAINNET_BRIDGE,
31 Network::Testnet => TESTNET_BRIDGE,
32 }
33}
34
35#[must_use]
37pub const fn usdc(network: Network) -> Address {
38 match network {
39 Network::Mainnet => MAINNET_USDC,
40 Network::Testnet => TESTNET_USDC,
41 }
42}
43
44#[must_use]
46pub const fn chain_id(network: Network) -> u64 {
47 network.arbitrum_chain_id()
48}
49
50#[must_use]
52pub const fn rpc_url(network: Network) -> &'static str {
53 match network {
54 Network::Mainnet => "https://arb1.arbitrum.io/rpc",
55 Network::Testnet => "https://sepolia-rollup.arbitrum.io/rpc",
56 }
57}
58
59pub const USDC_DECIMALS: u32 = 6;
60pub const ETH_DECIMALS: u32 = 18;
61
62pub const MINIMUM_DEPOSIT: Dec = Dec::from_int(5);
64
65const TRANSFER: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb];
67const BALANCE_OF: [u8; 4] = [0x70, 0xa0, 0x82, 0x31];
69
70#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
71pub enum BadDeposit {
72 #[error("{0} USDC has more than six decimal places")]
73 TooFine(Dec),
74 #[error("{0} USDC is not a positive amount")]
75 NotPositive(Dec),
76 #[error("{0} USDC is below the bridge's minimum of {MINIMUM_DEPOSIT}; less is lost")]
77 BelowMinimum(Dec),
78 #[error("{0} USDC is too large")]
79 TooLarge(Dec),
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
83pub enum RpcError {
84 #[error("the node refused: {0}")]
86 Refused(String),
87 #[error("the node's answer could not be read: {0}")]
88 Unreadable(String),
89}
90
91pub fn usdc_transfer_calldata(to: Address, amount: Dec) -> Result<String, BadDeposit> {
95 if !amount.is_positive() {
96 return Err(BadDeposit::NotPositive(amount));
97 }
98 if amount.places() > USDC_DECIMALS {
99 return Err(BadDeposit::TooFine(amount));
100 }
101 let units = amount
102 .units_at(USDC_DECIMALS, Round::Down)
103 .and_then(|units| u128::try_from(units).ok())
104 .ok_or(BadDeposit::TooLarge(amount))?;
105 let mut data = Vec::with_capacity(68);
106 data.extend_from_slice(&TRANSFER);
107 data.extend_from_slice(&address_word(to));
108 data.extend_from_slice(&uint_word(units));
109 Ok(hex::encode_prefixed(&data))
110}
111
112pub fn deposit_calldata(network: Network, amount: Dec) -> Result<String, BadDeposit> {
115 if amount.is_positive() && amount < MINIMUM_DEPOSIT {
116 return Err(BadDeposit::BelowMinimum(amount));
117 }
118 usdc_transfer_calldata(bridge(network), amount)
119}
120
121#[must_use]
123pub fn balance_of_calldata(owner: Address) -> String {
124 let mut data = Vec::with_capacity(36);
125 data.extend_from_slice(&BALANCE_OF);
126 data.extend_from_slice(&address_word(owner));
127 hex::encode_prefixed(&data)
128}
129
130#[must_use]
132pub fn eth_call_request(to: Address, data: &str) -> String {
133 let call = Json::Object(vec![
134 ("to".to_owned(), Json::String(to.to_hex())),
135 ("data".to_owned(), Json::String(data.to_owned())),
136 ]);
137 rpc("eth_call", vec![call, Json::String("latest".to_owned())])
138}
139
140#[must_use]
143pub fn usdc_balance_request(network: Network, owner: Address) -> String {
144 eth_call_request(usdc(network), &balance_of_calldata(owner))
145}
146
147#[must_use]
149pub fn eth_balance_request(owner: Address) -> String {
150 rpc("eth_getBalance", vec![Json::String(owner.to_hex()), Json::String("latest".to_owned())])
151}
152
153fn rpc(method: &str, params: Vec<Json>) -> String {
154 Json::Object(vec![
155 ("jsonrpc".to_owned(), Json::String("2.0".to_owned())),
156 ("id".to_owned(), Json::Number("1".to_owned())),
157 ("method".to_owned(), Json::String(method.to_owned())),
158 ("params".to_owned(), Json::Array(params)),
159 ])
160 .compact()
161}
162
163pub fn read_quantity(answer: &str, decimals: u32) -> Result<Dec, RpcError> {
166 let said: Value = serde_json::from_str(answer).map_err(|error| RpcError::Unreadable(error.to_string()))?;
167 if let Some(error) = said.get("error") {
168 let message = error.get("message").and_then(Value::as_str).map_or_else(|| error.to_string(), str::to_owned);
169 return Err(RpcError::Refused(message));
170 }
171 let result = said.get("result").and_then(Value::as_str).ok_or_else(|| RpcError::Unreadable(said.to_string()))?;
172 quantity(result, decimals).ok_or_else(|| RpcError::Unreadable(result.to_owned()))
173}
174
175#[must_use]
178pub fn quantity(text: &str, decimals: u32) -> Option<Dec> {
179 let digits = text.strip_prefix("0x")?.trim_start_matches('0');
180 let units = match digits.is_empty() {
181 true => 0,
182 false if digits.len() <= 32 => u128::from_str_radix(digits, 16).ok()?,
183 false => return None,
184 };
185 Dec::new(i128::try_from(units).ok()?, decimals)
186}
187
188fn address_word(address: Address) -> [u8; 32] {
189 let mut word = [0u8; 32];
190 word[12..].copy_from_slice(address.bytes());
191 word
192}
193
194fn uint_word(value: u128) -> [u8; 32] {
195 let mut word = [0u8; 32];
196 word[16..].copy_from_slice(&value.to_be_bytes());
197 word
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn dec(text: &str) -> Dec {
205 Dec::parse(text).unwrap()
206 }
207
208 #[test]
209 fn each_network_deposits_through_its_own_bridge_and_usdc() {
210 assert_eq!(bridge(Network::Mainnet).to_hex(), "0x2df1c51e09aecf9cacb7bc98cb1742757f163df7");
211 assert_eq!(bridge(Network::Testnet).to_hex(), "0x08cfc1b6b2dcf36a1480b99353a354aa8ac56f89");
212 assert_eq!(usdc(Network::Mainnet).to_hex(), "0xaf88d065e77c8cc2239327c5edb3a432268e5831");
213 assert_eq!(usdc(Network::Testnet).to_hex(), "0x1baabb04529d43a73232b713c0fe471f7c7334d5");
214 assert_eq!((chain_id(Network::Mainnet), chain_id(Network::Testnet)), (42_161, 421_614));
215 assert_eq!(rpc_url(Network::Mainnet), "https://arb1.arbitrum.io/rpc");
216 assert_eq!(rpc_url(Network::Testnet), "https://sepolia-rollup.arbitrum.io/rpc");
217 assert_eq!(MINIMUM_DEPOSIT, dec("5"));
218 }
219
220 #[test]
221 fn a_deposit_is_a_usdc_transfer_to_the_bridge_in_millionths() {
222 assert_eq!(
223 deposit_calldata(Network::Mainnet, dec("12.5")).unwrap(),
224 concat!(
225 "0xa9059cbb",
226 "0000000000000000000000002df1c51e09aecf9cacb7bc98cb1742757f163df7",
227 "0000000000000000000000000000000000000000000000000000000000bebc20",
228 )
229 );
230 assert_eq!(
231 deposit_calldata(Network::Testnet, dec("12.5")).unwrap(),
232 concat!(
233 "0xa9059cbb",
234 "00000000000000000000000008cfc1b6b2dcf36a1480b99353a354aa8ac56f89",
235 "0000000000000000000000000000000000000000000000000000000000bebc20",
236 )
237 );
238 assert_eq!(
239 usdc_transfer_calldata(Address::ZERO, dec("0.000001")).unwrap(),
240 concat!(
241 "0xa9059cbb",
242 "0000000000000000000000000000000000000000000000000000000000000000",
243 "0000000000000000000000000000000000000000000000000000000000000001",
244 )
245 );
246 assert_eq!(deposit_calldata(Network::Mainnet, dec("5")).unwrap().len(), 2 + 8 + 64 + 64);
247 }
248
249 #[test]
250 fn what_the_bridge_would_lose_or_round_is_refused() {
251 for network in Network::ALL {
252 assert_eq!(deposit_calldata(network, dec("4.999999")), Err(BadDeposit::BelowMinimum(dec("4.999999"))));
253 assert_eq!(deposit_calldata(network, dec("5.0000001")), Err(BadDeposit::TooFine(dec("5.0000001"))));
254 assert_eq!(deposit_calldata(network, Dec::ZERO), Err(BadDeposit::NotPositive(Dec::ZERO)));
255 }
256 assert_eq!(
257 usdc_transfer_calldata(bridge(Network::Mainnet), dec("-5")),
258 Err(BadDeposit::NotPositive(dec("-5")))
259 );
260 }
261
262 #[test]
263 fn the_reads_are_json_rpc_with_balance_of_calldata() {
264 let owner: Address = "0x14791697260E4c9A71f18484C9f997B308e59325".parse().unwrap();
265 assert_eq!(
266 balance_of_calldata(owner),
267 "0x70a0823100000000000000000000000014791697260e4c9a71f18484c9f997b308e59325"
268 );
269 assert_eq!(
270 usdc_balance_request(Network::Mainnet, owner),
271 concat!(
272 r#"{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0xaf88d065e77c8cc2239327c5edb3a432268e5831","#,
273 r#""data":"0x70a0823100000000000000000000000014791697260e4c9a71f18484c9f997b308e59325"},"latest"]}"#
274 )
275 );
276 assert!(
277 usdc_balance_request(Network::Testnet, owner)
278 .contains(r#""to":"0x1baabb04529d43a73232b713c0fe471f7c7334d5""#)
279 );
280 assert_eq!(
281 eth_balance_request(owner),
282 r#"{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0x14791697260e4c9a71f18484c9f997b308e59325","latest"]}"#
283 );
284 }
285
286 #[test]
287 fn answers_read_as_exact_amounts() {
288 let word =
289 r#"{"jsonrpc":"2.0","id":1,"result":"0x00000000000000000000000000000000000000000000000000000000004c4b40"}"#;
290 assert_eq!(read_quantity(word, USDC_DECIMALS), Ok(dec("5")));
291 let wei = r#"{"jsonrpc":"2.0","id":1,"result":"0x2386f26fc10000"}"#;
292 assert_eq!(read_quantity(wei, ETH_DECIMALS), Ok(dec("0.01")));
293 assert_eq!(read_quantity(r#"{"jsonrpc":"2.0","id":1,"result":"0x0"}"#, 18), Ok(Dec::ZERO));
294 assert_eq!(read_quantity(r#"{"jsonrpc":"2.0","id":1,"result":"0x"}"#, 6), Ok(Dec::ZERO));
295 assert_eq!(
296 read_quantity(r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"execution reverted"}}"#, 6),
297 Err(RpcError::Refused("execution reverted".to_owned()))
298 );
299 assert!(read_quantity(r#"{"jsonrpc":"2.0","id":1,"result":"12"}"#, 6).is_err());
300 assert!(read_quantity("<html>", 6).is_err());
301 assert_eq!(quantity(&format!("0x1{}", "0".repeat(32)), 6), None, "past 128 bits");
302 }
303}