df_papertrade_core/transfer/
deposit.rs1use df_hyperliquid_core::account::verify;
8use df_hyperliquid_core::client::evm::precompiles::core_user_exists;
9use df_hyperliquid_core::client::{ExchangeResponse, Http, Network, Request, TransportError};
10use df_hyperliquid_core::transfer::{Amount, SPOT_DEX, SendAsset, SpotSend, UsdClassTransfer, UserAction};
11use df_hyperliquid_core::utils::{Address, Dec};
12
13use crate::account::wallet::{Purpose, SignRequest, Wallet};
14use crate::client::{Call, Client, Error};
15use crate::utils::wire::{BadWire, address_of, field, parse};
16
17pub const ACTIVATION_FEE_USDC: u64 = 1_000_000;
19pub const MIN_DEPOSIT_USDC: u64 = 10_000_000;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Source {
25 Spot,
26 Perp,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum Mode {
33 Standard,
35 Pooled,
37}
38
39#[derive(Debug, Clone)]
41pub struct Deposit {
42 pub to: Address,
43 pub opens_account: bool,
45 pub activation_fee: u64,
48 pub sends: Vec<Send>,
49}
50
51#[derive(Debug, Clone)]
53pub enum Send {
54 ToSpot(UsdClassTransfer),
56 Spot(SpotSend),
58 Pooled(SendAsset),
60}
61
62impl Send {
63 #[must_use]
64 pub fn action(&self) -> &dyn UserAction {
65 match self {
66 Send::ToSpot(action) => action,
67 Send::Spot(action) => action,
68 Send::Pooled(action) => action,
69 }
70 }
71
72 #[must_use]
73 pub const fn purpose(&self) -> Purpose {
74 match self {
75 Send::ToSpot(_) => Purpose::DepositToSpot,
76 Send::Spot(_) | Send::Pooled(_) => Purpose::Deposit,
77 }
78 }
79}
80
81fn amount(usdc: u64) -> Result<Amount, Error> {
83 let value = Dec::new(i128::from(usdc), 6).ok_or_else(|| Error::Decode("amount out of range".into()))?;
84 Amount::cut(value, 6).map_err(|e| Error::Invalid(e.to_string()))
85}
86
87fn checked_address(text: &str) -> Result<Address, BadWire> {
89 let json = parse(text, "deposit_check")?;
90 address_of(field(&json, "proxy")?, "proxy")
91}
92
93impl<H: Http + Clone> Client<H> {
94 pub async fn register_deposit(&self, owner: Address) -> Result<Address, Error> {
96 let text = self.relayer().send(&Call::deposit_check(owner)).await?;
97 Ok(checked_address(&text)?)
98 }
99
100 pub async fn plan_deposit(&self, owner: Address, usdc: u64, source: Source, mode: Mode) -> Result<Deposit, Error> {
105 if usdc < MIN_DEPOSIT_USDC {
106 return Err(Error::Invalid("a deposit brings in at least 10 USDC".into()));
107 }
108 let to = self.register_deposit(owner).await?;
109 if to != self.deposit_address(owner).await? {
110 return Err(Error::Decode("the relayer named a deposit address the factory does not derive".into()));
111 }
112 let exists = self.rpc().read(&core_user_exists(to)).await.map_err(Error::Precompile)?;
113 let opens_account = !exists;
114 let activation_fee = if opens_account { ACTIVATION_FEE_USDC } else { 0 };
115 let nonce = self.now_ms();
116 let network = Network::Mainnet;
117 let sends = match (mode, source) {
118 (Mode::Pooled, _) => {
119 vec![Send::Pooled(SendAsset::usdc(network, to, SPOT_DEX, SPOT_DEX, amount(usdc)?, nonce))]
120 }
121 (Mode::Standard, Source::Spot) => {
122 vec![Send::Spot(SpotSend::new(to, network.usdc().wire, amount(usdc)?, nonce))]
123 }
124 (Mode::Standard, Source::Perp) => {
125 vec![
126 Send::ToSpot(UsdClassTransfer::new(amount(usdc.saturating_add(activation_fee))?, false, nonce)),
127 Send::Spot(SpotSend::new(to, network.usdc().wire, amount(usdc)?, nonce + 1)),
128 ]
129 }
130 };
131 Ok(Deposit { to, opens_account, activation_fee, sends })
132 }
133
134 pub async fn send_deposit(&self, wallet: &impl Wallet, deposit: &Deposit) -> Result<(), Error> {
138 let owner = wallet.address();
139 let network = Network::Mainnet;
140 for send in &deposit.sends {
141 let action = send.action();
142 let request = SignRequest {
143 purpose: send.purpose(),
144 json: action.wallet_json(network),
145 digest: action.digest(network),
146 };
147 let signature = wallet.sign(&request).await?;
148 verify(&request.digest, &signature, owner).map_err(|e| Error::Signature(e.to_string()))?;
149 let answer = self
150 .http()
151 .send(Request::post_json(self.config().exchange.clone(), action.body(&signature, network)))
152 .await?;
153 if !answer.is_success() {
154 return Err(TransportError::Status { status: answer.status, body: answer.text() }.into());
155 }
156 match ExchangeResponse::parse(&answer.text()).map_err(|e| Error::Decode(e.to_string()))? {
157 ExchangeResponse::Ok(_) => {}
158 ExchangeResponse::Err(why) => return Err(Error::Exchange(why)),
159 }
160 }
161 Ok(())
162 }
163}
164
165#[cfg(test)]
166#[path = "tests/deposit.rs"]
167mod tests;