1use crate::account::AccountId;
2use crate::error::{M10Error, M10Result};
3use crate::types::TxId;
4use crate::{EnhancedTransfer, EnhancedTransferStep, TransactionExt};
5use core::convert::{From, TryFrom};
6use core::result::Result;
7use core::time::Duration;
8use m10_protos::prost::Any;
9use m10_protos::sdk::transaction_data::Data;
10use m10_protos::{sdk, MetadataExt, MetadataType};
11use serde::{Serialize, Serializer};
12use serde_with::{serde_as, SerializeAs};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15#[serde_as]
16#[derive(Clone, Debug, serde::Serialize)]
17pub struct TransferStep {
18 pub from: AccountId,
19 pub to: AccountId,
20 pub amount: u64,
21 #[serde_as(as = "Vec<AsMetadata>")]
22 pub metadata: Vec<Any>,
23}
24
25struct AsMetadata;
26
27impl SerializeAs<Any> for AsMetadata {
28 fn serialize_as<S: Serializer>(data: &Any, serializer: S) -> Result<S::Ok, S::Error> {
29 let metatdata = if let Some(sdk::metadata::Memo { plaintext }) = data.protobuf() {
30 format!("Memo: {plaintext}")
31 } else if let Some(sdk::metadata::Fee {}) = data.protobuf() {
32 "Fee".to_string()
33 } else if let Some(sdk::metadata::Withdraw { bank_account_id }) = data.protobuf() {
34 format!("Withdraw {bank_account_id}")
35 } else if let Some(sdk::metadata::Deposit { bank_account_id }) = data.protobuf() {
36 format!("Deposit {bank_account_id}")
37 } else if let Some(sdk::metadata::Attachment { object_id, .. }) = data.protobuf() {
38 format!("Attachment: {object_id}")
39 } else if let Some(sdk::metadata::Contract { endorsements, .. }) = data.protobuf() {
40 format!("Contract with {} endorsments", endorsements.len())
41 } else if let Some(sdk::metadata::SelfTransfer {
42 from_account_name,
43 to_account_name,
44 }) = data.protobuf()
45 {
46 format!("Self transfer from {from_account_name} to {to_account_name}")
47 } else if let Some(sdk::metadata::RebalanceTransfer {}) = data.protobuf() {
48 "Rebalance transfer".to_string()
49 } else if let Some(sdk::metadata::TokenWithdraw {}) = data.protobuf() {
50 "Tokenization".to_string()
51 } else if let Some(sdk::metadata::OfflineTransfer { input }) = data.protobuf() {
52 format!("Offline payment from {input}")
53 } else {
54 "Unknown".to_string()
55 };
56
57 serializer.serialize_str(&metatdata)
58 }
59}
60
61#[cfg(feature = "format")]
62impl std::fmt::Display for TransferStep {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 let Self {
65 from,
66 to,
67 amount,
68 metadata,
69 } = self;
70 write!(f, "Step{{ from={from} to={to} amount={amount} metadata=[",)?;
71 for m in metadata {
72 write!(f, "{},", m.type_url)?;
73 }
74 write!(f, "] }}")
75 }
76}
77
78impl MetadataExt for TransferStep {
79 fn with_type<M: MetadataType>(&self) -> Option<&[u8]> {
80 self.metadata
81 .iter()
82 .find(|a| a.type_url == M::TYPE_URL)
83 .map(|a| a.value.as_slice())
84 }
85}
86
87impl TryFrom<sdk::TransferStep> for TransferStep {
88 type Error = M10Error;
89
90 fn try_from(step: sdk::TransferStep) -> Result<Self, Self::Error> {
91 let sdk::TransferStep {
92 from_account_id,
93 to_account_id,
94 amount,
95 metadata,
96 } = step;
97 Ok(TransferStep {
98 from: AccountId::try_from_be_slice(&from_account_id)?,
99 to: AccountId::try_from_be_slice(&to_account_id)?,
100 amount,
101 metadata,
102 })
103 }
104}
105
106#[cfg_attr(feature = "format", derive(parse_display::Display))]
107#[derive(Clone, Debug, Copy, Serialize, PartialEq, Eq)]
108pub enum TransferStatus {
109 Pending,
110 Accepted,
111 Rejected,
112 Expired,
113}
114
115impl From<sdk::finalized_transfer::TransferState> for TransferStatus {
116 fn from(status: sdk::finalized_transfer::TransferState) -> Self {
117 match status {
118 sdk::finalized_transfer::TransferState::Pending => TransferStatus::Pending,
119 sdk::finalized_transfer::TransferState::Accepted => TransferStatus::Accepted,
120 sdk::finalized_transfer::TransferState::Rejected => TransferStatus::Rejected,
121 sdk::finalized_transfer::TransferState::Expired => TransferStatus::Expired,
122 }
123 }
124}
125
126#[serde_as]
127#[derive(Clone, Debug, Serialize)]
128pub struct Transfer {
129 pub tx_id: TxId,
130 #[serde_as(as = "serde_with::hex::Hex")]
131 pub context_id: Vec<u8>,
132 pub timestamp: SystemTime,
133 pub steps: Vec<TransferStep>,
134 pub success: bool,
135 pub status: TransferStatus,
136}
137
138#[cfg(feature = "format")]
139impl std::fmt::Display for Transfer {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 let Self {
142 tx_id,
143 context_id,
144 timestamp,
145 steps,
146 success,
147 status,
148 } = self;
149 write!(f, "Transfer{{ tx_id={tx_id} context_id={} timestamp={timestamp:?} success={success} status={status} steps=[", hex::encode(context_id))?;
150 for step in steps {
151 write!(f, "{step},")?;
152 }
153 write!(f, "] }}")
154 }
155}
156
157impl TryFrom<sdk::FinalizedTransfer> for Transfer {
158 type Error = M10Error;
159
160 fn try_from(transfer: sdk::FinalizedTransfer) -> Result<Self, Self::Error> {
161 Ok(Transfer {
162 tx_id: transfer.tx_id,
163 context_id: transfer.context_id,
164 success: transfer.error.is_none(),
165 timestamp: UNIX_EPOCH + Duration::from_micros(transfer.timestamp),
166 steps: transfer
167 .transfer_steps
168 .into_iter()
169 .map(TransferStep::try_from)
170 .collect::<M10Result<_>>()?,
171 status: TransferStatus::from(
172 sdk::finalized_transfer::TransferState::try_from(transfer.state)
173 .map_err(|_| M10Error::InvalidTransaction)?,
174 ),
175 })
176 }
177}
178
179impl TryFrom<sdk::FinalizedTransaction> for Transfer {
180 type Error = M10Error;
181
182 fn try_from(txn: sdk::FinalizedTransaction) -> Result<Self, Self::Error> {
183 let response = txn.response.as_ref().ok_or(M10Error::InvalidTransaction)?;
184 let context_id = txn
185 .request
186 .as_ref()
187 .ok_or(M10Error::InvalidTransaction)?
188 .context_id
189 .clone();
190 let success = response.error.is_none();
191 let tx_id = response.tx_id;
192 let timestamp = UNIX_EPOCH + Duration::from_micros(response.timestamp);
193 match txn.data().ok_or(M10Error::InvalidTransaction)? {
194 Data::Transfer(transfer) => Ok(Self {
195 tx_id,
196 context_id,
197 timestamp,
198 steps: transfer
199 .transfer_steps
200 .iter()
201 .cloned()
202 .map(TransferStep::try_from)
203 .collect::<M10Result<_>>()?,
204 success,
205 status: TransferStatus::Accepted,
206 }),
207 Data::InitiateTransfer(transfer) => Ok(Self {
208 tx_id,
209 context_id,
210 timestamp,
211 steps: transfer
212 .transfer_steps
213 .iter()
214 .cloned()
215 .map(TransferStep::try_from)
216 .collect::<M10Result<_>>()?,
217 success,
218 status: TransferStatus::Pending,
219 }),
220 Data::CommitTransfer(commit) => {
221 let status = if success {
222 TransferStatus::from(
223 sdk::finalized_transfer::TransferState::try_from(commit.new_state)
224 .map_err(|_| M10Error::InvalidTransaction)?,
225 )
226 } else {
227 TransferStatus::Pending
228 };
229 let transfer = response
230 .transfer_committed
231 .as_ref()
232 .ok_or(M10Error::InvalidTransaction)?;
233 Ok(Self {
234 tx_id,
235 context_id,
236 timestamp,
237 steps: transfer
238 .transfer_steps
239 .iter()
240 .cloned()
241 .map(TransferStep::try_from)
242 .collect::<M10Result<_>>()?,
243 success,
244 status,
245 })
246 }
247 _ => Err(M10Error::InvalidTransaction),
248 }
249 }
250}
251
252impl MetadataExt for Transfer {
253 fn with_type<M: MetadataType>(&self) -> Option<&[u8]> {
254 self.steps.iter().find_map(TransferStep::with_type::<M>)
255 }
256}
257
258#[derive(Clone, Debug, Serialize)]
259pub struct ExpandedTransfer {
260 pub tx_id: TxId,
261 pub context_id: Vec<u8>,
262 pub timestamp: SystemTime,
263 pub steps: Vec<ExpandedTransferStep>,
264 pub success: bool,
265 pub status: TransferStatus,
266}
267
268#[cfg(feature = "format")]
269impl std::fmt::Display for ExpandedTransfer {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 let Self {
272 tx_id,
273 context_id,
274 timestamp,
275 steps,
276 success,
277 status,
278 } = self;
279 write!(f, "Transfer{{ tx_id={tx_id} context_id={} timestamp={timestamp:?} success={success} status={status} steps=[", hex::encode(context_id))?;
280 for step in steps {
281 write!(f, "{step},")?;
282 }
283 write!(f, "] }}")
284 }
285}
286
287#[cfg_attr(feature = "format", derive(parse_display::Display))]
288#[cfg_attr(
289 feature = "format",
290 display("Account{{ id={id} name={public_name} image={profile_image_url} currency={code}({decimals}) }}")
291)]
292#[derive(Clone, Debug, Serialize)]
293pub struct ExpandedAccount {
294 pub id: AccountId,
295 pub public_name: String,
296 pub profile_image_url: String,
297 pub code: String,
298 pub decimals: u32,
299}
300
301#[derive(Clone, Debug, Serialize)]
302pub struct ExpandedTransferStep {
303 pub from: ExpandedAccount,
304 pub to: ExpandedAccount,
305 pub amount: u64,
306 #[serde(skip)]
308 pub metadata: Vec<Any>,
309}
310
311#[cfg(feature = "format")]
312impl std::fmt::Display for ExpandedTransferStep {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 let Self {
315 from,
316 to,
317 amount,
318 metadata,
319 } = self;
320 write!(f, "Step{{ from={from} to={to} amount={amount} metadata=[",)?;
321 for m in metadata {
322 write!(f, "{},", m.type_url)?;
323 }
324 write!(f, "] }}")
325 }
326}
327
328impl TryFrom<(EnhancedTransferStep, sdk::TransferStep)> for ExpandedTransferStep {
329 type Error = M10Error;
330
331 fn try_from(
332 (step, raw): (EnhancedTransferStep, sdk::TransferStep),
333 ) -> Result<Self, Self::Error> {
334 let from = step.from.ok_or(M10Error::InvalidTransaction)?;
335 let to = step.to.ok_or(M10Error::InvalidTransaction)?;
336 Ok(Self {
337 from: ExpandedAccount {
338 id: AccountId::try_from_be_slice(&from.account_id)?,
339 public_name: from.public_name,
340 profile_image_url: from.profile_image_url,
341 code: from.code,
342 decimals: from.decimal_places,
343 },
344 to: ExpandedAccount {
345 id: AccountId::try_from_be_slice(&to.account_id)?,
346 public_name: to.public_name,
347 profile_image_url: to.profile_image_url,
348 code: to.code,
349 decimals: to.decimal_places,
350 },
351 amount: raw.amount,
352 metadata: raw.metadata,
353 })
354 }
355}
356
357impl TryFrom<EnhancedTransfer> for ExpandedTransfer {
358 type Error = M10Error;
359
360 fn try_from(transfer: EnhancedTransfer) -> Result<Self, Self::Error> {
361 Ok(Self {
362 tx_id: transfer.transfer.tx_id,
363 context_id: transfer.transfer.context_id,
364 timestamp: UNIX_EPOCH + Duration::from_micros(transfer.transfer.timestamp),
365 steps: transfer
366 .enhanced_steps
367 .into_iter()
368 .zip(transfer.transfer.transfer_steps.into_iter())
369 .map(ExpandedTransferStep::try_from)
370 .collect::<M10Result<_>>()?,
371 success: transfer.transfer.error.is_none(),
372 status: TransferStatus::from(
373 sdk::finalized_transfer::TransferState::try_from(transfer.transfer.state)
374 .map_err(|_| M10Error::InvalidTransaction)?,
375 ),
376 })
377 }
378}
379
380#[derive(serde::Serialize)]
381pub struct TransferView {
382 #[serde(flatten)]
383 pub transfer: TransferData,
384
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub tx_sender: Option<String>,
387}
388
389#[derive(serde::Serialize)]
390#[serde(untagged)]
391pub enum TransferData {
392 Basic(Transfer),
393 Expanded(ExpandedTransfer),
394}