m10_sdk/types/
transfer.rs

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    // TODO @sadroeck - fixme
307    #[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}