Skip to main content

polyester/codecs/decode/
deposit_withdraw.rs

1//! Deposit / withdraw / internal-transfer decoders.
2
3use super::money::decode_asset_amount_u128;
4use crate::codecs::scalars::LEDGER_SCALE;
5use crate::errors::{Error, Result};
6use crate::models::{
7    DepositAddress, DepositAddressesList, InternalTransferResult, WithdrawIntentResult,
8};
9use crate::proto::chain::deposit::v1::{
10    CreateDepositAddressResponse, DepositAddress as ProtoDepositAddress,
11    ListDepositAddressesResponse,
12};
13use crate::proto::chain::withdraw::v1::{
14    CreateTradingWithdrawResponse, CreateWalletTradingWithdrawResponse,
15};
16use crate::proto::transfer::v1::CreateInternalTransferResponse;
17use crate::types::QuantityDomain;
18
19pub fn deposit_address_from_proto(msg: &ProtoDepositAddress) -> DepositAddress {
20    DepositAddress {
21        chain_id: msg.chain_id,
22        deposit_address: msg.deposit_address.clone(),
23    }
24}
25
26pub fn deposit_addresses_list_from_proto(
27    msg: &ListDepositAddressesResponse,
28) -> DepositAddressesList {
29    DepositAddressesList {
30        addresses: msg
31            .deposit_addresses
32            .iter()
33            .map(deposit_address_from_proto)
34            .collect(),
35    }
36}
37
38pub fn create_deposit_address_from_proto(
39    msg: &CreateDepositAddressResponse,
40) -> Result<DepositAddress> {
41    let address = msg.deposit_address.as_option().ok_or_else(|| {
42        Error::transport("invalid CreateDepositAddress response: missing deposit_address")
43    })?;
44    if address.deposit_address.trim().is_empty() {
45        return Err(Error::transport(
46            "invalid CreateDepositAddress response: empty deposit address",
47        ));
48    }
49    Ok(deposit_address_from_proto(address))
50}
51
52pub fn withdraw_intent_from_proto(
53    msg: &CreateTradingWithdrawResponse,
54) -> Result<WithdrawIntentResult> {
55    if msg.intent_id.trim().is_empty() {
56        return Err(Error::transport(
57            "invalid CreateTradingWithdraw response: missing intent_id",
58        ));
59    }
60    Ok(WithdrawIntentResult {
61        intent_id: msg.intent_id.clone(),
62        status: String::new(),
63        flow_id: String::new(),
64    })
65}
66
67pub fn withdraw_intent_from_wallet_proto(
68    msg: &CreateWalletTradingWithdrawResponse,
69) -> Result<WithdrawIntentResult> {
70    if msg.intent_id.trim().is_empty() {
71        return Err(Error::transport(
72            "invalid CreateWalletTradingWithdraw response: missing intent_id",
73        ));
74    }
75    Ok(WithdrawIntentResult {
76        intent_id: msg.intent_id.clone(),
77        status: String::new(),
78        flow_id: String::new(),
79    })
80}
81
82pub fn internal_transfer_from_proto(
83    msg: &CreateInternalTransferResponse,
84) -> Result<InternalTransferResult> {
85    if msg.request_id.trim().is_empty() || msg.transfer_id.trim().is_empty() {
86        return Err(Error::transport(
87            "invalid CreateInternalTransfer response: missing request_id or transfer_id",
88        ));
89    }
90    let asset_id = msg.asset_id;
91    let quantity = msg.amount_e18.as_option().and_then(|u| {
92        decode_asset_amount_u128(
93            u.hi,
94            u.lo,
95            Some(LEDGER_SCALE),
96            QuantityDomain::LedgerE18,
97            Some(asset_id),
98        )
99    });
100    Ok(InternalTransferResult {
101        request_id: msg.request_id.clone(),
102        transfer_id: msg.transfer_id.clone(),
103        asset_id,
104        asset_code: msg.asset_code.clone(),
105        quantity,
106    })
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::proto::polyester::r#type::v1::U128;
113
114    #[test]
115    fn deposit_addresses_list_maps_rows() {
116        let msg = ListDepositAddressesResponse {
117            deposit_addresses: vec![ProtoDepositAddress {
118                chain_id: 1,
119                deposit_address: "0xabc".into(),
120                ..Default::default()
121            }],
122            ..Default::default()
123        };
124        let list = deposit_addresses_list_from_proto(&msg);
125        assert_eq!(list.addresses.len(), 1);
126        assert_eq!(list.addresses[0].chain_id, 1);
127        assert_eq!(list.addresses[0].deposit_address, "0xabc");
128    }
129
130    #[test]
131    fn create_deposit_address_rejects_missing_required_entity() {
132        let err = create_deposit_address_from_proto(&CreateDepositAddressResponse::default())
133            .expect_err("missing deposit address must fail closed");
134        assert!(err.to_string().contains("missing deposit_address"));
135    }
136
137    #[test]
138    fn internal_transfer_maps_u128_quantity() {
139        let msg = CreateInternalTransferResponse {
140            request_id: "req".into(),
141            transfer_id: "xfer".into(),
142            asset_id: 7,
143            asset_code: "USDT".into(),
144            amount_e18: U128 {
145                hi: 0,
146                lo: 1_000_000_000_000_000_000,
147                ..Default::default()
148            }
149            .into(),
150            ..Default::default()
151        };
152        let result = internal_transfer_from_proto(&msg).unwrap();
153        assert_eq!(result.request_id, "req");
154        assert_eq!(result.transfer_id, "xfer");
155        assert_eq!(result.asset_id, 7);
156        assert_eq!(
157            result.quantity.as_ref().unwrap().as_scaled(),
158            1_000_000_000_000_000_000
159        );
160    }
161
162    #[test]
163    fn withdraw_intent_maps_id() {
164        let msg = CreateTradingWithdrawResponse {
165            intent_id: "intent-1".into(),
166            ..Default::default()
167        };
168        assert_eq!(
169            withdraw_intent_from_proto(&msg).unwrap().intent_id,
170            "intent-1"
171        );
172    }
173
174    #[test]
175    fn singular_mutations_reject_empty_success_responses() {
176        assert!(withdraw_intent_from_proto(&CreateTradingWithdrawResponse::default()).is_err());
177        assert!(
178            withdraw_intent_from_wallet_proto(&CreateWalletTradingWithdrawResponse::default())
179                .is_err()
180        );
181        assert!(internal_transfer_from_proto(&CreateInternalTransferResponse::default()).is_err());
182    }
183}