cap-standards 0.2.0

A collection of transaction standards that can be used with Cap.
use std::{collections::HashMap, convert::TryInto};

use candid::{Nat, Principal};
use cap_sdk::{DetailValue, IndefiniteEvent, IntoEvent, TryFromEvent};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug)]
pub struct XTCTransactionDetailsERC20 {
    to: Principal,
    amount: Nat,
    fee: Nat,
    index: Nat,
}

#[derive(Debug, Error)]
pub enum XTCTransactionDetailsERC20Error {
    #[error("missing key {0}")]
    MissingKey(String),
    #[error("couldn't convert item with key {0} to DetailValue")]
    ConversionError(String),
    #[error("invalid operation {0}")]
    InvalidOperation(String),
}

#[cfg(feature = "sdk-impls")]
impl IntoEvent for XTCTransactionDetailsERC20 {
    fn details(&self) -> Vec<(String, DetailValue)> {
        vec![
            ("to".into(), self.to.into()),
            ("amount".into(), self.amount.clone().into()),
            ("fee".into(), self.fee.clone().into()),
            ("index".into(), self.index.clone().into()),
        ]
    }
}

#[cfg(feature = "sdk-impls")]
trait MapFailed<T, E> {
    fn map_failure(self, key: &'static str) -> Result<T, E>;
}

#[cfg(feature = "sdk-impls")]
impl<T, O> MapFailed<T, XTCTransactionDetailsERC20Error> for Result<T, O> {
    fn map_failure(self, key: &'static str) -> Result<T, XTCTransactionDetailsERC20Error> {
        self.map_err(|_| XTCTransactionDetailsERC20Error::ConversionError(key.to_owned()))
    }
}

#[cfg(feature = "sdk-impls")]
fn try_get_and_clone(
    map: &HashMap<String, DetailValue>,
    key: &'static str,
) -> Result<DetailValue, XTCTransactionDetailsERC20Error> {
    if let Some(item) = map.get(key) {
        Ok(item.clone())
    } else {
        Err(XTCTransactionDetailsERC20Error::MissingKey(key.to_owned()))
    }
}

#[cfg(feature = "sdk-impls")]
impl TryFromEvent for XTCTransactionDetailsERC20 {
    type Error = XTCTransactionDetailsERC20Error;

    fn try_from_event(
        event: impl Into<IndefiniteEvent>,
    ) -> Result<Self, XTCTransactionDetailsERC20Error> {
        let details = event.into().details;

        let map = details.iter().cloned().collect::<HashMap<_, _>>();

        Ok(Self {
            to: try_get_and_clone(&map, "to")?
                .try_into()
                .map_failure("to")?,
            amount: try_get_and_clone(&map, "amount")?
                .try_into()
                .map_failure("amount")?,
            fee: try_get_and_clone(&map, "fee")?
                .try_into()
                .map_failure("fee")?,
            index: try_get_and_clone(&map, "index")?
                .try_into()
                .map_failure("index")?,
        })
    }
}

pub struct XTCTransactionDetailsLegacy {
    pub fee: Nat,
    pub cycles: Nat,
    pub kind: XTCTransactionKindLegacy,
}

#[cfg(feature = "sdk-impls")]
impl IntoEvent for XTCTransactionKindLegacy {
    fn operation(&self) -> Option<&'static str> {
        Some(match *self {
            Self::Transfer { .. } => "transfer",
            Self::TransferFrom { .. } => "transfer_from",
            Self::Approve { .. } => "approve",
            Self::Burn { .. } => "burn",
            Self::Mint { .. } => "mint",
            Self::CanisterCalled { .. } => "canister_called",
            Self::CanisterCreated { .. } => "canister_created",
        })
    }

