use std::string::ToString; use borsh::{BorshDeserialize, BorshSerialize}; use core::fmt; use core::str::FromStr; use near_sdk::json_types::{U128, U64}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::serde_with::hex::Hex; use near_sdk::{near, serde_json, AccountId}; use num_enum::IntoPrimitive; use schemars::JsonSchema; use sol_address::SolAddress; pub mod aptos; pub mod bounded_string; pub mod btc; pub mod errors; pub mod evm; pub mod hex_types; pub mod locker_args; pub mod mpc_types; pub mod near_events; pub mod prover_args; pub mod prover_result; pub mod sol_address; pub mod starknet; pub mod utils; #[cfg(test)] mod tests; pub use bounded_string::BoundedString; pub use errors::{ BridgeError, OmniError, ProverError, StorageBalanceError, StorageError, TokenError, TypesError, }; pub use hex_types::{H160, H256}; #[near(serializers = [borsh, json])] #[derive( Debug, Eq, Clone, Copy, PartialEq, PartialOrd, Ord, strum_macros::AsRefStr, Default, IntoPrimitive, Hash, )] #[repr(u8)] pub enum ChainKind { #[default] #[serde(alias = "eth")] Eth, #[serde(alias = "near")] Near, #[serde(alias = "sol")] Sol, #[serde(alias = "arb")] Arb, #[serde(alias = "base")] Base, #[serde(alias = "bnb")] Bnb, #[serde(alias = "btc")] Btc, #[serde(alias = "zcash")] Zcash, #[serde(alias = "pol")] Pol, #[serde(rename = "HlEvm")] #[serde(alias = "hlevm")] #[strum(serialize = "HlEvm")] HyperEvm, #[serde(alias = "strk")] Strk, #[serde(alias = "abs")] Abs, #[serde(alias = "fogo")] Fogo, #[serde(alias = "aptos")] Aptos, } impl ChainKind { pub const fn is_evm_chain(&self) -> bool { match self { Self::Eth | Self::Arb | Self::Base | Self::Bnb | Self::Pol | Self::HyperEvm | Self::Abs => true, Self::Btc | Self::Zcash | Self::Near | Self::Sol | Self::Strk | Self::Fogo | Self::Aptos => false, } } pub const fn is_utxo_chain(&self) -> bool { match self { Self::Btc | Self::Zcash => true, Self::Eth | Self::Arb | Self::Base | Self::Bnb | Self::Pol | Self::Near | Self::Sol | Self::HyperEvm | Self::Strk | Self::Abs | Self::Fogo | Self::Aptos => false, } } pub const fn is_svm_chain(&self) -> bool { match self { Self::Sol | Self::Fogo => true, Self::Eth | Self::Arb | Self::Base | Self::Bnb | Self::Pol | Self::Near | Self::HyperEvm | Self::Strk | Self::Abs | Self::Btc | Self::Zcash | Self::Aptos => false, } } } impl FromStr for ChainKind { type Err = String; fn from_str(s: &str) -> Result { near_sdk::serde_json::from_str(&format!("\"{s}\"")).map_err(stringify) } } impl From<&OmniAddress> for ChainKind { fn from(input: &OmniAddress) -> Self { input.get_chain() } } impl TryFrom for ChainKind { type Error = String; fn try_from(input: u8) -> Result { match input { 0 => Ok(Self::Eth), 1 => Ok(Self::Near), 2 => Ok(Self::Sol), 3 => Ok(Self::Arb), 4 => Ok(Self::Base), 5 => Ok(Self::Bnb), 6 => Ok(Self::Btc), 7 => Ok(Self::Zcash), 8 => Ok(Self::Pol), 9 => Ok(Self::HyperEvm), 10 => Ok(Self::Strk), 11 => Ok(Self::Abs), 12 => Ok(Self::Fogo), 13 => Ok(Self::Aptos), _ => Err(format!("{input:?} invalid chain kind")), } } } pub type EvmAddress = H160; pub type UTXOChainAddress = String; pub type StarknetAddress = H256; pub type AptosAddress = H256; pub const ZERO_ACCOUNT_ID: &str = "0000000000000000000000000000000000000000000000000000000000000000"; #[near(serializers=[borsh])] #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum OmniAddress { Eth(EvmAddress), Near(AccountId), Sol(SolAddress), Arb(EvmAddress), Base(EvmAddress), Bnb(EvmAddress), Btc(UTXOChainAddress), Zcash(UTXOChainAddress), Pol(EvmAddress), HyperEvm(EvmAddress), Strk(StarknetAddress), Abs(EvmAddress), Fogo(SolAddress), Aptos(AptosAddress), } impl OmniAddress { #[allow(clippy::missing_panics_doc)] pub fn new_zero(chain_kind: ChainKind) -> Result { match chain_kind { ChainKind::Eth => Ok(Self::Eth(H160::ZERO)), ChainKind::Near => Ok(Self::Near(ZERO_ACCOUNT_ID.parse().map_err(stringify)?)), ChainKind::Sol => Ok(Self::Sol(SolAddress::ZERO)), ChainKind::Arb => Ok(Self::Arb(H160::ZERO)), ChainKind::Base => Ok(Self::Base(H160::ZERO)), ChainKind::Bnb => Ok(Self::Bnb(H160::ZERO)), ChainKind::Pol => Ok(Self::Pol(H160::ZERO)), ChainKind::HyperEvm => Ok(Self::HyperEvm(H160::ZERO)), ChainKind::Btc => Ok(Self::Btc(String::new())), ChainKind::Zcash => Ok(Self::Zcash(String::new())), ChainKind::Strk => Ok(Self::Strk(H256::ZERO)), ChainKind::Abs => Ok(Self::Abs(H160::ZERO)), ChainKind::Fogo => Ok(Self::Fogo(SolAddress::ZERO)), ChainKind::Aptos => Ok(Self::Aptos(H256::ZERO)), } } pub fn new_from_evm_address( chain_kind: ChainKind, address: EvmAddress, ) -> Result { match chain_kind { ChainKind::Eth => Ok(Self::Eth(address)), ChainKind::Arb => Ok(Self::Arb(address)), ChainKind::Base => Ok(Self::Base(address)), ChainKind::Bnb => Ok(Self::Bnb(address)), ChainKind::Pol => Ok(Self::Pol(address)), ChainKind::HyperEvm => Ok(Self::HyperEvm(address)), ChainKind::Abs => Ok(Self::Abs(address)), _ => Err(format!("{chain_kind:?} is not an EVM chain")), } } pub fn new_from_slice(chain_kind: ChainKind, address: &[u8]) -> Result { match chain_kind { ChainKind::Sol => Ok(Self::Sol(Self::to_sol_address(address)?)), ChainKind::Fogo => Ok(Self::Fogo(Self::to_sol_address(address)?)), ChainKind::Eth | ChainKind::Arb | ChainKind::Base | ChainKind::Bnb | ChainKind::Pol | ChainKind::HyperEvm | ChainKind::Abs => { Self::new_from_evm_address(chain_kind, Self::to_evm_address(address)?) } ChainKind::Near => Ok(Self::Near(Self::to_near_account_id(address)?)), ChainKind::Btc => Ok(Self::Btc( String::from_utf8(address.to_vec()) .map_err(|e| format!("Invalid BTC address: {e}"))?, )), ChainKind::Zcash => Ok(Self::Zcash( String::from_utf8(address.to_vec()) .map_err(|e| format!("Invalid ZCash address: {e}"))?, )), ChainKind::Strk => Ok(Self::Strk(H256(address.try_into().map_err(stringify)?))), ChainKind::Aptos => Ok(Self::Aptos(H256(address.try_into().map_err(stringify)?))), } } pub const fn get_chain(&self) -> ChainKind { match self { Self::Eth(_) => ChainKind::Eth, Self::Near(_) => ChainKind::Near, Self::Sol(_) => ChainKind::Sol, Self::Arb(_) => ChainKind::Arb, Self::Base(_) => ChainKind::Base, Self::Bnb(_) => ChainKind::Bnb, Self::Pol(_) => ChainKind::Pol, Self::HyperEvm(_) => ChainKind::HyperEvm, Self::Btc(_) => ChainKind::Btc, Self::Zcash(_) => ChainKind::Zcash, Self::Strk(_) => ChainKind::Strk, Self::Abs(_) => ChainKind::Abs, Self::Fogo(_) => ChainKind::Fogo, Self::Aptos(_) => ChainKind::Aptos, } } pub fn encode(&self, separator: char, skip_zero_address: bool) -> String { let (chain_str, address) = match self { Self::Eth(address) => ("eth", address.to_string()), Self::Near(address) => ("near", address.to_string()), Self::Sol(address) => ("sol", address.to_string()), Self::Arb(address) => ("arb", address.to_string()), Self::Base(address) => ("base", address.to_string()), Self::Bnb(address) => ("bnb", address.to_string()), Self::Pol(address) => ("pol", address.to_string()), Self::HyperEvm(address) => ("hlevm", address.to_string()), Self::Btc(address) => ("btc", address.clone()), Self::Zcash(address) => ("zcash", address.clone()), Self::Strk(address) => ("strk", address.to_string()), Self::Abs(address) => ("abs", address.to_string()), Self::Fogo(address) => ("fogo", address.to_string()), Self::Aptos(address) => ("aptos", address.to_string()), }; if skip_zero_address && self.is_zero() { chain_str.to_string() } else { format!("{chain_str}{separator}{address}") } } pub fn is_zero(&self) -> bool { match self { Self::Eth(address) | Self::Arb(address) | Self::Base(address) | Self::Bnb(address) | Self::Pol(address) | Self::HyperEvm(address) | Self::Abs(address) => address.is_zero(), Self::Near(address) => *address == ZERO_ACCOUNT_ID, Self::Sol(address) | Self::Fogo(address) => address.is_zero(), Self::Btc(address) | Self::Zcash(address) => address.is_empty(), Self::Strk(address) | Self::Aptos(address) => address.is_zero(), } } pub fn get_token_prefix(&self) -> String { match self { Self::Sol(address) => Self::hashed_token_prefix("sol", &H256(address.0)), Self::Fogo(address) => Self::hashed_token_prefix("fogo", &H256(address.0)), Self::Strk(address) => Self::hashed_token_prefix("strk", address), Self::Aptos(address) => Self::hashed_token_prefix("aptos", address), Self::Eth(address) => { if self.is_zero() { "eth".to_string() } else { address.to_string()[2..].to_string() } } _ => self.encode('-', true), } } pub fn get_utxo_address(&self) -> Option { match self { Self::Btc(btc_address) => Some(btc_address.clone()), Self::Zcash(zcash_address) => Some(zcash_address.clone()), _ => None, } } pub fn is_evm_chain(&self) -> bool { self.get_chain().is_evm_chain() } pub fn is_utxo_chain(&self) -> bool { self.get_chain().is_utxo_chain() } // The AccountId on Near can't be uppercased and has a 64 character limit, // so we encode the address into 20 bytes to bypass these restrictions fn hashed_token_prefix(prefix: &str, address: &H256) -> String { if address.is_zero() { prefix.to_string() } else { let hashed_address = H160( utils::keccak256(&address.0)[12..] .try_into() .unwrap_or_default(), ) .to_string(); format!("{prefix}-{hashed_address}") } } fn to_evm_address(address: &[u8]) -> Result { let address = if address.len() == 32 { &address[address.len() - 20..] } else { address }; address.try_into().map_or_else( |_| Err("Invalid EVM address".to_string()), |bytes| Ok(H160(bytes)), ) } fn to_sol_address(address: &[u8]) -> Result { address.try_into().map_or_else( |_| Err("Invalid SOL address".to_string()), |bytes| Ok(SolAddress(bytes)), ) } fn to_near_account_id(address: &[u8]) -> Result { AccountId::from_str(&String::from_utf8(address.to_vec()).map_err(stringify)?) .map_err(stringify) } } impl FromStr for OmniAddress { type Err = String; fn from_str(input: &str) -> Result { let (chain, recipient) = input.split_once(':').unwrap_or(("eth", input)); match chain { "eth" => Ok(Self::Eth(recipient.parse().map_err(stringify)?)), "near" => Ok(Self::Near(recipient.parse().map_err(stringify)?)), "sol" => Ok(Self::Sol(recipient.parse().map_err(stringify)?)), "arb" => Ok(Self::Arb(recipient.parse().map_err(stringify)?)), "base" => Ok(Self::Base(recipient.parse().map_err(stringify)?)), "bnb" => Ok(Self::Bnb(recipient.parse().map_err(stringify)?)), "pol" => Ok(Self::Pol(recipient.parse().map_err(stringify)?)), "hlevm" => Ok(Self::HyperEvm(recipient.parse().map_err(stringify)?)), "abs" => Ok(Self::Abs(recipient.parse().map_err(stringify)?)), "btc" => Ok(Self::Btc(recipient.to_string())), "zcash" => Ok(Self::Zcash(recipient.to_string())), "strk" => Ok(Self::Strk(recipient.parse().map_err(stringify)?)), "fogo" => Ok(Self::Fogo(recipient.parse().map_err(stringify)?)), "aptos" => Ok(Self::Aptos(recipient.parse().map_err(stringify)?)), _ => Err(format!("Chain {chain} is not supported")), } } } impl fmt::Display for OmniAddress { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", &self.encode(':', false)) } } impl JsonSchema for OmniAddress { fn is_referenceable() -> bool { false } fn schema_name() -> String { String::schema_name() } fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { String::json_schema(gen) } } impl Serialize for OmniAddress { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for OmniAddress { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct OmniAddressVisitor; impl serde::de::Visitor<'_> for OmniAddressVisitor { type Value = OmniAddress; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string in the format 'chain:address'") } fn visit_str(self, input: &str) -> Result where E: serde::de::Error, { OmniAddress::from_str(input).map_err(E::custom) } } deserializer.deserialize_str(OmniAddressVisitor) } } #[derive(Serialize, Deserialize, Debug, Clone)] pub enum BridgeOnTransferMsg { InitTransfer(InitTransferMsg), FastFinTransfer(FastFinTransferMsg), UtxoFinTransfer(UtxoFinTransferMsg), SwapMigratedToken, } /// Maximum byte length for `InitTransferMsg::msg` — caps the user-supplied hook payload /// forwarded to the destination chain. pub const MAX_INIT_TRANSFER_MSG_LEN: usize = 2048; /// Maximum byte length for `InitTransferMsg::external_id` — large enough for UUIDs or /// hex-encoded 32-byte hashes, small enough to bound storage-account-hash inputs. pub const MAX_EXTERNAL_ID_LEN: usize = 64; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct InitTransferMsg { pub recipient: OmniAddress, pub fee: U128, pub native_token_fee: U128, /// Optional caller-supplied destination-chain hook payload. Length-capped to /// [`MAX_INIT_TRANSFER_MSG_LEN`] bytes to prevent unbounded storage/gas inflation. pub msg: Option>, /// Optional caller-provided identifier mixed into the virtual storage account ID hash. /// Lets otherwise-identical transfers derive distinct storage accounts so their /// storage deposits do not collide. Length-capped to [`MAX_EXTERNAL_ID_LEN`] bytes. pub external_id: Option>, } impl InitTransferMsg { pub const fn get_destination_chain(&self) -> ChainKind { self.recipient.get_chain() } } #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize, Debug, Clone)] pub struct FastFinTransferMsg { pub transfer_id: UnifiedTransferId, pub recipient: OmniAddress, pub fee: Fee, pub msg: String, pub amount: U128, pub storage_deposit_amount: Option, pub relayer: AccountId, } #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct UtxoFinTransferMsg { pub utxo_id: UtxoId, pub recipient: OmniAddress, pub relayer_fee: U128, pub msg: String, } impl UtxoFinTransferMsg { pub const fn get_destination_chain(&self) -> ChainKind { self.recipient.get_chain() } pub fn get_transfer_id(&self, origin_chain: ChainKind) -> UnifiedTransferId { UnifiedTransferId { origin_chain, kind: TransferIdKind::Utxo(self.utxo_id.clone()), } } } #[near(serializers=[borsh, json])] #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Fee { pub fee: U128, pub native_fee: U128, } impl Fee { pub const fn is_zero(&self) -> bool { self.fee.0 == 0 && self.native_fee.0 == 0 } } #[near(serializers = [borsh, json])] #[derive(Debug, Clone, PartialEq, Eq, Default, Copy)] pub struct TransferId { // The origin chain kind pub origin_chain: ChainKind, // The transfer nonce that maintained on the source chain pub origin_nonce: Nonce, } #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct TransferMessage { pub origin_nonce: Nonce, pub token: OmniAddress, pub amount: U128, pub recipient: OmniAddress, pub fee: Fee, pub sender: OmniAddress, pub msg: String, pub destination_nonce: Nonce, pub origin_transfer_id: Option, } impl TransferMessage { pub const fn get_origin_chain(&self) -> ChainKind { self.sender.get_chain() } pub const fn get_transfer_id(&self) -> TransferId { TransferId { origin_chain: self.get_origin_chain(), origin_nonce: self.origin_nonce, } } pub const fn get_destination_chain(&self) -> ChainKind { self.recipient.get_chain() } pub fn calculate_storage_account_id(&self, external_id: Option) -> AccountId { TransferMessageStorageAccount::from(self.clone()).id(external_id) } pub fn amount_without_fee(&self) -> Option { self.amount.0.checked_sub(self.fee.fee.0) } } // Used to calculate virtual account ID that can be used to deposit storage required for the message #[near(serializers=[borsh])] #[derive(Debug, Clone)] pub struct TransferMessageStorageAccount { pub token: OmniAddress, pub amount: U128, pub recipient: OmniAddress, pub fee: Fee, pub sender: OmniAddress, pub msg: String, } impl TransferMessageStorageAccount { #[allow(clippy::missing_panics_doc)] pub fn id(&self, external_id: Option) -> AccountId { let mut bytes = borsh::to_vec(self).unwrap(); if let Some(external_id) = external_id { bytes.extend_from_slice(external_id.as_bytes()); } let hash = utils::sha256(&bytes); let implicit_account_id = hex::encode(hash); AccountId::try_from(implicit_account_id).unwrap() } } impl From for TransferMessageStorageAccount { fn from(value: TransferMessage) -> Self { Self { token: value.token, amount: value.amount, recipient: value.recipient, fee: value.fee, sender: value.sender, msg: value.msg, } } } #[near(serializers = [borsh, json])] #[derive(Debug, Clone)] pub enum PayloadType { TransferMessage, Metadata, ClaimNativeFee, } #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct TransferMessagePayloadV1 { pub prefix: PayloadType, pub destination_nonce: Nonce, pub transfer_id: TransferId, pub token_address: OmniAddress, pub amount: U128, pub recipient: OmniAddress, pub fee_recipient: Option, } impl From for TransferMessagePayloadV1 { fn from(payload: TransferMessagePayload) -> Self { Self { prefix: payload.prefix, destination_nonce: payload.destination_nonce, transfer_id: payload.transfer_id, token_address: payload.token_address, amount: payload.amount, recipient: payload.recipient, fee_recipient: payload.fee_recipient, } } } #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct TransferMessagePayload { pub prefix: PayloadType, pub destination_nonce: Nonce, pub transfer_id: TransferId, pub token_address: OmniAddress, pub amount: U128, pub recipient: OmniAddress, pub fee_recipient: Option, #[serde(default)] pub message: Vec, } impl TransferMessagePayload { pub fn encode_hashable(&self) -> Result, String> { if self.message.is_empty() { borsh::to_vec(&TransferMessagePayloadV1::from(self.clone())).map_err(stringify) } else { borsh::to_vec(self).map_err(stringify) } } } #[near(serializers = [borsh, json])] #[derive(Debug, Clone)] pub struct MetadataPayload { pub prefix: PayloadType, pub token: String, pub name: String, pub symbol: String, pub decimals: u8, } #[near(serializers=[borsh, json])] #[derive(Clone)] pub struct SignRequest { pub payload: [u8; 32], pub path: String, pub key_version: u32, } #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub enum UpdateFee { Fee(Fee), Proof(Vec), } pub type Nonce = u64; pub fn stringify(item: T) -> String { item.to_string() } #[near(serializers=[json])] #[derive(Clone, Debug)] pub struct BasicMetadata { pub name: String, pub symbol: String, pub decimals: u8, } #[near(serializers=[borsh, json])] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(try_from = "String", into = "String")] pub struct UtxoId { pub tx_hash: String, pub vout: u32, } impl std::str::FromStr for UtxoId { type Err = String; fn from_str(s: &str) -> Result { let parts: Vec<&str> = s.split('@').collect(); if parts.len() != 2 { return Err(format!( "Invalid UtxoId format '{s}': expected 'tx_hash@vout'", )); } let tx_hash = parts[0].to_string(); let vout = parts[1] .parse::() .map_err(|e| format!("Invalid vout '{}' in UtxoId: {}", parts[1], e))?; Ok(UtxoId { tx_hash, vout }) } } impl std::fmt::Display for UtxoId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}@{}", self.tx_hash, self.vout) } } impl TryFrom for UtxoId { type Error = String; fn try_from(s: String) -> Result { s.parse() } } impl From for String { fn from(utxo_id: UtxoId) -> Self { format!("{}@{}", utxo_id.tx_hash, utxo_id.vout) } } #[near(serializers=[borsh, json])] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum TransferIdKind { Nonce(Nonce), Utxo(UtxoId), } #[near(serializers=[borsh, json])] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct UnifiedTransferId { pub origin_chain: ChainKind, pub kind: TransferIdKind, } impl From for UnifiedTransferId { fn from(value: TransferId) -> Self { Self { origin_chain: value.origin_chain, kind: TransferIdKind::Nonce(value.origin_nonce), } } } impl UnifiedTransferId { pub fn is_utxo(&self) -> bool { matches!(self.kind, TransferIdKind::Utxo(_)) } } impl TryInto for &UnifiedTransferId { type Error = &'static str; fn try_into(self) -> Result { let origin_nonce = match self.kind { TransferIdKind::Nonce(nonce) => nonce, TransferIdKind::Utxo(_) => { return Err("Cannot convert UTXO transfer ID to general transfer ID") } }; Ok(TransferId { origin_chain: self.origin_chain, origin_nonce, }) } } impl std::fmt::Display for UnifiedTransferId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.kind { TransferIdKind::Nonce(nonce) => write!(f, "{}:{}", self.origin_chain.as_ref(), nonce), TransferIdKind::Utxo(utxo_id) => { write!(f, "{}:{}", self.origin_chain.as_ref(), utxo_id) } } } } #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct FastTransferId(pub [u8; 32]); #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct FastTransfer { pub transfer_id: UnifiedTransferId, pub token_id: AccountId, pub amount: U128, pub fee: Fee, pub recipient: OmniAddress, pub msg: String, } impl FastTransfer { #[allow(clippy::missing_panics_doc)] pub fn id(&self) -> FastTransferId { FastTransferId(utils::sha256(&borsh::to_vec(self).unwrap())) } pub const fn get_destination_chain(&self) -> ChainKind { self.recipient.get_chain() } pub fn amount_without_fee(&self) -> Option { self.amount.0.checked_sub(self.fee.fee.0) } } impl FastTransfer { pub fn from_transfer(transfer: TransferMessage, token_id: AccountId) -> Self { Self { transfer_id: UnifiedTransferId { origin_chain: transfer.get_origin_chain(), kind: TransferIdKind::Nonce(transfer.origin_nonce), }, token_id, amount: transfer.amount, fee: transfer.fee, recipient: transfer.recipient, msg: transfer.msg, } } pub fn from_utxo_transfer( transfer: UtxoFinTransferMsg, token_id: AccountId, amount: U128, origin_chain: ChainKind, ) -> Self { Self { transfer_id: UnifiedTransferId { origin_chain, kind: TransferIdKind::Utxo(transfer.utxo_id), }, token_id, amount, fee: Fee { fee: transfer.relayer_fee, native_fee: U128(0), }, recipient: transfer.recipient, msg: transfer.msg, } } } #[near(serializers=[borsh, json])] #[derive(Debug, Clone)] pub struct FastTransferStatus { pub finalised: bool, pub relayer: AccountId, pub storage_owner: AccountId, } #[near(serializers=[json])] #[derive(Debug, PartialEq)] pub enum DestinationChainMsg { MaxGasFee(U64), DestHexMsg(#[serde_as(as = "Hex")] Vec), } impl DestinationChainMsg { pub fn max_gas_fee(&self) -> Option { if let Self::MaxGasFee(fee) = self { Some(U128(fee.0.into())) } else { None } } pub fn destination_msg(&self) -> Option> { if let Self::DestHexMsg(msg) = self { Some(msg.clone()) } else { None } } pub fn from_json(s: &str) -> Option { serde_json::from_str(s).ok() } } pub fn get_native_token_address(chain_kind: ChainKind) -> Result { match chain_kind { ChainKind::Strk => OmniAddress::from_str( "strk:0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", ), // APT is itself a Fungible Asset with a well-known canonical metadata // object address (`0xa`) — identical on mainnet/testnet/devnet. ChainKind::Aptos => OmniAddress::from_str( "aptos:0x000000000000000000000000000000000000000000000000000000000000000a", ), ChainKind::Eth | ChainKind::Near | ChainKind::Sol | ChainKind::Arb | ChainKind::Base | ChainKind::Bnb | ChainKind::Btc | ChainKind::Zcash | ChainKind::Pol | ChainKind::HyperEvm | ChainKind::Abs | ChainKind::Fogo => OmniAddress::new_zero(chain_kind), } }