use serde::{Deserialize, Deserializer};
use crate::client::http::{ApiError, Call, Transport, read};
use crate::utils::ids::whole;
use crate::utils::{AccountIndex, Address, AssetId, Dec, Route};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Page<T> {
pub rows: Vec<T>,
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum HistoryFilter {
#[default]
All,
Pending,
Claimable,
}
impl HistoryFilter {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
HistoryFilter::All => "all",
HistoryFilter::Pending => "pending",
HistoryFilter::Claimable => "claimable",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DepositStatus {
Pending,
Completed,
Claimable,
Failed,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct DepositRow {
pub id: String,
#[serde(rename = "asset_id")]
pub asset: AssetId,
pub amount: Dec,
#[serde(deserialize_with = "whole")]
pub timestamp: u64,
pub status: DepositStatus,
#[serde(default)]
pub l1_tx_hash: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WithdrawalStatus {
Pending,
Claimable,
Completed,
Refunded,
Failed,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WithdrawalKind {
Secure,
Fast,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct WithdrawalRow {
pub id: String,
#[serde(rename = "asset_id")]
pub asset: AssetId,
pub amount: Dec,
#[serde(deserialize_with = "whole")]
pub timestamp: u64,
pub status: WithdrawalStatus,
#[serde(rename = "type")]
pub kind: WithdrawalKind,
#[serde(default)]
pub l1_tx_hash: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
pub enum TransferKind {
L2TransferInflow,
L2TransferOutflow,
L2SelfTransfer,
L2BurnSharesInflow,
L2BurnSharesOutflow,
L2MintSharesInflow,
L2MintSharesOutflow,
L2ForceBurnSharesInflow,
L2ForceBurnSharesOutflow,
L2StakeAssetInflow,
L2StakeAssetOutflow,
L2UnstakeAssetInflow,
L2UnstakeAssetOutflow,
L1BurnSharesInflow,
L1BurnSharesOutflow,
L1UnstakeAssetInflow,
L1UnstakeAssetOutflow,
L2CreatePublicPoolInflow,
L2CreatePublicPoolOutflow,
L2CreateStakingPoolInflow,
L2CreateStakingPoolOutflow,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct TransferRow {
pub id: String,
#[serde(rename = "asset_id")]
pub asset: AssetId,
pub amount: Dec,
pub fee: Dec,
#[serde(deserialize_with = "whole")]
pub timestamp: u64,
#[serde(rename = "type")]
pub kind: TransferKind,
pub from_l1_address: Address,
pub to_l1_address: Address,
#[serde(rename = "from_account_index")]
pub from_account: AccountIndex,
#[serde(rename = "to_account_index")]
pub to_account: AccountIndex,
#[serde(deserialize_with = "route")]
pub from_route: Option<Route>,
#[serde(deserialize_with = "route")]
pub to_route: Option<Route>,
pub tx_hash: String,
}
fn route<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Route>, D::Error> {
Ok(match Option::<String>::deserialize(deserializer)?.as_deref() {
Some("perps") => Some(Route::Perps),
Some("spot") => Some(Route::Spot),
_ => None,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TransferFilter {
#[default]
All,
L2Transfer,
L2MintShares,
L2BurnShares,
L2StakeAssets,
L2UnstakeAssets,
L2CreatePublicPool,
L2CreateStakingPool,
L1BurnShares,
L1UnstakeAssets,
ReferralProgramPayout,
}
impl TransferFilter {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
TransferFilter::All => "all",
TransferFilter::L2Transfer => "L2Transfer",
TransferFilter::L2MintShares => "L2MintShares",
TransferFilter::L2BurnShares => "L2BurnShares",
TransferFilter::L2StakeAssets => "L2StakeAssets",
TransferFilter::L2UnstakeAssets => "L2UnstakeAssets",
TransferFilter::L2CreatePublicPool => "L2CreatePublicPool",
TransferFilter::L2CreateStakingPool => "L2CreateStakingPool",
TransferFilter::L1BurnShares => "L1BurnShares",
TransferFilter::L1UnstakeAssets => "L1UnstakeAssets",
TransferFilter::ReferralProgramPayout => "ReferralProgramPayout",
}
}
}
#[derive(Deserialize)]
struct Deposits {
#[serde(default)]
deposits: Vec<DepositRow>,
#[serde(default)]
cursor: String,
}
#[derive(Deserialize)]
struct Withdrawals {
#[serde(default)]
withdraws: Vec<WithdrawalRow>,
#[serde(default)]
cursor: String,
}
#[derive(Deserialize)]
struct Transfers {
#[serde(default)]
transfers: Vec<TransferRow>,
#[serde(default)]
cursor: String,
}
fn page<T>(rows: Vec<T>, cursor: String) -> Page<T> {
let cursor = (!cursor.is_empty() && !rows.is_empty()).then_some(cursor);
Page { rows, cursor }
}
#[must_use]
pub fn deposit_history_call(
account: AccountIndex,
l1_address: Address,
filter: HistoryFilter,
cursor: Option<&str>,
) -> Call {
Call::get("deposit/history")
.field("account_index", account)
.field("l1_address", l1_address)
.field("filter", filter.name())
.maybe("cursor", cursor)
.authed()
}
pub async fn deposit_history(
transport: &impl Transport,
account: AccountIndex,
l1_address: Address,
filter: HistoryFilter,
cursor: Option<&str>,
) -> Result<Page<DepositRow>, ApiError> {
let answer: Deposits = read(transport, deposit_history_call(account, l1_address, filter, cursor)).await?;
Ok(page(answer.deposits, answer.cursor))
}
#[must_use]
pub fn withdraw_history_call(account: AccountIndex, filter: HistoryFilter, cursor: Option<&str>) -> Call {
Call::get("withdraw/history")
.field("account_index", account)
.field("filter", filter.name())
.maybe("cursor", cursor)
.authed()
}
pub async fn withdraw_history(
transport: &impl Transport,
account: AccountIndex,
filter: HistoryFilter,
cursor: Option<&str>,
) -> Result<Page<WithdrawalRow>, ApiError> {
let answer: Withdrawals = read(transport, withdraw_history_call(account, filter, cursor)).await?;
Ok(page(answer.withdraws, answer.cursor))
}
#[must_use]
pub fn transfer_history_call(account: AccountIndex, filter: TransferFilter, cursor: Option<&str>) -> Call {
let call = Call::get("transfer/history").field("account_index", account);
let call = match filter {
TransferFilter::All => call,
other => call.field("type", other.name()),
};
call.maybe("cursor", cursor).authed()
}
pub async fn transfer_history(
transport: &impl Transport,
account: AccountIndex,
filter: TransferFilter,
cursor: Option<&str>,
) -> Result<Page<TransferRow>, ApiError> {
let answer: Transfers = read(transport, transfer_history_call(account, filter, cursor)).await?;
Ok(page(answer.transfers, answer.cursor))
}
#[cfg(test)]
#[path = "tests/history.rs"]
mod tests;