use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::types::wire::wire_enum;
wire_enum! {
pub enum CryptoChain {
Sol => "SOL",
Eth => "ETH",
Btc => "BTC",
Xrp => "XRP",
Arb => "ARB",
}
}
wire_enum! {
pub enum CryptoNetwork {
Ethereum => "ethereum",
Solana => "solana",
}
}
wire_enum! {
pub enum TransferDirection {
Incoming => "INCOMING",
Outgoing => "OUTGOING",
}
}
wire_enum! {
pub enum CryptoTransferStatus {
Processing => "PROCESSING",
Failed => "FAILED",
Complete => "COMPLETE",
}
}
wire_enum! {
pub enum WhitelistStatus {
Approved => "APPROVED",
Pending => "PENDING",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CryptoWallet {
#[serde(default)]
pub address: Option<String>,
#[serde(default)]
pub chain: Option<CryptoChain>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CryptoTransfer {
#[serde(default)]
pub id: Option<Uuid>,
#[serde(default)]
pub asset: Option<String>,
#[serde(default, with = "crate::types::option_decimal")]
pub amount: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub usd_value: Option<Decimal>,
#[serde(default)]
pub chain: Option<CryptoChain>,
#[serde(default)]
pub direction: Option<TransferDirection>,
#[serde(default)]
pub status: Option<CryptoTransferStatus>,
#[serde(default)]
pub from_address: Option<String>,
#[serde(default)]
pub to_address: Option<String>,
#[serde(default, with = "crate::types::option_decimal")]
pub fees: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub network_fee: Option<Decimal>,
#[serde(default)]
pub tx_hash: Option<String>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct WhitelistedAddress {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub address: Option<String>,
#[serde(default)]
pub asset: Option<String>,
#[serde(default)]
pub chain: Option<CryptoChain>,
#[serde(default)]
pub status: Option<WhitelistStatus>,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TransferFeeEstimate {
#[serde(default, with = "crate::types::option_decimal")]
pub fee: Option<Decimal>,
#[serde(default, with = "crate::types::option_decimal")]
pub network_fee: Option<Decimal>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GetCryptoWalletsRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asset: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chain: Option<CryptoChain>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network: Option<CryptoNetwork>,
}
impl GetCryptoWalletsRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn asset(mut self, asset: impl Into<String>) -> Self {
self.asset = Some(asset.into());
self
}
#[must_use]
pub fn chain(mut self, chain: CryptoChain) -> Self {
self.chain = Some(chain);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateCryptoTransferRequest {
pub address: String,
#[serde(with = "crate::types::decimal")]
pub amount: Decimal,
pub asset: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chain: Option<CryptoChain>,
}
impl CreateCryptoTransferRequest {
#[must_use]
pub fn new(address: impl Into<String>, asset: impl Into<String>, amount: Decimal) -> Self {
Self {
address: address.into(),
amount,
asset: asset.into(),
chain: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateWhitelistedAddressRequest {
pub address: String,
pub asset: String,
pub chain: CryptoChain,
}
impl CreateWhitelistedAddressRequest {
#[must_use]
pub fn new(address: impl Into<String>, asset: impl Into<String>, chain: CryptoChain) -> Self {
Self {
address: address.into(),
asset: asset.into(),
chain,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TransferFeeEstimateRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asset: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from_address: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to_address: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::types::option_decimal"
)]
pub amount: Option<Decimal>,
}
impl TransferFeeEstimateRequest {
#[must_use]
pub fn new(
asset: impl Into<String>,
from_address: impl Into<String>,
to_address: impl Into<String>,
amount: Decimal,
) -> Self {
Self {
asset: Some(asset.into()),
from_address: Some(from_address.into()),
to_address: Some(to_address.into()),
amount: Some(amount),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crypto_transfer_statuses_are_upper_case_on_the_wire() {
assert_eq!(CryptoTransferStatus::Complete.as_str(), "COMPLETE");
assert_eq!(TransferDirection::Incoming.as_str(), "INCOMING");
}
#[test]
fn money_fields_are_decimals_because_they_cross_the_wire_as_strings() {
let transfer: CryptoTransfer = serde_json::from_value(serde_json::json!({
"amount": "0.5",
"usd_value": "1234.56",
"network_fee": "0.0001",
"status": "COMPLETE",
}))
.unwrap();
assert_eq!(transfer.amount, Some(Decimal::new(5, 1)));
assert_eq!(transfer.usd_value, Some(Decimal::new(123_456, 2)));
}
}