#[cfg(feature = "sign")]
use crate::account::keys::LocalWallet;
use crate::account::keys::WalletError;
use crate::client::evm::records::Receipt;
use crate::client::evm::rpc::{BlockTag, CallRequest, L1Error, Query, Rpc, RpcError, eth};
use crate::client::evm::tx::Call;
#[cfg(feature = "sign")]
use crate::client::evm::tx::tx_hash;
use crate::client::evm::u256::{B256, BadAmount, U256};
use crate::client::evm::zklighter;
use crate::client::{Http, Network};
use crate::transfer::erc20;
use crate::utils::{Address, AssetId, Dec, Route};
pub const MINIMUM_DEPOSIT: Dec = Dec::from_int(1);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BadDeposit {
#[error("{0} is not a positive amount")]
NotPositive(Dec),
#[error("{value} has more than the token's {decimals} decimal places")]
TooFine { value: Dec, decimals: u32 },
#[error("{amount} {symbol} is below the {minimum} {symbol} a deposit carries at least")]
BelowMinimum { amount: Dec, minimum: Dec, symbol: &'static str },
#[error("{0} is more than this can count")]
TooLarge(Dec),
}
impl BadDeposit {
fn from_amount(error: BadAmount, value: Dec) -> BadDeposit {
match error {
BadAmount::NotPositive(value) => BadDeposit::NotPositive(value),
BadAmount::TooFine { value, decimals } => BadDeposit::TooFine { value, decimals },
BadAmount::TooLarge | BadAmount::Unkept { .. } => BadDeposit::TooLarge(value),
}
}
}
fn units(amount: Dec, decimals: u32) -> Result<U256, BadDeposit> {
if !amount.is_positive() {
return Err(BadDeposit::NotPositive(amount));
}
U256::from_dec(amount, decimals).map_err(|error| BadDeposit::from_amount(error, amount))
}
pub fn collateral_units(network: Network, amount: Dec) -> Result<U256, BadDeposit> {
let collateral = network.collateral();
if amount.is_positive() && amount < MINIMUM_DEPOSIT {
return Err(BadDeposit::BelowMinimum { amount, minimum: MINIMUM_DEPOSIT, symbol: collateral.symbol });
}
units(amount, collateral.l1_decimals)
}
#[must_use]
pub fn approve_call(network: Network, amount: U256) -> Call {
Call::to(network.collateral().token, erc20::approve(network.contract(), amount))
}
#[must_use]
pub fn approve_max_call(network: Network) -> Call {
approve_call(network, U256::MAX)
}
pub fn deposit_call(network: Network, to: Address, amount: Dec) -> Result<Call, BadDeposit> {
let collateral = network.collateral();
let units = collateral_units(network, amount)?;
Ok(Call::to(network.contract(), zklighter::deposit(to, collateral.asset, Route::Perps, units)))
}
pub fn asset_deposit_call(
network: Network,
to: Address,
asset: AssetId,
route: Route,
token: Option<Address>,
l1_decimals: u32,
amount: Dec,
) -> Result<Call, BadDeposit> {
let units = units(amount, l1_decimals)?;
let mut call = Call::to(network.contract(), zklighter::deposit(to, asset, route, units));
if token.is_none() {
call.value = units;
}
Ok(call)
}
#[must_use]
pub fn balance_request(network: Network, owner: Address) -> Query<Vec<u8>> {
eth::call(&CallRequest::to(network.collateral().token, erc20::balance_of(owner)), BlockTag::Latest)
}
#[must_use]
pub fn allowance_request(network: Network, owner: Address) -> Query<Vec<u8>> {
eth::call(
&CallRequest::to(network.collateral().token, erc20::allowance(owner, network.contract())),
BlockTag::Latest,
)
}
#[must_use]
pub fn account_index_request(network: Network, owner: Address) -> Query<Vec<u8>> {
eth::call(&CallRequest::to(network.contract(), zklighter::address_to_account_index(owner)), BlockTag::Latest)
}
pub fn collateral_amount(network: Network, answer: &[u8]) -> Result<Dec, RpcError> {
let unreadable = |detail: String| RpcError::Unreadable { method: "eth_call", detail };
let units = erc20::read_amount(answer).map_err(|error| unreadable(error.to_string()))?;
units.to_dec(network.collateral().l1_decimals).map_err(|error| unreadable(error.to_string()))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DepositError {
#[error(transparent)]
Bad(#[from] BadDeposit),
#[error("the node is on chain {found}, not {expected}; nothing was sent")]
WrongChain { expected: u64, found: u64 },
#[error("the wallet holds {have} {symbol}, less than the {need} to deposit")]
Short { have: Dec, need: Dec, symbol: &'static str },
#[error(transparent)]
Rpc(#[from] RpcError),
#[error(transparent)]
Wallet(#[from] WalletError),
#[error("transaction {0} reverted")]
Reverted(B256),
#[error("transaction {0} was sent and is not yet mined; it may still land")]
Unconfirmed(B256),
}
impl From<L1Error> for DepositError {
fn from(error: L1Error) -> DepositError {
match error {
L1Error::Rpc(error) => DepositError::Rpc(error),
L1Error::Wallet(error) => DepositError::Wallet(error),
L1Error::WrongChain { expected, found } => DepositError::WrongChain { expected, found },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepositPlan {
pub network: Network,
pub owner: Address,
pub to: Address,
pub amount: U256,
pub balance: U256,
pub allowance: U256,
pub approve: Option<Call>,
pub deposit: Call,
}
impl DepositPlan {
#[must_use]
pub const fn chain_id(&self) -> u64 {
self.network.l1_chain_id()
}
#[must_use]
pub fn calls(&self) -> Vec<Call> {
self.approve.iter().cloned().chain([self.deposit.clone()]).collect()
}
}
pub async fn plan_deposit<H: Http>(
rpc: &Rpc<H>,
network: Network,
owner: Address,
to: Address,
amount: Dec,
) -> Result<DepositPlan, DepositError> {
let units = collateral_units(network, amount)?;
let deposit = deposit_call(network, to, amount)?;
rpc.check_chain(network.l1_chain_id()).await?;
let unreadable = |detail: String| RpcError::Unreadable { method: "eth_call", detail };
let balance = erc20::read_amount(&rpc.run(balance_request(network, owner)).await?)
.map_err(|error| unreadable(error.to_string()))?;
let collateral = network.collateral();
if balance < units {
let have = balance.to_dec(collateral.l1_decimals).unwrap_or(Dec::ZERO);
return Err(DepositError::Short { have, need: amount, symbol: collateral.symbol });
}
let allowance = erc20::read_amount(&rpc.run(allowance_request(network, owner)).await?)
.map_err(|error| unreadable(error.to_string()))?;
let approve = (allowance < units).then(|| approve_call(network, units));
Ok(DepositPlan { network, owner, to, amount: units, balance, allowance, approve, deposit })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Deposited {
pub approval: Option<Receipt>,
pub deposit: Receipt,
}
#[cfg(feature = "sign")]
pub async fn deposit<H: Http>(
rpc: &Rpc<H>,
wallet: &LocalWallet,
network: Network,
amount: Dec,
pause: impl AsyncFn(u64),
) -> Result<Deposited, DepositError> {
let owner = wallet.address();
let plan = plan_deposit(rpc, network, owner, owner, amount).await?;
let approval = match plan.approve {
Some(call) => Some(mined(rpc, wallet, network, call, &pause).await?),
None => None,
};
let deposit = mined(rpc, wallet, network, plan.deposit, &pause).await?;
Ok(Deposited { approval, deposit })
}
#[cfg(feature = "sign")]
async fn mined<H: Http>(
rpc: &Rpc<H>,
wallet: &LocalWallet,
network: Network,
call: Call,
pause: &impl AsyncFn(u64),
) -> Result<Receipt, DepositError> {
let raw = rpc.sign_call(wallet, network.l1_chain_id(), call).await?;
let hash = tx_hash(&raw);
match rpc.send_raw_transaction(&raw).await {
Ok(_) => {}
Err(error) if error.may_have_landed() => return Err(DepositError::Unconfirmed(hash)),
Err(error) => return Err(error.into()),
}
match rpc.wait_for_receipt(hash, async |ms| pause(ms).await).await {
Ok(Some(receipt)) if receipt.succeeded() => Ok(receipt),
Ok(Some(_)) => Err(DepositError::Reverted(hash)),
Ok(None) | Err(_) => Err(DepositError::Unconfirmed(hash)),
}
}
#[cfg(test)]
#[path = "tests/deposit.rs"]
mod tests;