1use super::money::decode_asset_amount_u128;
4use crate::codecs::scalars::LEDGER_SCALE;
5use crate::errors::{Error, Result};
6use crate::models::{
7 DepositAddress, DepositAddressesList, InternalTransferResult, WithdrawDestinationValidation,
8 WithdrawIntentResult,
9};
10use crate::proto::chain::deposit::v1::{
11 CreateDepositAddressResponse, DepositAddress as ProtoDepositAddress,
12 ListDepositAddressesResponse,
13};
14use crate::proto::chain::withdraw::v1::{
15 CreateTradingWithdrawResponse, CreateWalletTradingWithdrawResponse,
16 ValidateWithdrawDestinationResponse, WithdrawDestinationValidationCode,
17};
18use crate::proto::transfer::v1::CreateInternalTransferResponse;
19use crate::types::QuantityDomain;
20
21fn withdraw_destination_validation_code_label(
22 value: &buffa::EnumValue<WithdrawDestinationValidationCode>,
23) -> String {
24 match value.as_known() {
25 Some(WithdrawDestinationValidationCode::RESULT_UNSPECIFIED) => "unspecified".to_owned(),
26 Some(WithdrawDestinationValidationCode::VALID) => "valid".to_owned(),
27 Some(WithdrawDestinationValidationCode::INVALID_ADDRESS) => "invalid_address".to_owned(),
28 Some(WithdrawDestinationValidationCode::UNSUPPORTED_CHAIN) => {
29 "unsupported_chain".to_owned()
30 }
31 Some(WithdrawDestinationValidationCode::POLYESTER_SMART_ACCOUNT) => {
32 "polyester_smart_account".to_owned()
33 }
34 Some(WithdrawDestinationValidationCode::TOKEN_CONTRACT) => "token_contract".to_owned(),
35 Some(WithdrawDestinationValidationCode::DENYLISTED_ADDRESS) => {
36 "denylisted_address".to_owned()
37 }
38 None => format!("unknown_code_{}", value.to_i32()),
39 }
40}
41
42pub fn withdraw_destination_validation_from_proto(
43 msg: &ValidateWithdrawDestinationResponse,
44) -> WithdrawDestinationValidation {
45 WithdrawDestinationValidation {
46 valid: msg.valid,
47 code: withdraw_destination_validation_code_label(&msg.code),
48 message: msg.message.clone(),
49 canonical_destination_address: msg.canonical_destination_address.clone(),
50 }
51}
52
53pub fn deposit_address_from_proto(msg: &ProtoDepositAddress) -> DepositAddress {
54 DepositAddress {
55 chain_id: msg.chain_id,
56 deposit_address: msg.deposit_address.clone(),
57 }
58}
59
60pub fn deposit_addresses_list_from_proto(
61 msg: &ListDepositAddressesResponse,
62) -> DepositAddressesList {
63 DepositAddressesList {
64 addresses: msg
65 .deposit_addresses
66 .iter()
67 .map(deposit_address_from_proto)
68 .collect(),
69 }
70}
71
72pub fn create_deposit_address_from_proto(
73 msg: &CreateDepositAddressResponse,
74) -> Result<DepositAddress> {
75 let address = msg.deposit_address.as_option().ok_or_else(|| {
76 Error::transport("invalid CreateDepositAddress response: missing deposit_address")
77 })?;
78 if address.deposit_address.trim().is_empty() {
79 return Err(Error::transport(
80 "invalid CreateDepositAddress response: empty deposit address",
81 ));
82 }
83 Ok(deposit_address_from_proto(address))
84}
85
86pub fn withdraw_intent_from_proto(
87 msg: &CreateTradingWithdrawResponse,
88) -> Result<WithdrawIntentResult> {
89 if msg.intent_id.trim().is_empty() {
90 return Err(Error::transport(
91 "invalid CreateTradingWithdraw response: missing intent_id",
92 ));
93 }
94 Ok(WithdrawIntentResult {
95 intent_id: msg.intent_id.clone(),
96 status: String::new(),
97 flow_id: String::new(),
98 })
99}
100
101pub fn withdraw_intent_from_wallet_proto(
102 msg: &CreateWalletTradingWithdrawResponse,
103) -> Result<WithdrawIntentResult> {
104 if msg.intent_id.trim().is_empty() {
105 return Err(Error::transport(
106 "invalid CreateWalletTradingWithdraw response: missing intent_id",
107 ));
108 }
109 Ok(WithdrawIntentResult {
110 intent_id: msg.intent_id.clone(),
111 status: String::new(),
112 flow_id: String::new(),
113 })
114}
115
116pub fn internal_transfer_from_proto(
117 msg: &CreateInternalTransferResponse,
118) -> Result<InternalTransferResult> {
119 if msg.request_id.trim().is_empty() || msg.transfer_id.trim().is_empty() {
120 return Err(Error::transport(
121 "invalid CreateInternalTransfer response: missing request_id or transfer_id",
122 ));
123 }
124 let asset_id = msg.asset_id;
125 let quantity = msg.amount_e18.as_option().and_then(|u| {
126 decode_asset_amount_u128(
127 u.hi,
128 u.lo,
129 Some(LEDGER_SCALE),
130 QuantityDomain::LedgerE18,
131 Some(asset_id),
132 )
133 });
134 Ok(InternalTransferResult {
135 request_id: msg.request_id.clone(),
136 transfer_id: msg.transfer_id.clone(),
137 asset_id,
138 asset_code: msg.asset_code.clone(),
139 quantity,
140 })
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::proto::polyester::r#type::v1::U128;
147
148 #[test]
149 fn deposit_addresses_list_maps_rows() {
150 let msg = ListDepositAddressesResponse {
151 deposit_addresses: vec![ProtoDepositAddress {
152 chain_id: 1,
153 deposit_address: "0xabc".into(),
154 ..Default::default()
155 }],
156 ..Default::default()
157 };
158 let list = deposit_addresses_list_from_proto(&msg);
159 assert_eq!(list.addresses.len(), 1);
160 assert_eq!(list.addresses[0].chain_id, 1);
161 assert_eq!(list.addresses[0].deposit_address, "0xabc");
162 }
163
164 #[test]
165 fn create_deposit_address_rejects_missing_required_entity() {
166 let err = create_deposit_address_from_proto(&CreateDepositAddressResponse::default())
167 .expect_err("missing deposit address must fail closed");
168 assert!(err.to_string().contains("missing deposit_address"));
169 }
170
171 #[test]
172 fn internal_transfer_maps_u128_quantity() {
173 let msg = CreateInternalTransferResponse {
174 request_id: "req".into(),
175 transfer_id: "xfer".into(),
176 asset_id: 7,
177 asset_code: "USDT".into(),
178 amount_e18: U128 {
179 hi: 0,
180 lo: 1_000_000_000_000_000_000,
181 ..Default::default()
182 }
183 .into(),
184 ..Default::default()
185 };
186 let result = internal_transfer_from_proto(&msg).unwrap();
187 assert_eq!(result.request_id, "req");
188 assert_eq!(result.transfer_id, "xfer");
189 assert_eq!(result.asset_id, 7);
190 assert_eq!(
191 result.quantity.as_ref().unwrap().as_scaled(),
192 1_000_000_000_000_000_000
193 );
194 }
195
196 #[test]
197 fn withdraw_intent_maps_id() {
198 let msg = CreateTradingWithdrawResponse {
199 intent_id: "intent-1".into(),
200 ..Default::default()
201 };
202 assert_eq!(
203 withdraw_intent_from_proto(&msg).unwrap().intent_id,
204 "intent-1"
205 );
206 }
207
208 #[test]
209 fn withdraw_destination_validation_maps_codes() {
210 let cases = [
211 (
212 WithdrawDestinationValidationCode::RESULT_UNSPECIFIED,
213 "unspecified",
214 ),
215 (WithdrawDestinationValidationCode::VALID, "valid"),
216 (
217 WithdrawDestinationValidationCode::INVALID_ADDRESS,
218 "invalid_address",
219 ),
220 (
221 WithdrawDestinationValidationCode::UNSUPPORTED_CHAIN,
222 "unsupported_chain",
223 ),
224 (
225 WithdrawDestinationValidationCode::POLYESTER_SMART_ACCOUNT,
226 "polyester_smart_account",
227 ),
228 (
229 WithdrawDestinationValidationCode::TOKEN_CONTRACT,
230 "token_contract",
231 ),
232 (
233 WithdrawDestinationValidationCode::DENYLISTED_ADDRESS,
234 "denylisted_address",
235 ),
236 ];
237 for (code, expected) in cases {
238 let msg = ValidateWithdrawDestinationResponse {
239 valid: code == WithdrawDestinationValidationCode::VALID,
240 code: code.into(),
241 message: "msg".into(),
242 canonical_destination_address: if code == WithdrawDestinationValidationCode::VALID {
243 "0xabc".into()
244 } else {
245 String::new()
246 },
247 ..Default::default()
248 };
249 let got = withdraw_destination_validation_from_proto(&msg);
250 assert_eq!(got.code, expected);
251 assert_eq!(got.message, "msg");
252 }
253 let unknown = ValidateWithdrawDestinationResponse {
254 code: buffa::EnumValue::from(99),
255 ..Default::default()
256 };
257 assert_eq!(
258 withdraw_destination_validation_from_proto(&unknown).code,
259 "unknown_code_99"
260 );
261 }
262
263 #[test]
264 fn singular_mutations_reject_empty_success_responses() {
265 assert!(withdraw_intent_from_proto(&CreateTradingWithdrawResponse::default()).is_err());
266 assert!(
267 withdraw_intent_from_wallet_proto(&CreateWalletTradingWithdrawResponse::default())
268 .is_err()
269 );
270 assert!(internal_transfer_from_proto(&CreateInternalTransferResponse::default()).is_err());
271 }
272}