    fn details(&self) -> Vec<(String, DetailValue)> {
        match self {
            Self::Transfer { from, to } => {
                vec![
                    ("to".to_owned(), to.clone().into()),
                    ("from".to_owned(), from.clone().into()),
                ]
            }
            Self::TransferFrom { from, to } => {
                vec![
                    ("to".to_owned(), to.clone().into()),
                    ("from".to_owned(), from.clone().into()),
                ]
            }
            Self::Approve { from, to } => {
                vec![
                    ("to".to_owned(), to.clone().into()),
                    ("from".to_owned(), from.clone().into()),
                ]
            }
            Self::Burn { from, to } => {
                vec![
                    ("to".to_owned(), to.clone().into()),
                    ("from".to_owned(), from.clone().into()),
                ]
            }
            Self::Mint { to } => {
                vec![("to".to_owned(), to.clone().into())]
            }
            Self::CanisterCalled { from, to, method } => {
                vec![
                    ("to".to_owned(), to.clone().into()),
                    ("from".to_owned(), from.clone().into()),
                    ("method".to_owned(), method.clone().into()),
                ]
            }
            Self::CanisterCreated { from, canister } => {
                vec![
                    ("canister".to_owned(), canister.clone().into()),
                    ("from".to_owned(), from.clone().into()),
                ]
            }
        }
    }
}

#[cfg(feature = "sdk-impls")]
impl TryFromEvent for XTCTransactionKindLegacy {
    type Error = XTCTransactionDetailsERC20Error;

    fn try_from_event(event: impl Into<IndefiniteEvent>) -> Result<Self, Self::Error> {
        let event = event.into();

        let map = event.details.iter().cloned().collect::<HashMap<_, _>>();

        Ok(match event.operation.as_str() {
            "transfer" => XTCTransactionKindLegacy::Transfer {
                to: try_get_and_clone(&map, "to")?
                    .try_into()
                    .map_failure("to")?,
                from: try_get_and_clone(&map, "from")?
                    .try_into()
                    .map_failure("from")?,
            },
            "transfer_from" => XTCTransactionKindLegacy::TransferFrom {
                to: try_get_and_clone(&map, "to")?
                    .try_into()
                    .map_failure("to")?,
                from: try_get_and_clone(&map, "from")?
                    .try_into()
                    .map_failure("from")?,
            },
            "approve" => XTCTransactionKindLegacy::Approve {
                to: try_get_and_clone(&map, "to")?
                    .try_into()
                    .map_failure("to")?,
                from: try_get_and_clone(&map, "from")?
                    .try_into()
                    .map_failure("from")?,
            },
            "burn" => XTCTransactionKindLegacy::Burn {
                to: try_get_and_clone(&map, "to")?
                    .try_into()
                    .map_failure("to")?,
                from: try_get_and_clone(&map, "from")?
                    .try_into()
                    .map_failure("from")?,
            },
            "mint" => XTCTransactionKindLegacy::Mint {
                to: try_get_and_clone(&map, "to")?
                    .try_into()
                    .map_failure("to")?,
            },
            "canister_called" => XTCTransactionKindLegacy::CanisterCalled {
                method: try_get_and_clone(&map, "method")?
                    .try_into()
                    .map_failure("method")?,
                to: try_get_and_clone(&map, "to")?
                    .try_into()
                    .map_failure("to")?,
                from: try_get_and_clone(&map, "from")?
                    .try_into()
                    .map_failure("from")?,
            },
            "canister_created" => XTCTransactionKindLegacy::CanisterCreated {
                canister: try_get_and_clone(&map, "canister")?
                    .try_into()
                    .map_failure("canister")?,
                from: try_get_and_clone(&map, "from")?
                    .try_into()
                    .map_failure("from")?,
            },
            operation => {
                return Err(XTCTransactionDetailsERC20Error::InvalidOperation(
                    operation.to_owned(),
                ))
            }
        })
    }
}

#[derive(Serialize, Deserialize)]
pub enum XTCTransactionKindLegacy {
    Transfer {
        from: Principal,
        to: Principal,
    },

    Mint {
        to: Principal,
    },

    Burn {
        from: Principal,
        to: Principal,
    },

    CanisterCalled {
        from: Principal,
        to: Principal,
        method: String,
    },

    CanisterCreated {
        from: Principal,
        canister: Principal,
    },

    TransferFrom {
        from: Principal,
        to: Principal,
    },

    Approve {
        from: Principal,
        to: Principal,
    },
}