use df_hyperliquid_core::account::verify;
use df_hyperliquid_core::client::evm::precompiles::core_user_exists;
use df_hyperliquid_core::client::{ExchangeResponse, Http, Network, Request, TransportError};
use df_hyperliquid_core::transfer::{Amount, SPOT_DEX, SendAsset, SpotSend, UsdClassTransfer, UserAction};
use df_hyperliquid_core::utils::{Address, Dec};
use crate::account::wallet::{Purpose, SignRequest, Wallet};
use crate::client::{Call, Client, Error};
use crate::utils::wire::{BadWire, address_of, field, parse};
pub const ACTIVATION_FEE_USDC: u64 = 1_000_000;
pub const MIN_DEPOSIT_USDC: u64 = 10_000_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Source {
Spot,
Perp,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mode {
Standard,
Pooled,
}
#[derive(Debug, Clone)]
pub struct Deposit {
pub to: Address,
pub opens_account: bool,
pub activation_fee: u64,
pub sends: Vec<Send>,
}
#[derive(Debug, Clone)]
pub enum Send {
ToSpot(UsdClassTransfer),
Spot(SpotSend),
Pooled(SendAsset),
}
impl Send {
#[must_use]
pub fn action(&self) -> &dyn UserAction {
match self {
Send::ToSpot(action) => action,
Send::Spot(action) => action,
Send::Pooled(action) => action,
}
}
#[must_use]
pub const fn purpose(&self) -> Purpose {
match self {
Send::ToSpot(_) => Purpose::DepositToSpot,
Send::Spot(_) | Send::Pooled(_) => Purpose::Deposit,
}
}
}
fn amount(usdc: u64) -> Result<Amount, Error> {
let value = Dec::new(i128::from(usdc), 6).ok_or_else(|| Error::Decode("amount out of range".into()))?;
Amount::cut(value, 6).map_err(|e| Error::Invalid(e.to_string()))
}
fn checked_address(text: &str) -> Result<Address, BadWire> {
let json = parse(text, "deposit_check")?;
address_of(field(&json, "proxy")?, "proxy")
}
impl<H: Http + Clone> Client<H> {
pub async fn register_deposit(&self, owner: Address) -> Result<Address, Error> {
let text = self.relayer().send(&Call::deposit_check(owner)).await?;
Ok(checked_address(&text)?)
}
pub async fn plan_deposit(&self, owner: Address, usdc: u64, source: Source, mode: Mode) -> Result<Deposit, Error> {
if usdc < MIN_DEPOSIT_USDC {
return Err(Error::Invalid("a deposit brings in at least 10 USDC".into()));
}
let to = self.register_deposit(owner).await?;
if to != self.deposit_address(owner).await? {
return Err(Error::Decode("the relayer named a deposit address the factory does not derive".into()));
}
let exists = self.rpc().read(&core_user_exists(to)).await.map_err(Error::Precompile)?;
let opens_account = !exists;
let activation_fee = if opens_account { ACTIVATION_FEE_USDC } else { 0 };
let nonce = self.now_ms();
let network = Network::Mainnet;
let sends = match (mode, source) {
(Mode::Pooled, _) => {
vec![Send::Pooled(SendAsset::usdc(network, to, SPOT_DEX, SPOT_DEX, amount(usdc)?, nonce))]
}
(Mode::Standard, Source::Spot) => {
vec![Send::Spot(SpotSend::new(to, network.usdc().wire, amount(usdc)?, nonce))]
}
(Mode::Standard, Source::Perp) => {
vec![
Send::ToSpot(UsdClassTransfer::new(amount(usdc.saturating_add(activation_fee))?, false, nonce)),
Send::Spot(SpotSend::new(to, network.usdc().wire, amount(usdc)?, nonce + 1)),
]
}
};
Ok(Deposit { to, opens_account, activation_fee, sends })
}
pub async fn send_deposit(&self, wallet: &impl Wallet, deposit: &Deposit) -> Result<(), Error> {
let owner = wallet.address();
let network = Network::Mainnet;
for send in &deposit.sends {
let action = send.action();
let request = SignRequest {
purpose: send.purpose(),
json: action.wallet_json(network),
digest: action.digest(network),
};
let signature = wallet.sign(&request).await?;
verify(&request.digest, &signature, owner).map_err(|e| Error::Signature(e.to_string()))?;
let answer = self
.http()
.send(Request::post_json(self.config().exchange.clone(), action.body(&signature, network)))
.await?;
if !answer.is_success() {
return Err(TransportError::Status { status: answer.status, body: answer.text() }.into());
}
match ExchangeResponse::parse(&answer.text()).map_err(|e| Error::Decode(e.to_string()))? {
ExchangeResponse::Ok(_) => {}
ExchangeResponse::Err(why) => return Err(Error::Exchange(why)),
}
}
Ok(())
}
}
#[cfg(test)]
#[path = "tests/deposit.rs"]
mod tests;