polyester/codecs/decode/
deposit_withdraw.rs1use 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}