diff --git a/magic_bridge/ic/src/dip721_proxy/Cargo.toml b/magic_bridge/ic/src/dip721_proxy/Cargo.toml index 082f73a6..3c75a5ec 100644 --- a/magic_bridge/ic/src/dip721_proxy/Cargo.toml +++ b/magic_bridge/ic/src/dip721_proxy/Cargo.toml @@ -4,12 +4,17 @@ version = "0.1.0" edition = "2021" [lib] -path = "src/lib.rs" +path = "src/main.rs" crate-type = ["cdylib"] [dependencies] ic-kit = "0.4.4" ic-cdk = "0.4.0" -ic-cdk-macros = "0.3" +candid = "0.7.4" +ic-cdk-macros = "0.4" hex = "0.4.3" -serde = "1.0.116" \ No newline at end of file +sha3 = "0.9.1" +async-trait = "0.1.51" +serde = "1.0.130" +serde_bytes = "0.11.5" +num-bigint = "0.4.3" \ No newline at end of file diff --git a/magic_bridge/ic/src/dip721_proxy/dip721_proxy.did b/magic_bridge/ic/src/dip721_proxy/dip721_proxy.did index 3f19151c..70fb3d5b 100644 --- a/magic_bridge/ic/src/dip721_proxy/dip721_proxy.did +++ b/magic_bridge/ic/src/dip721_proxy/dip721_proxy.did @@ -1,4 +1,5 @@ type Result = variant { Ok : nat; Err : TxError }; +type Result_1 = variant { Ok : vec record { text; nat }; Err : text }; type TxError = variant { InsufficientAllowance; InsufficientBalance; @@ -6,10 +7,15 @@ type TxError = variant { Unauthorized; LedgerTrap; ErrorTo; + Other : text; BlockUsed; AmountTooSmall; - Other : text; }; -service : () -> { +service : { + burn : (principal, principal, nat) -> (Result); + get_all_token_balance : () -> (Result_1); + get_balance : (principal) -> (opt nat); handle_message : (principal, nat, vec nat) -> (Result); + mint : (principal, nat, vec nat) -> (Result); + widthdraw : (principal, principal) -> (Result); } \ No newline at end of file diff --git a/magic_bridge/ic/src/dip721_proxy/src/api/burn.rs b/magic_bridge/ic/src/dip721_proxy/src/api/burn.rs new file mode 100644 index 00000000..e3c81f8f --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/api/burn.rs @@ -0,0 +1,73 @@ +use ic_kit::candid::candid_method; +use ic_kit::{ic, macros::update}; + +use crate::common::dip721::Dip721; +use crate::common::tera::Tera; +use crate::proxy::{ToNat, ERC721_ADDRESS_ETH, STATE, TERA_ADDRESS}; +use ic_cdk::export::candid::{Nat, Principal}; + +use crate::common::types::{EthereumAddr, TokendId, TxError, TxReceipt}; + +// should we allow users to just pass in the corresponding eth_addr on ETH +// or should we use our magic_bridge to check if a key exists +#[update(name = "burn")] +#[candid_method(update, rename = "burn")] +async fn burn(token_id: TokendId, eth_addr: EthereumAddr, amount: Nat) -> TxReceipt { + let caller = ic::caller(); + let self_id = ic::id(); + + if (token_id.name().await).is_err() { + return Err(TxError::Other(format!( + "Token {} canister is not responding!", + token_id.to_string(), + ))); + } + + let erc721_addr_hex = ERC721_ADDRESS_ETH.trim_start_matches("0x"); + let erc721_addr_pid = Principal::from_slice(&hex::decode(erc721_addr_hex).unwrap()); + + let transfer_from = token_id + .transfer_from(caller, self_id, amount.clone()) + .await; + + match transfer_from { + Ok(_) => { + STATE.with(|s| s.add_balance(caller, token_id, amount.clone())); + + let burn = token_id.burn(amount.clone()).await; + + match burn { + Ok(burn_txn_id) => { + let tera_id = Principal::from_text(TERA_ADDRESS).unwrap(); + let payload = [ + token_id.clone().to_nat(), + eth_addr.clone().to_nat(), + amount.clone(), + ] + .to_vec(); + + if tera_id.send_message(erc721_addr_pid, payload).await.is_err() { + return Err(TxError::Other(format!( + "Sending message to L1 failed with caller {:?}!", + caller.to_string() + ))); + } + + // there could be an underflow here + // like negative balance + let current_balance = + STATE.with(|s| s.get_balance(caller, token_id).unwrap_or(Nat::from(0))); + + STATE.with(|s| { + s.update_balance(caller, token_id, current_balance - amount.clone()) + }); + return Ok(burn_txn_id); + } + Err(error) => { + return Err(error); + } + }; + } + Err(error) => Err(error), + } +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/api/get_balance.rs b/magic_bridge/ic/src/dip721_proxy/src/api/get_balance.rs new file mode 100644 index 00000000..df543b6b --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/api/get_balance.rs @@ -0,0 +1,21 @@ +use ic_kit::{ + candid::{candid_method, Nat}, + ic, + macros::update, +}; + +use crate::{common::types::TokendId, proxy::STATE}; + +#[update(name = "get_balance")] +#[candid_method(update, rename = "get_balance")] +pub async fn get_balance(token_id: TokendId) -> Option { + let caller = ic::caller(); + STATE.with(|s| s.get_balance(caller, token_id)) +} + +#[update(name = "get_all_token_balance")] +#[candid_method(update, rename = "get_all_token_balance")] +pub async fn get_all_balances() -> Result, String> { + let caller = ic::caller(); + STATE.with(|s| s.get_all_balances(caller)) +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/api/handle_message.rs b/magic_bridge/ic/src/dip721_proxy/src/api/handle_message.rs new file mode 100644 index 00000000..30529ebe --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/api/handle_message.rs @@ -0,0 +1,43 @@ +use crate::api::mint::mint; +use ic_kit::candid::candid_method; +use ic_kit::{ic, macros::update}; + +use ic_cdk::export::candid::{Nat, Principal}; + +use crate::common::types::{EthereumAddr, MagicResponse, Nonce, TokenType, TxError, TxReceipt}; +use crate::proxy::{ERC721_ADDRESS_ETH, MAGIC_ADDRESS_IC}; + +#[update(name = "handle_message")] +#[candid_method(update, rename = "handle_message")] +async fn handler(eth_addr: EthereumAddr, nonce: Nonce, payload: Vec) -> TxReceipt { + let erc721_addr_hex = hex::encode(eth_addr); + + if !(erc721_addr_hex + == ERC721_ADDRESS_ETH + .trim_start_matches("0x") + .to_ascii_lowercase()) + { + return Err(TxError::Other(format!( + "ERC721 Contract Address is inccorrect: {}", + erc721_addr_hex + ))); + } + + let magic_ic_addr_pid = Principal::from_text(MAGIC_ADDRESS_IC).unwrap(); + + let create_canister: (MagicResponse,) = + match ic::call(magic_ic_addr_pid, "create", (TokenType::DIP721, &payload)).await { + Ok(res) => res, + Err((code, err)) => { + return Err(TxError::Other(format!( + "RejectionCode: {:?}\n{}", + code, err + ))) + } + }; + + match create_canister { + (Ok(token_id),) => mint(token_id, nonce, payload).await, + (Err(error),) => Err(TxError::Other(error.to_string())), + } +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/api/init.rs b/magic_bridge/ic/src/dip721_proxy/src/api/init.rs new file mode 100644 index 00000000..d76e692b --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/api/init.rs @@ -0,0 +1,8 @@ +use ic_kit::{ic, macros::*}; + +use crate::proxy::STATE; + +#[init] +pub fn init() { + STATE.with(|s| s.controllers.borrow_mut().push(ic::caller())); +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/api/mint.rs b/magic_bridge/ic/src/dip721_proxy/src/api/mint.rs new file mode 100644 index 00000000..ce8bba14 --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/api/mint.rs @@ -0,0 +1,87 @@ +use ic_kit::candid::candid_method; +use ic_kit::{ic, macros::update}; + +use crate::common::dip721::Dip721; +use crate::common::tera::Tera; +use crate::common::utils::Keccak256HashFn; +use crate::proxy::{FromNat, ToNat, ERC721_ADDRESS_ETH, STATE, TERA_ADDRESS}; +use ic_cdk::export::candid::{Nat, Principal}; + +use crate::common::types::{ + IncomingMessageHashParams, Message, MessageStatus, Nonce, TokendId, TxError, TxReceipt, +}; + +#[update(name = "mint")] +#[candid_method(update, rename = "mint")] +pub async fn mint(token_id: TokendId, nonce: Nonce, payload: Vec) -> TxReceipt { + if (token_id.name().await).is_err() { + return Err(TxError::Other(format!( + "Token {} canister is not responding!", + token_id.to_string() + ))); + } + + let self_id = ic::id(); + let erc721_addr_hex = ERC721_ADDRESS_ETH.trim_start_matches("0x"); + let erc721_addr_pid = Principal::from_slice(&hex::decode(erc721_addr_hex).unwrap()); + + let msg_hash = Message.calculate_hash(IncomingMessageHashParams { + from: erc721_addr_pid.to_nat(), + to: self_id.to_nat(), + nonce: nonce.clone(), + payload: payload.clone(), + }); + + let msg_exists = STATE.with(|s| s.get_message(&msg_hash)); + + if let Some(status) = msg_exists { + match status { + MessageStatus::ConsumedNotMinted => (), + _ => { + return Err(TxError::Other(format!( + "Meesage {}: is already being consumed/minted!", + &msg_hash + ))); + } + } + } else { + let tera_id = Principal::from_text(TERA_ADDRESS).unwrap(); + if tera_id + .consume_message(erc721_addr_pid, nonce, payload.clone()) + .await + .is_err() + { + return Err(TxError::Other(format!( + "Consuming message from L1 failed with message {:?}!", + msg_hash, + ))); + } + STATE.with(|s| s.store_incoming_message(msg_hash.clone())); + }; + + STATE.with(|s| s.update_incoming_message_status(msg_hash.clone(), MessageStatus::Consuming)); + + let amount = Nat::from(payload[2].0.clone()); + let to = Principal::from_nat(payload[1].clone()); + + match token_id.mint(to, amount).await { + Ok(txn_id) => { + if STATE + .with(|s| s.remove_incoming_message(msg_hash.clone())) + .is_some() + { + return Ok(txn_id); + } + Err(TxError::Other(format!( + "Message {:?} does not exist!", + &msg_hash, + ))) + } + Err(error) => { + STATE.with(|s| { + s.update_incoming_message_status(msg_hash.clone(), MessageStatus::ConsumedNotMinted) + }); + Err(error) + } + } +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/api/mod.rs b/magic_bridge/ic/src/dip721_proxy/src/api/mod.rs new file mode 100644 index 00000000..48f0ed5a --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/api/mod.rs @@ -0,0 +1,7 @@ +mod burn; +mod get_balance; +mod handle_message; +mod init; +mod mint; +mod upgrade; +mod withdraw; diff --git a/magic_bridge/ic/src/dip721_proxy/src/api/upgrade.rs b/magic_bridge/ic/src/dip721_proxy/src/api/upgrade.rs new file mode 100644 index 00000000..f944f344 --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/api/upgrade.rs @@ -0,0 +1,22 @@ +use ic_kit::ic; +use ic_kit::macros::{post_upgrade, pre_upgrade}; + +use crate::common::types::StableProxyState; +use crate::proxy::STATE; + +#[pre_upgrade] +fn pre_upgrade() { + let stable_magic_state = STATE.with(|s| s.take_all()); + + ic::stable_store((stable_magic_state,)).expect("failed to messsage state"); +} + +#[post_upgrade] +fn post_upgrade() { + STATE.with(|s| s.clear_all()); + + let (stable_message_state,): (StableProxyState,) = + ic::stable_restore().expect("failed to restore stable messsage state"); + + STATE.with(|s| s.replace_all(stable_message_state)); +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/api/withdraw.rs b/magic_bridge/ic/src/dip721_proxy/src/api/withdraw.rs new file mode 100644 index 00000000..6785a1a1 --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/api/withdraw.rs @@ -0,0 +1,52 @@ +use ic_kit::{ + candid::{candid_method, Nat}, + ic, + macros::update, + Principal, +}; + +use crate::{ + common::{ + dip721::Dip721, + tera::Tera, + types::{EthereumAddr, TokendId, TxError, TxReceipt}, + }, + proxy::{ToNat, ERC721_ADDRESS_ETH, STATE, TERA_ADDRESS}, +}; + +/// withdraw left over balance if burn/mint fails +/// this will attempt to bridge the leftover balance +/// todo withdraw specific balance +#[update(name = "withdraw")] +#[candid_method(update, rename = "withdraw")] +pub async fn withdraw(token_id: TokendId, eth_addr: EthereumAddr, _amount: Nat) -> TxReceipt { + let caller = ic::caller(); + + if (token_id.name().await).is_err() { + return Err(TxError::Other(format!( + "Token {} canister is not responding!", + token_id.to_string(), + ))); + } + + let erc721_addr_hex = ERC721_ADDRESS_ETH.trim_start_matches("0x"); + let erc721_addr_pid = Principal::from_slice(&hex::decode(erc721_addr_hex).unwrap()); + + let get_balance = STATE.with(|s| s.get_balance(caller, token_id)); + if let Some(balance) = get_balance { + let payload = [eth_addr.clone().to_nat(), balance.clone()].to_vec(); + let tera_id = Principal::from_text(TERA_ADDRESS).unwrap(); + if tera_id.send_message(erc721_addr_pid, payload).await.is_err() { + return Err(TxError::Other(format!("Sending message to L1 failed!"))); + } + + let zero = Nat::from(0_u32); + STATE.with(|s| s.update_balance(caller, token_id, zero)); + } + + Err(TxError::Other(format!( + "No balance for caller {:?} in canister {:?}!", + caller.to_string(), + token_id.to_string(), + ))) +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/common/dip721.rs b/magic_bridge/ic/src/dip721_proxy/src/common/dip721.rs new file mode 100644 index 00000000..e44e76ca --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/common/dip721.rs @@ -0,0 +1,82 @@ +use async_trait::async_trait; +use ic_cdk::call; +use ic_cdk::export::candid::{Nat, Principal}; + +use crate::common::types::{TxError, TxReceipt}; + +#[async_trait] +pub trait Dip721 { + async fn burn(&self, amount: Nat) -> TxReceipt; + async fn name(&self) -> Result; + async fn mint(&self, to: Principal, amount: Nat) -> TxReceipt; + async fn transfer_from(&self, from: Principal, to: Principal, amount: Nat) -> TxReceipt; +} + +#[async_trait] +impl Dip721 for Principal { + async fn name(&self) -> Result { + let name: (String,) = match call(*self, "name", ()).await { + Ok(res) => res, + Err((code, err)) => { + return Err(TxError::Other(format!( + "RejectionCode: {:?}\n{}", + code, err + ))) + } + }; + + Ok(name.0) + } + + async fn mint(&self, to: Principal, amount: Nat) -> TxReceipt { + let mint: (TxReceipt,) = match call(*self, "mint", (to, amount)).await { + Ok(res) => res, + Err((code, err)) => { + return Err(TxError::Other(format!( + "RejectionCode: {:?}\n{}", + code, err + ))) + } + }; + + match mint { + (Ok(tx_id),) => Ok(tx_id), + (Err(error),) => Err(error), + } + } + + async fn burn(&self, amount: Nat) -> TxReceipt { + let burn_from: (TxReceipt,) = match call(*self, "burn", (amount,)).await { + Ok(res) => res, + Err((code, err)) => { + return Err(TxError::Other(format!( + "RejectionCode: {:?}\n{}", + code, err + ))) + } + }; + + match burn_from { + (Ok(tx_id),) => Ok(tx_id), + (Err(error),) => Err(error), + } + } + + async fn transfer_from(&self, from: Principal, to: Principal, amount: Nat) -> TxReceipt { + let transfer_from: (TxReceipt,) = + match call(*self, "transferFrom", (from, to, amount)).await { + Ok(res) => res, + Err((code, err)) => { + return Err(TxError::Other(format!( + "RejectionCode: {:?}\n{}", + code, err + ))) + } + }; + + match transfer_from { + (Ok(tx_id),) => Ok(tx_id), + (Err(error),) => Err(error), + } + } +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/common/mod.rs b/magic_bridge/ic/src/dip721_proxy/src/common/mod.rs new file mode 100644 index 00000000..6e08858c --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/common/mod.rs @@ -0,0 +1,4 @@ +pub mod dip721; +pub mod tera; +pub mod types; +pub mod utils; diff --git a/magic_bridge/ic/src/dip721_proxy/src/common/tera.rs b/magic_bridge/ic/src/dip721_proxy/src/common/tera.rs new file mode 100644 index 00000000..553aef3f --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/common/tera.rs @@ -0,0 +1,73 @@ +use async_trait::async_trait; +use ic_cdk::call; +use ic_cdk::export::candid::{Nat, Principal}; + +use crate::common::types::{Nonce, OutgoingMessage, TxError}; + +#[async_trait] +pub trait Tera { + async fn consume_message( + &self, + erc20_addr_pid: Principal, + nonce: Nonce, + payload: Vec, + ) -> Result; + async fn send_message( + &self, + erc20_addr_pid: Principal, + payload: Vec, + ) -> Result; +} + +#[async_trait] +impl Tera for Principal { + async fn consume_message( + &self, + erc20_addr_pid: Principal, + nonce: Nonce, + payload: Vec, + ) -> Result { + let consume: (Result,) = match call( + *self, + "consume_message", + (&erc20_addr_pid, &nonce, &payload), + ) + .await + { + Ok(res) => res, + Err((code, err)) => { + return Err(TxError::Other(format!( + "RejectionCode: {:?}\n{}", + code, err + ))) + } + }; + + match consume { + (Ok(_),) => Ok(true), + (Err(error),) => Err(TxError::Other(format!("Consume Message: {:?}", error))), + } + } + + async fn send_message( + &self, + erc20_addr_pid: Principal, + payload: Vec, + ) -> Result { + let send: (Result,) = + match call(*self, "consume_message", (&erc20_addr_pid, &payload)).await { + Ok(res) => res, + Err((code, err)) => { + return Err(TxError::Other(format!( + "RejectionCode: {:?}\n{}", + code, err + ))) + } + }; + + match send { + (Ok(_),) => Ok(true), + (Err(error),) => Err(TxError::Other(format!("Send Message: {:?}", error))), + } + } +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/common/types.rs b/magic_bridge/ic/src/dip721_proxy/src/common/types.rs new file mode 100644 index 00000000..7ae6fec3 --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/common/types.rs @@ -0,0 +1,95 @@ +use std::cell::RefCell; +use std::collections::HashMap; + +use ic_kit::candid::{CandidType, Deserialize, Nat, Principal}; +use serde::Serialize; + +pub type Nonce = Nat; + +pub type TokendId = Principal; + +pub type MessageHash = String; + +pub type EthereumAddr = Principal; + +pub type TxReceipt = Result; + +pub type MagicResponse = Result; + +#[derive(Serialize, CandidType, Deserialize)] +pub struct Message; + +#[derive(CandidType, Deserialize, Debug)] +pub enum FactoryError { + CreateCanisterError, + CanisterStatusNotAvailableError, + EncodeError, + CodeAlreadyInstalled, + InstallCodeError, +} + +#[derive(Clone, CandidType, Deserialize, Eq, PartialEq, Debug)] +pub enum MessageStatus { + Consuming, + ConsumedNotMinted, +} + +#[derive(CandidType, Deserialize)] +pub struct IncomingMessageHashParams { + pub from: Nat, + pub to: Nat, + pub nonce: Nonce, + pub payload: Vec, +} + +#[derive(CandidType, Deserialize)] +pub struct OutgoingMessageHashParams { + pub from: Nat, + pub to: Nat, + pub payload: Vec, +} + +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq, Hash)] +pub struct OutgoingMessage { + pub msg_key: [u8; 32], + pub msg_hash: String, +} + +#[derive(CandidType, Deserialize, Default)] +pub struct ProxyState { + /// store incoming messages against status locks + pub incoming_messages: RefCell>, + /// user balances + pub balances: RefCell>>, + /// authorized principals + pub controllers: RefCell>, +} + +#[derive(CandidType, Deserialize, Default)] +pub struct StableProxyState { + /// store incoming messages against status locks + pub incoming_messages: HashMap, + /// user balances + pub balances: HashMap>, + /// authorized principals + pub controllers: Vec, +} + +#[derive(CandidType, Deserialize, Clone, Copy)] +pub enum TokenType { + DIP20, + DIP721, +} + +#[derive(Deserialize, CandidType, Debug, PartialEq)] +pub enum TxError { + InsufficientBalance, + InsufficientAllowance, + Unauthorized, + LedgerTrap, + AmountTooSmall, + BlockUsed, + ErrorOperationStyle, + ErrorTo, + Other(String), +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/common/utils.rs b/magic_bridge/ic/src/dip721_proxy/src/common/utils.rs new file mode 100644 index 00000000..b70e01eb --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/common/utils.rs @@ -0,0 +1,89 @@ +use std::fmt; + +use ic_kit::candid::Nat; +use sha3::{Digest, Keccak256}; + +use super::types::{ + FactoryError, IncomingMessageHashParams, Message, MessageHash, OutgoingMessageHashParams, +}; + +impl fmt::Display for FactoryError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FactoryError::CreateCanisterError => write!(f, "CreateCanisterError"), + FactoryError::CanisterStatusNotAvailableError => { + write!(f, "CanisterStatusNotAvailableError") + } + FactoryError::EncodeError => write!(f, "EncodeError"), + FactoryError::CodeAlreadyInstalled => write!(f, "CodeAlreadyInstalled"), + FactoryError::InstallCodeError => write!(f, "InstallCodeError"), + } + } +} + +pub trait Keccak256HashFn { + fn calculate_hash(&self, params: T) -> MessageHash; +} + +impl Keccak256HashFn for Message { + fn calculate_hash(&self, params: IncomingMessageHashParams) -> MessageHash { + let mut data = vec![ + params.from, + params.to, + params.nonce, + Nat::from(params.payload.len()), + ]; + data.extend(params.payload); + + let data_encoded: Vec> = data + .clone() + .into_iter() + .map(|x| { + // take a slice of 32 + let f = [0u8; 32]; + let slice = &x.0.to_bytes_be()[..]; + // calculate zero values padding + let l = 32 - slice.len(); + [&f[..l], &slice].concat() + }) + .collect(); + + let concated = data_encoded.concat().to_vec(); + let mut hasher = Keccak256::new(); + + hasher.update(concated); + + let result = hasher.finalize(); + + hex::encode(result.to_vec()) + } +} + +impl Keccak256HashFn for Message { + fn calculate_hash(&self, params: OutgoingMessageHashParams) -> MessageHash { + let mut data = vec![params.from, params.to, Nat::from(params.payload.len())]; + data.extend(params.payload); + + let data_encoded: Vec> = data + .clone() + .into_iter() + .map(|x| { + // take a slice of 32 + let f = [0u8; 32]; + let slice = &x.0.to_bytes_be()[..]; + // calculate zero values padding + let l = 32 - slice.len(); + [&f[..l], &slice].concat() + }) + .collect(); + + let concated = data_encoded.concat().to_vec(); + let mut hasher = Keccak256::new(); + + hasher.update(concated); + + let result = hasher.finalize(); + + hex::encode(result.to_vec()) + } +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/lib.rs b/magic_bridge/ic/src/dip721_proxy/src/lib.rs deleted file mode 100644 index ac270a77..00000000 --- a/magic_bridge/ic/src/dip721_proxy/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn handler() { - todo!() -} diff --git a/magic_bridge/ic/src/dip721_proxy/src/main.rs b/magic_bridge/ic/src/dip721_proxy/src/main.rs new file mode 100644 index 00000000..8c0160f8 --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/main.rs @@ -0,0 +1,48 @@ +mod api; +mod common; +mod proxy; + +#[cfg(any(target_arch = "wasm32", test))] +fn main() {} + +#[cfg(not(any(target_arch = "wasm32", test)))] +fn main() { + use common::types::*; + use ic_kit::candid; + use ic_kit::candid::Nat; + use ic_kit::Principal; + + candid::export_service!(); + std::print!("{}", __export_service()); +} + +#[cfg(test)] +mod tests { + use ic_cdk::export::candid::{decode_args, encode_args, Nat}; + use std::str::FromStr; + + #[test] + fn test_decode_eth_payload() { + let payload = [ + // amount + Nat::from_str("100000000000000000").unwrap(), + // eth_addr + Nat::from_str("1390849295786071768276380950238675083608645509734").unwrap(), + ] + .to_vec(); + + let args_raw = encode_args(( + Nat::from(payload[0].0.clone()), + hex::encode(&payload[1].0.to_bytes_be()), + )) + .unwrap(); + + let (amount, eth_addr): (Nat, String) = decode_args(&args_raw).unwrap(); + + let expected_amount = "016345785d8a0000"; + assert_eq!(hex::encode(amount.0.to_bytes_be()), expected_amount); + + let expected_eth_addr = "f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + assert_eq!(eth_addr, expected_eth_addr); + } +} diff --git a/magic_bridge/ic/src/dip721_proxy/src/proxy.rs b/magic_bridge/ic/src/dip721_proxy/src/proxy.rs new file mode 100644 index 00000000..418ae159 --- /dev/null +++ b/magic_bridge/ic/src/dip721_proxy/src/proxy.rs @@ -0,0 +1,369 @@ +use std::{collections::HashMap, ops::AddAssign}; + +use ic_cdk::export::candid::{Nat, Principal}; +use ic_kit::ic; + +use crate::common::types::{MessageHash, MessageStatus, ProxyState, StableProxyState, TokendId}; + +pub const TERA_ADDRESS: &str = "timop-6qaaa-aaaab-qaeea-cai"; +pub const MAGIC_ADDRESS_IC: &str = "7z6fu-giaaa-aaaab-qafkq-cai"; +pub const ERC721_ADDRESS_ETH: &str = "0x15b661f6d3fd9a7ed8ed4c88bccfd1546644443f"; + +thread_local! { + pub static STATE: ProxyState = ProxyState::default(); +} + +impl ProxyState { + pub fn store_incoming_message(&self, msg_hash: MessageHash) { + self.incoming_messages + .borrow_mut() + .entry(msg_hash) + .or_insert(MessageStatus::Consuming); + } + + pub fn get_message(&self, msg_hash: &MessageHash) -> Option { + self.incoming_messages.borrow().get(msg_hash).cloned() + } + + pub fn update_incoming_message_status(&self, msg_hash: MessageHash, status: MessageStatus) { + self.incoming_messages.borrow_mut().insert(msg_hash, status); + } + + pub fn remove_incoming_message(&self, msg_hash: MessageHash) -> Option { + self.incoming_messages.borrow_mut().remove(&msg_hash) + } + + pub fn get_balance(&self, caller: Principal, token_id: TokendId) -> Option { + self.balances + .borrow() + .get(&caller) + .map(|s| s.get(&token_id)) + .map(|b| match b { + Some(balance) => balance.clone(), + None => Nat::from(0_u32), + }) + } + + pub fn get_all_balances(&self, caller: Principal) -> Result, String> { + let token_balances = self.balances.borrow().get(&caller).cloned(); + + if let Some(balances) = token_balances { + return Ok(balances + .into_iter() + .map(|(p, n)| (p.to_string(), n)) + .collect::>()); + } + + Err(format!("User {} has no token balances!", &caller)) + } + + pub fn add_balance(&self, caller: Principal, token_id: TokendId, amount: Nat) { + self.balances + .borrow_mut() + .entry(caller) + .or_default() + .entry(token_id) + .or_default() + .add_assign(amount.clone()) + } + + pub fn update_balance(&self, caller: Principal, token_id: TokendId, amount: Nat) { + self.balances + .borrow_mut() + .insert(caller, HashMap::from([(token_id, amount)])); + } + + pub fn _authorize(&self, other: Principal) { + let caller = ic::caller(); + let caller_autorized = self.controllers.borrow().iter().any(|p| *p == caller); + if caller_autorized { + self.controllers.borrow_mut().push(other); + } + } + + pub fn _is_authorized(&self) -> Result<(), String> { + self.controllers + .borrow() + .contains(&ic::caller()) + .then(|| ()) + .ok_or("Caller is not authorized".to_string()) + } + + pub fn take_all(&self) -> StableProxyState { + StableProxyState { + balances: self.balances.take(), + controllers: self.controllers.take(), + incoming_messages: self.incoming_messages.take(), + } + } + + pub fn clear_all(&self) { + self.balances.borrow_mut().clear(); + self.controllers.borrow_mut().clear(); + self.incoming_messages.borrow_mut().clear(); + } + + pub fn replace_all(&self, stable_message_state: StableProxyState) { + self.balances.replace(stable_message_state.balances); + self.controllers.replace(stable_message_state.controllers); + self.incoming_messages + .replace(stable_message_state.incoming_messages); + } +} + +pub trait ToNat { + fn to_nat(&self) -> Nat; +} + +impl ToNat for Principal { + fn to_nat(&self) -> Nat { + Nat::from(num_bigint::BigUint::from_bytes_be(&self.as_slice()[..])) + } +} + +pub trait FromNat { + fn from_nat(input: Nat) -> Principal; +} + +impl FromNat for Principal { + #[inline(always)] + fn from_nat(input: Nat) -> Principal { + let be_bytes = input.0.to_bytes_be(); + let be_bytes_len = be_bytes.len(); + let padding_bytes = if be_bytes_len > 10 && be_bytes_len < 29 { + 29 - be_bytes_len + } else if be_bytes_len < 10 { + 10 - be_bytes_len + } else { + 0 + }; + let mut p_slice = vec![0u8; padding_bytes]; + p_slice.extend_from_slice(&be_bytes); + Principal::from_slice(&p_slice) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::common::{ + types::{IncomingMessageHashParams, Message}, + utils::Keccak256HashFn, + }; + + use super::*; + use ic_kit::mock_principals; + + #[test] + fn test_message_status_new_message() { + let msg_hash = + String::from("c9e23418a985892acc0fa031331080bfce112bdf841a3ae04a5181c6da1610b1"); + + STATE.with(|s| { + let mut message = s.incoming_messages.borrow_mut(); + let status = message + .entry(msg_hash.clone()) + .or_insert(MessageStatus::Consuming); + + *status = MessageStatus::ConsumedNotMinted; + }); + + let message_status = STATE.with(|s| s.get_message(&msg_hash)); + + assert_eq!(message_status.unwrap(), MessageStatus::ConsumedNotMinted); + } + + #[test] + fn test_message_status_update_message() { + let msg_hash = + String::from("c9e23418a985892acc0fa031331080bfce112bdf841a3ae04a5181c6da1610b1"); + + STATE.with(|s| s.store_incoming_message(msg_hash.clone())); + + STATE.with(|s| { + s.update_incoming_message_status(msg_hash.clone(), MessageStatus::ConsumedNotMinted) + }); + + let message_status = STATE.with(|s| s.get_message(&msg_hash)); + + assert_eq!( + message_status.clone().unwrap(), + MessageStatus::ConsumedNotMinted + ); + + STATE + .with(|s| s.update_incoming_message_status(msg_hash.clone(), MessageStatus::Consuming)); + + let message_status1 = STATE.with(|s| s.get_message(&msg_hash)); + + assert_eq!(message_status1.clone().unwrap(), MessageStatus::Consuming); + // println!("{:#?}", message_status); + } + + #[test] + fn test_remove_message() { + let msg_hash = + String::from("c9e23418a985892acc0fa031331080bfce112bdf841a3ae04a5181c6da1610b1"); + + STATE.with(|s| { + s.update_incoming_message_status(msg_hash.clone(), MessageStatus::ConsumedNotMinted) + }); + + let _ = STATE.with(|s| s.remove_incoming_message(msg_hash.clone())); + + let message_status = STATE.with(|s| s.get_message(&msg_hash)); + + assert_eq!(message_status.is_none(), true); + } + + #[test] + fn test_add_balance() { + let amount = Nat::from(100_u32); + let pid = mock_principals::bob(); + let token_id = mock_principals::alice(); + + STATE.with(|s| s.add_balance(pid, token_id, amount.clone())); + + let balance_of = STATE.with(|s| s.get_balance(pid, token_id)); + let balance = balance_of.unwrap(); + + assert_eq!(balance, amount.clone()); + } + + #[test] + fn test_get_all_balances() { + let amount = Nat::from(100_u32); + let caller = mock_principals::bob(); + let token_id_1 = mock_principals::alice(); + let token_id_2 = mock_principals::john(); + + STATE.with(|s| s.add_balance(caller, token_id_1, amount.clone())); + STATE.with(|s| s.add_balance(caller, token_id_2, amount.clone())); + + let balances = STATE.with(|s| s.get_all_balances(caller)); + + assert_eq!(balances.as_ref().unwrap()[0].0, token_id_1.to_string()); + assert_eq!(balances.as_ref().unwrap()[1].0, token_id_2.to_string()); + + assert_eq!(balances.as_ref().unwrap()[0].1, amount.clone()); + assert_eq!(balances.as_ref().unwrap()[1].1, amount.clone()); + } + + #[test] + fn test_update_balance() { + let amount = Nat::from(100_u32); + let caller = mock_principals::bob(); + let token_id = mock_principals::alice(); + + STATE.with(|s| s.add_balance(caller, token_id, amount.clone())); + + let balance_of = STATE.with(|s| s.get_balance(caller, token_id)); + let balance = balance_of.unwrap(); + + assert_eq!(balance, amount.clone()); + + let new_balance = Nat::from(134_u32); + STATE.with(|s| s.update_balance(caller, token_id, new_balance.clone())); + + let balance_after_update = STATE.with(|s| s.get_balance(caller, token_id)); + + assert_eq!(balance_after_update.unwrap(), new_balance); + } + + #[test] + fn test_store_incoming_message() { + let nonce = Nat::from(4_u32); + let receiver = + Nat::from_str("5575946531581959547228116840874869615988566799087422752926889285441538") + .unwrap(); + + let token_id = Principal::from_text("tcy4r-qaaaa-aaaab-qadyq-cai").unwrap(); + let to = token_id.to_nat(); + + let from_slice = hex::decode("1b864e1CA9189CFbD8A14a53A02E26B00AB5e91a").unwrap(); + let from = Nat::from(num_bigint::BigUint::from_bytes_be(&from_slice[..])); + + let amount = Nat::from_str("69000000").unwrap(); + let payload = [receiver, amount].to_vec(); + + let msg_hash_expected = "c9e23418a985892acc0fa031331080bfce112bdf841a3ae04a5181c6da1610b1"; + let msg_hash = Message.calculate_hash(IncomingMessageHashParams { + from, + to: to.clone(), + nonce, + payload, + }); + + assert_eq!(msg_hash, msg_hash_expected); + + STATE.with(|s| s.store_incoming_message(msg_hash.clone())); + + let msg_exists = STATE.with(|s| s.get_message(&msg_hash)); + assert_eq!(msg_exists.unwrap(), MessageStatus::Consuming); + } + + #[test] + fn test_store_erc721_incoming_message() { + let nonce = Nat::from(37_u64); + let receiver = + Nat::from_str("5575946531581959547228116840874869615988566799087422752926889285441538") + .unwrap(); + + let dip20_token_id = Principal::from_text("767da-lqaaa-aaaab-qafka-cai").unwrap(); + let to = dip20_token_id.to_nat(); + + let from_slice = hex::decode("15B661f6D3FD9A7ED8Ed4c88bCcfD1546644443f").unwrap(); + let from = Nat::from(num_bigint::BigUint::from_bytes_be(&from_slice[..])); + + let amount = Nat::from(1_u64); + + let originating_erc721_token = + Nat::from_str("1064074219490881077210656189219336181026035659484").unwrap(); + let name = Nat::from_str( + "31834093750153841782852689224122693026672464094252661502799082895056765452288", + ) + .unwrap(); + let symbol = Nat::from_str( + "31777331108478719365477537505109683054320756229570641444674276344806789611520", + ) + .unwrap(); + let decimals = Nat::from_str("18").unwrap(); + + let payload = [ + originating_erc721_token, + receiver, + amount, + name, + symbol, + decimals, + ] + .to_vec(); + + let msg_hash_expected = "eebd5cf3d4e41e9671f34f875a7fdcf7547753a98cc1cb822826f01e91432dca"; + let msg_hash = Message.calculate_hash(IncomingMessageHashParams { + from, + to: to.clone(), + nonce, + payload, + }); + + assert_eq!(msg_hash, msg_hash_expected); + } + + #[test] + fn test_hex_to_pid() { + let erc721_addr_hex = "15B661f6D3FD9A7ED8Ed4c88bCcfD1546644443f"; + + let erc721_addr_pid = Principal::from_slice(&hex::decode(erc721_addr_hex).unwrap()); + + let erc721_addr_hex = hex::encode( + Principal::from_text("6iiev-lyvwz-q7nu7-5tj7n-r3kmr-c6m7u-kumzc-eipy").unwrap(), + ); + + assert_eq!( + erc721_addr_pid.to_string(), + "6iiev-lyvwz-q7nu7-5tj7n-r3kmr-c6m7u-kumzc-eipy" + ); + } +}