diff --git a/dev-logs/2023-feb/features/nft_integration_poc b/dev-logs/2023-feb/features/nft_integration_poc new file mode 100644 index 0000000000..bfdaff18c6 --- /dev/null +++ b/dev-logs/2023-feb/features/nft_integration_poc @@ -0,0 +1,4 @@ +NFT integration PoC added. Includes ERC721 support for ETH and BSC. + + +author: @laruh \ No newline at end of file diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index abeff5863c..ada1698a06 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -9,6 +9,7 @@ zhtlc-native-tests = [] # Remove this once the solana integration becomes stable/completed. disable-solana-tests = [] default = ["disable-solana-tests"] +enable-nft-integration = [] [lib] name = "coins" diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 6d7ecc942d..11ad6acc27 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -21,6 +21,8 @@ // Copyright © 2022 AtomicDEX. All rights reserved. // use super::eth::Action::{Call, Create}; +#[cfg(feature = "enable-nft-integration")] +use crate::nft::nft_structs::{Chain, ContractType, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; use async_trait::async_trait; use bitcrypto::{keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry}; @@ -97,7 +99,12 @@ pub use rlp; mod web3_transport; #[path = "eth/v2_activation.rs"] pub mod v2_activation; -use v2_activation::build_address_and_priv_key_policy; +#[cfg(feature = "enable-nft-integration")] +use crate::nft::WithdrawNftResult; +use crate::MyWalletAddress; +#[cfg(feature = "enable-nft-integration")] +use crate::{lp_coinfind_or_err, MmCoinEnum, TransactionType}; +use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; mod nonce; use nonce::ParityNonce; @@ -109,6 +116,8 @@ use nonce::ParityNonce; const SWAP_CONTRACT_ABI: &str = include_str!("eth/swap_contract_abi.json"); /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md const ERC20_ABI: &str = include_str!("eth/erc20_abi.json"); +/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md +const ERC721_ABI: &str = include_str!("eth/erc721_abi.json"); /// Payment states from etomic swap smart contract: https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol#L5 pub const PAYMENT_STATE_UNINITIALIZED: u8 = 0; pub const PAYMENT_STATE_SENT: u8 = 1; @@ -147,6 +156,7 @@ const GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC: i64 = 90; lazy_static! { pub static ref SWAP_CONTRACT: Contract = Contract::load(SWAP_CONTRACT_ABI.as_bytes()).unwrap(); pub static ref ERC20_CONTRACT: Contract = Contract::load(ERC20_ABI.as_bytes()).unwrap(); + pub static ref ERC721_CONTRACT: Contract = Contract::load(ERC721_ABI.as_bytes()).unwrap(); } pub type Web3RpcFut = Box> + Send>; @@ -876,6 +886,124 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { }) } +#[cfg(feature = "enable-nft-integration")] +pub async fn withdraw_erc1155(ctx: MmArc, req: WithdrawErc1155) -> WithdrawNftResult { + let ticker = match req.chain { + Chain::Bsc => "BNB", + Chain::Eth => "ETH", + }; + let _coin = lp_coinfind_or_err(&ctx, ticker).await?; + unimplemented!() +} + +#[cfg(feature = "enable-nft-integration")] +pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResult { + let ticker = match req.chain { + Chain::Bsc => "BNB", + Chain::Eth => "ETH", + }; + let coin = lp_coinfind_or_err(&ctx, ticker).await?; + let eth_coin = match coin { + MmCoinEnum::EthCoin(eth_coin) => eth_coin, + _ => { + return MmError::err(WithdrawError::CoinDoesntSupportNftWithdraw { + coin: coin.ticker().to_owned(), + }) + }, + }; + let from_addr = valid_addr_from_str(&req.from).map_to_mm(WithdrawError::InvalidAddress)?; + if eth_coin.my_address != from_addr { + return MmError::err(WithdrawError::AddressMismatchError { + my_address: eth_coin.my_address.to_string(), + from: req.from, + }); + } + let to_addr = valid_addr_from_str(&req.to).map_to_mm(WithdrawError::InvalidAddress)?; + let token_addr = addr_from_str(&req.token_address).map_to_mm(WithdrawError::InvalidAddress)?; + let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { + EthCoinType::Eth => { + let function = ERC721_CONTRACT.function("safeTransferFrom")?; + let token_id_u256 = U256::from_dec_str(&req.token_id.to_string()) + .map_err(|e| format!("{:?}", e)) + .map_to_mm(NumConversError::new)?; + let data = function.encode_input(&[ + Token::Address(from_addr), + Token::Address(to_addr), + Token::Uint(token_id_u256), + ])?; + (0.into(), data, token_addr, eth_coin.ticker()) + }, + EthCoinType::Erc20 { .. } => { + return MmError::err(WithdrawError::InternalError( + "Erc20 coin type doesnt support withdraw nft".to_owned(), + )) + }, + }; + let (gas, gas_price) = match req.fee { + Some(WithdrawFee::EthGas { gas_price, gas }) => { + let gas_price = wei_from_big_decimal(&gas_price, 9)?; + (gas.into(), gas_price) + }, + Some(fee_policy) => { + let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); + return MmError::err(WithdrawError::InvalidFeePolicy(error)); + }, + None => { + let gas_price = eth_coin.get_gas_price().compat().await?; + let estimate_gas_req = CallRequest { + value: Some(eth_value), + data: Some(data.clone().into()), + from: Some(eth_coin.my_address), + to: Some(call_addr), + gas: None, + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + gas_price: Some(gas_price), + ..CallRequest::default() + }; + // Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. + // Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. + let gas_limit = eth_coin.estimate_gas(estimate_gas_req).compat().await?; + (gas_limit, gas_price) + }, + }; + let _nonce_lock = eth_coin.nonce_lock.lock().await; + let nonce = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + + let tx = UnSignedEthTx { + nonce, + value: eth_value, + action: Action::Call(call_addr), + data, + gas, + gas_price, + }; + let secret = eth_coin.priv_key_policy.key_pair_or_err()?.secret(); + let signed = tx.sign(secret, eth_coin.chain_id); + let signed_bytes = rlp::encode(&signed); + let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + Ok(TransactionNftDetails { + tx_hex: BytesJson::from(signed_bytes.to_vec()), + tx_hash: format!("{:02x}", signed.tx_hash()), + from: vec![req.from], + to: vec![req.to], + contract_type: ContractType::Erc721, + token_address: req.token_address, + token_id: req.token_id, + amount: 1.into(), + fee_details: Some(fee_details.into()), + coin: eth_coin.ticker.clone(), + block_height: 0, + timestamp: now_ms() / 1000, + internal_id: 0, + transaction_type: TransactionType::NftTransfer, + }) +} + #[derive(Clone)] pub struct EthCoin(Arc); impl Deref for EthCoin { @@ -4762,3 +4890,37 @@ fn increase_gas_price_by_stage(gas_price: U256, level: &FeeApproxStage) -> U256 }, } } + +#[derive(Debug, Deserialize, Serialize, Display)] +pub enum GetEthAddressError { + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + EthActivationV2Error(EthActivationV2Error), + Internal(String), +} + +impl From for GetEthAddressError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { GetEthAddressError::PrivKeyPolicyNotAllowed(e) } +} + +impl From for GetEthAddressError { + fn from(e: EthActivationV2Error) -> Self { GetEthAddressError::EthActivationV2Error(e) } +} + +impl From for GetEthAddressError { + fn from(e: CryptoCtxError) -> Self { GetEthAddressError::Internal(e.to_string()) } +} + +/// `get_eth_address` returns wallet address for coin with `ETH` protocol type. +pub async fn get_eth_address(ctx: &MmArc, ticker: &str) -> MmResult { + let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(ctx)?; + // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. + let priv_key_policy = EthPrivKeyBuildPolicy::try_from(priv_key_policy)?; + + let (my_address, ..) = build_address_and_priv_key_policy(&ctx.conf, priv_key_policy).await?; + let wallet_address = checksum_address(&format!("{:#02x}", my_address)); + + Ok(MyWalletAddress { + coin: ticker.to_owned(), + wallet_address, + }) +} diff --git a/mm2src/coins/eth/erc1155_abi.json b/mm2src/coins/eth/erc1155_abi.json new file mode 100644 index 0000000000..211a562a85 --- /dev/null +++ b/mm2src/coins/eth/erc1155_abi.json @@ -0,0 +1,314 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/mm2src/coins/eth/erc721_abi.json b/mm2src/coins/eth/erc721_abi.json new file mode 100644 index 0000000000..20e0fca0b4 --- /dev/null +++ b/mm2src/coins/eth/erc721_abi.json @@ -0,0 +1,346 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "_approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 3395cbed70..0fb6a5d02c 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -6,7 +6,7 @@ use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; -#[derive(Display, EnumFromTrait, Serialize, SerializeErrorType)] +#[derive(Debug, Deserialize, Display, EnumFromTrait, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum EthActivationV2Error { InvalidPayload(String), diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 265b3e69ea..f4a883afc7 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -200,7 +200,7 @@ use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut}; pub mod coins_tests; pub mod eth; -use eth::{eth_coin_from_conf_and_request, EthCoin, EthTxFeeDetails, SignedEthTx}; +use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthTxFeeDetails, GetEthAddressError, SignedEthTx}; pub mod hd_confirm_address; pub mod hd_pubkey; @@ -255,6 +255,8 @@ use utxo::utxo_standard::{utxo_standard_coin_with_policy, UtxoStandardCoin}; use utxo::UtxoActivationParams; use utxo::{BlockchainNetwork, GenerateTxError, UtxoFeeDetails, UtxoTx}; +#[cfg(feature = "enable-nft-integration")] pub mod nft; + #[cfg(not(target_arch = "wasm32"))] pub mod z_coin; #[cfg(not(target_arch = "wasm32"))] use z_coin::ZCoin; @@ -331,6 +333,38 @@ impl From for RawTransactionError { } } +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetMyAddressError { + CoinsConfCheckError(String), + CoinIsNotSupported(String), + #[from_stringify("CryptoCtxError")] + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[from_stringify("serde_json::Error")] + #[display(fmt = "Invalid request error error: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Get Eth address error: {}", _0)] + GetEthAddressError(GetEthAddressError), +} + +impl From for GetMyAddressError { + fn from(e: GetEthAddressError) -> Self { GetMyAddressError::GetEthAddressError(e) } +} + +impl HttpStatusCode for GetMyAddressError { + fn status_code(&self) -> StatusCode { + match self { + GetMyAddressError::CoinsConfCheckError(_) + | GetMyAddressError::CoinIsNotSupported(_) + | GetMyAddressError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + GetMyAddressError::Internal(_) | GetMyAddressError::GetEthAddressError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} + #[derive(Deserialize)] pub struct RawTransactionRequest { pub coin: String, @@ -343,6 +377,17 @@ pub struct RawTransactionRes { pub tx_hex: BytesJson, } +#[derive(Debug, Deserialize)] +pub struct MyAddressReq { + coin: String, +} + +#[derive(Debug, Serialize)] +pub struct MyWalletAddress { + coin: String, + wallet_address: String, +} + pub type SignatureResult = Result>; pub type VerificationResult = Result>; @@ -361,7 +406,7 @@ pub enum TxHistoryError { InternalError(String), } -#[derive(Clone, Debug, Display)] +#[derive(Clone, Debug, Display, Deserialize)] pub enum PrivKeyPolicyNotAllowed { #[display(fmt = "Hardware Wallet is not supported")] HardwareWalletNotSupported, @@ -1165,6 +1210,7 @@ pub enum TransactionType { msg_type: CustomTendermintMsgType, token_id: Option, }, + NftTransfer, } /// Transaction details @@ -1730,6 +1776,12 @@ pub enum WithdrawError { #[from_stringify("NumConversError", "UnexpectedDerivationMethod", "PrivKeyPolicyNotAllowed")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), + #[display(fmt = "{} coin doesn't support NFT withdrawing", coin)] + CoinDoesntSupportNftWithdraw { coin: String }, + #[display(fmt = "My address {} and from address {} mismatch", my_address, from)] + AddressMismatchError { my_address: String, from: String }, + #[display(fmt = "Contract type {} doesnt support 'withdraw_nft' yet", _0)] + ContractTypeDoesntSupportNftWithdrawing(String), } impl HttpStatusCode for WithdrawError { @@ -1748,7 +1800,10 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::FromAddressNotFound | WithdrawError::UnexpectedFromAddress(_) | WithdrawError::UnknownAccount { .. } - | WithdrawError::UnexpectedUserAction { .. } => StatusCode::BAD_REQUEST, + | WithdrawError::UnexpectedUserAction { .. } + | WithdrawError::CoinDoesntSupportNftWithdraw { .. } + | WithdrawError::AddressMismatchError { .. } + | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, @@ -1831,7 +1886,7 @@ impl WithdrawError { } } -#[derive(Serialize, Display, Debug, EnumFromStringify, SerializeErrorType)] +#[derive(Debug, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum SignatureError { #[display(fmt = "Invalid request: {}", _0)] @@ -1856,7 +1911,7 @@ impl HttpStatusCode for SignatureError { } } -#[derive(Serialize, Display, Debug, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum VerificationError { #[display(fmt = "Invalid request: {}", _0)] @@ -2478,7 +2533,7 @@ pub trait CoinWithDerivationMethod { } #[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "protocol_data")] pub enum CoinProtocol { UTXO, @@ -2698,34 +2753,11 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result Result; } + +/// `get_my_address` function returns wallet address for necessary coin without its activation. +/// Currently supports only coins with `ETH` protocol type. +pub async fn get_my_address(ctx: MmArc, req: MyAddressReq) -> MmResult { + let ticker = req.coin.as_str(); + let coins_en = coin_conf(&ctx, ticker); + coins_conf_check(&ctx, &coins_en, ticker, None).map_to_mm(GetMyAddressError::CoinsConfCheckError)?; + + let protocol: CoinProtocol = json::from_value(coins_en["protocol"].clone())?; + + let my_address = match protocol { + CoinProtocol::ETH => get_eth_address(&ctx, ticker).await?, + _ => { + return MmError::err(GetMyAddressError::CoinIsNotSupported(format!( + "{} doesn't support get_my_address", + req.coin + ))); + }, + }; + + Ok(my_address) +} + +fn coins_conf_check(ctx: &MmArc, coins_en: &Json, ticker: &str, req: Option<&Json>) -> Result<(), String> { + if coins_en.is_null() { + let warning = format!( + "Warning, coin {} is used without a corresponding configuration.", + ticker + ); + ctx.log.log( + "😅", + #[allow(clippy::unnecessary_cast)] + &[&("coin" as &str), &ticker, &("no-conf" as &str)], + &warning, + ); + } + + if let Some(req) = req { + if coins_en["mm2"].is_null() && req["mm2"].is_null() { + return ERR!(concat!( + "mm2 param is not set neither in coins config nor enable request, assuming that coin is not supported" + )); + } + } else if coins_en["mm2"].is_null() { + return ERR!(concat!( + "mm2 param is not set in coins config, assuming that coin is not supported" + )); + } + + if coins_en["protocol"].is_null() { + return ERR!( + r#""protocol" field is missing in coins file. The file format is deprecated, please execute ./mm2 update_config command to convert it or download a new one"# + ); + } + Ok(()) +} diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index 971ab7b19e..1635be9e80 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -233,7 +233,8 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T TransactionType::StakingDelegation | TransactionType::RemoveDelegation | TransactionType::FeeForTokenTx - | TransactionType::StandardTransfer => tx_hash.clone(), + | TransactionType::StandardTransfer + | TransactionType::NftTransfer => tx_hash.clone(), }; TransactionDetails { diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs new file mode 100644 index 0000000000..0dc9a0aef3 --- /dev/null +++ b/mm2src/coins/nft.rs @@ -0,0 +1,267 @@ +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::{MmError, MmResult}; + +pub(crate) mod nft_errors; +pub(crate) mod nft_structs; + +use crate::WithdrawError; +use nft_errors::GetNftInfoError; +use nft_structs::{Chain, Nft, NftList, NftListReq, NftMetadataReq, NftTransferHistory, NftTransferHistoryWrapper, + NftTransfersReq, NftWrapper, NftsTransferHistoryList, TransactionNftDetails, WithdrawNftReq}; + +use crate::eth::{get_eth_address, withdraw_erc721}; +use common::{APPLICATION_JSON, X_API_KEY}; +use http::header::ACCEPT; +use serde_json::Value as Json; + +/// url for moralis requests +const URL_MORALIS: &str = "https://deep-index.moralis.io/api/v2/"; +/// query parameter for moralis request: The format of the token ID +const FORMAT_DECIMAL_MORALIS: &str = "format=decimal"; +/// query parameter for moralis request: The transfer direction +const DIRECTION_BOTH_MORALIS: &str = "direction=both"; + +pub type WithdrawNftResult = Result>; + +/// `get_nft_list` function returns list of NFTs on ETH or/and BNB chains owned by user. +pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult { + let api_key = ctx.conf["api_key"] + .as_str() + .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; + + let mut res_list = Vec::new(); + + for chain in req.chains { + let (coin_str, chain_str) = match chain { + Chain::Bsc => ("BNB", "bsc"), + Chain::Eth => ("ETH", "eth"), + }; + let my_address = get_eth_address(&ctx, coin_str).await?; + let uri_without_cursor = format!( + "{}{}/nft?chain={}&{}", + URL_MORALIS, my_address.wallet_address, chain_str, FORMAT_DECIMAL_MORALIS + ); + + // The cursor returned in the previous response (used for getting the next page). + let mut cursor = String::new(); + loop { + let uri = format!("{}{}", uri_without_cursor, cursor); + let response = send_moralis_request(uri.as_str(), api_key).await?; + if let Some(nfts_list) = response["result"].as_array() { + for nft_json in nfts_list { + let nft_wrapper: NftWrapper = serde_json::from_str(&nft_json.to_string())?; + let nft = Nft { + chain, + token_address: nft_wrapper.token_address, + token_id: nft_wrapper.token_id.0, + amount: nft_wrapper.amount.0, + owner_of: nft_wrapper.owner_of, + token_hash: nft_wrapper.token_hash, + block_number_minted: *nft_wrapper.block_number_minted, + block_number: *nft_wrapper.block_number, + contract_type: nft_wrapper.contract_type.map(|v| v.0), + name: nft_wrapper.name, + symbol: nft_wrapper.symbol, + token_uri: nft_wrapper.token_uri, + metadata: nft_wrapper.metadata, + last_token_uri_sync: nft_wrapper.last_token_uri_sync, + last_metadata_sync: nft_wrapper.last_metadata_sync, + minter_address: nft_wrapper.minter_address, + }; + // collect NFTs from the page + res_list.push(nft); + } + // if cursor is not null, there are other NFTs on next page, + // and we need to send new request with cursor to get info from the next page. + if let Some(cursor_res) = response["cursor"].as_str() { + cursor = format!("{}{}", "&cursor=", cursor_res); + continue; + } else { + break; + } + } + } + } + drop_mutability!(res_list); + let nft_list = NftList { + count: res_list.len() as u64, + nfts: res_list, + }; + Ok(nft_list) +} + +/// `get_nft_metadata` function returns info of one specific NFT. +/// Current implementation sends request to Moralis. +/// Later, after adding caching, metadata lookup can be performed using previously obtained NFTs info without +/// sending new moralis request. The moralis request can be sent as a fallback, if the data was not found in the cache. +pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult { + let api_key = ctx.conf["api_key"] + .as_str() + .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; + let chain_str = match req.chain { + Chain::Bsc => "bsc", + Chain::Eth => "eth", + }; + let uri = format!( + "{}nft/{}/{}?chain={}&{}", + URL_MORALIS, req.token_address, req.token_id, chain_str, FORMAT_DECIMAL_MORALIS + ); + let response = send_moralis_request(uri.as_str(), api_key).await?; + let nft_wrapper: NftWrapper = serde_json::from_str(&response.to_string())?; + let nft_metadata = Nft { + chain: req.chain, + token_address: nft_wrapper.token_address, + token_id: nft_wrapper.token_id.0, + amount: nft_wrapper.amount.0, + owner_of: nft_wrapper.owner_of, + token_hash: nft_wrapper.token_hash, + block_number_minted: *nft_wrapper.block_number_minted, + block_number: *nft_wrapper.block_number, + contract_type: nft_wrapper.contract_type.map(|v| v.0), + name: nft_wrapper.name, + symbol: nft_wrapper.symbol, + token_uri: nft_wrapper.token_uri, + metadata: nft_wrapper.metadata, + last_token_uri_sync: nft_wrapper.last_token_uri_sync, + last_metadata_sync: nft_wrapper.last_metadata_sync, + minter_address: nft_wrapper.minter_address, + }; + Ok(nft_metadata) +} + +/// `get_nft_transfers` function returns a transfer history of NFTs on ETH or/and BNb chains owned by user. +/// Currently doesnt support filters. +pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult { + let api_key = ctx.conf["api_key"] + .as_str() + .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; + + let mut res_list = Vec::new(); + + for chain in req.chains { + let (coin_str, chain_str) = match chain { + Chain::Bsc => ("BNB", "bsc"), + Chain::Eth => ("ETH", "eth"), + }; + let my_address = get_eth_address(&ctx, coin_str).await?; + let uri_without_cursor = format!( + "{}{}/nft/transfers?chain={}&{}&{}", + URL_MORALIS, my_address.wallet_address, chain_str, FORMAT_DECIMAL_MORALIS, DIRECTION_BOTH_MORALIS + ); + + // The cursor returned in the previous response (used for getting the next page). + let mut cursor = String::new(); + loop { + let uri = format!("{}{}", uri_without_cursor, cursor); + let response = send_moralis_request(uri.as_str(), api_key).await?; + if let Some(transfer_list) = response["result"].as_array() { + for transfer in transfer_list { + let transfer_wrapper: NftTransferHistoryWrapper = serde_json::from_str(&transfer.to_string())?; + let transfer_history = NftTransferHistory { + chain, + block_number: *transfer_wrapper.block_number, + block_timestamp: transfer_wrapper.block_timestamp, + block_hash: transfer_wrapper.block_hash, + transaction_hash: transfer_wrapper.transaction_hash, + transaction_index: transfer_wrapper.transaction_index, + log_index: transfer_wrapper.log_index, + value: transfer_wrapper.value.0, + contract_type: transfer_wrapper.contract_type.0, + transaction_type: transfer_wrapper.transaction_type, + token_address: transfer_wrapper.token_address, + token_id: transfer_wrapper.token_id.0, + from_address: transfer_wrapper.from_address, + to_address: transfer_wrapper.to_address, + amount: transfer_wrapper.amount.0, + verified: transfer_wrapper.verified, + operator: transfer_wrapper.operator, + }; + // collect NFTs transfers from the page + res_list.push(transfer_history); + } + // if the cursor is not null, there are other NFTs transfers on next page, + // and we need to send new request with cursor to get info from the next page. + if let Some(cursor_res) = response["cursor"].as_str() { + cursor = format!("{}{}", "&cursor=", cursor_res); + continue; + } else { + break; + } + } + } + } + drop_mutability!(res_list); + let transfer_history_list = NftsTransferHistoryList { + count: res_list.len() as u64, + transfer_history: res_list, + }; + Ok(transfer_history_list) +} + +/// `withdraw_nft` function generates, signs and returns a transaction that transfers NFT +/// from my address to recipient's address. +/// This method generates a raw transaction which should then be broadcast using `send_raw_transaction`. +/// Currently support ERC721 withdrawing, ERC1155 support will be added later. +pub async fn withdraw_nft(ctx: MmArc, req_type: WithdrawNftReq) -> WithdrawNftResult { + match req_type { + WithdrawNftReq::WithdrawErc1155(_) => MmError::err(WithdrawError::ContractTypeDoesntSupportNftWithdrawing( + "ERC1155".to_owned(), + )), + WithdrawNftReq::WithdrawErc721(erc721_req) => withdraw_erc721(ctx, erc721_req).await, + } +} + +#[cfg(not(target_arch = "wasm32"))] +async fn send_moralis_request(uri: &str, api_key: &str) -> MmResult { + use http::header::HeaderValue; + use mm2_net::transport::slurp_req_body; + + let request = http::Request::builder() + .method("GET") + .uri(uri) + .header(X_API_KEY, api_key) + .header(ACCEPT, HeaderValue::from_static(APPLICATION_JSON)) + .body(hyper::Body::from(""))?; + + let (status, _header, body) = slurp_req_body(request).await?; + if !status.is_success() { + return Err(MmError::new(GetNftInfoError::Transport(format!( + "Response !200 from {}: {}, {}", + uri, status, body + )))); + } + Ok(body) +} + +#[cfg(target_arch = "wasm32")] +async fn send_moralis_request(uri: &str, api_key: &str) -> MmResult { + use mm2_net::wasm_http::FetchRequest; + + macro_rules! try_or { + ($exp:expr, $errtype:ident) => { + match $exp { + Ok(x) => x, + Err(e) => return Err(MmError::new(GetNftInfoError::$errtype(ERRL!("{:?}", e)))), + } + }; + } + + let result = FetchRequest::get(uri) + .cors() + .body_utf8("".to_owned()) + .header(X_API_KEY, api_key) + .header(ACCEPT.as_str(), APPLICATION_JSON) + .request_str() + .await; + let (status_code, response_str) = try_or!(result, Transport); + if !status_code.is_success() { + return Err(MmError::new(GetNftInfoError::Transport(ERRL!( + "!200: {}, {}", + status_code, + response_str + )))); + } + + let response: Json = try_or!(serde_json::from_str(&response_str), InvalidResponse); + Ok(response) +} diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs new file mode 100644 index 0000000000..d48753266b --- /dev/null +++ b/mm2src/coins/nft/nft_errors.rs @@ -0,0 +1,68 @@ +use crate::eth::GetEthAddressError; +use common::HttpStatusCode; +use derive_more::Display; +use enum_from::EnumFromStringify; +use http::StatusCode; +use mm2_net::transport::SlurpError; +use serde::{Deserialize, Serialize}; +use web3::Error; + +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetNftInfoError { + /// `http::Error` can appear on an HTTP request [`http::Builder::build`] building. + #[from_stringify("http::Error")] + #[display(fmt = "Invalid request: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[from_stringify("serde_json::Error")] + #[display(fmt = "Invalid response: {}", _0)] + InvalidResponse(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), + GetEthAddressError(GetEthAddressError), + #[display(fmt = "X-API-Key is missing")] + ApiKeyError, +} + +impl From for GetNftInfoError { + fn from(e: SlurpError) -> Self { + let error_str = e.to_string(); + match e { + SlurpError::ErrorDeserializing { .. } => GetNftInfoError::InvalidResponse(error_str), + SlurpError::Transport { .. } | SlurpError::Timeout { .. } => GetNftInfoError::Transport(error_str), + SlurpError::Internal(_) | SlurpError::InvalidRequest(_) => GetNftInfoError::Internal(error_str), + } + } +} + +impl From for GetNftInfoError { + fn from(e: Error) -> Self { + let error_str = e.to_string(); + match e { + web3::Error::InvalidResponse(_) | web3::Error::Decoder(_) | web3::Error::Rpc(_) => { + GetNftInfoError::InvalidResponse(error_str) + }, + web3::Error::Transport(_) | web3::Error::Io(_) => GetNftInfoError::Transport(error_str), + _ => GetNftInfoError::Internal(error_str), + } + } +} + +impl From for GetNftInfoError { + fn from(e: GetEthAddressError) -> Self { GetNftInfoError::GetEthAddressError(e) } +} + +impl HttpStatusCode for GetNftInfoError { + fn status_code(&self) -> StatusCode { + match self { + GetNftInfoError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + GetNftInfoError::InvalidResponse(_) => StatusCode::FAILED_DEPENDENCY, + GetNftInfoError::ApiKeyError => StatusCode::FORBIDDEN, + GetNftInfoError::Transport(_) | GetNftInfoError::Internal(_) | GetNftInfoError::GetEthAddressError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs new file mode 100644 index 0000000000..efe89cfab4 --- /dev/null +++ b/mm2src/coins/nft/nft_structs.rs @@ -0,0 +1,231 @@ +use crate::{TransactionType, TxFeeDetails, WithdrawFee}; +use mm2_number::BigDecimal; +use rpc::v1::types::Bytes as BytesJson; +use serde::Deserialize; +use std::str::FromStr; + +#[derive(Debug, Deserialize)] +pub struct NftListReq { + pub(crate) chains: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct NftMetadataReq { + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) chain: Chain, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub(crate) enum Chain { + Bsc, + Eth, +} + +#[derive(Debug, Display)] +pub(crate) enum ParseContractTypeError { + UnsupportedContractType, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub(crate) enum ContractType { + Erc1155, + Erc721, +} + +impl FromStr for ContractType { + type Err = ParseContractTypeError; + + #[inline] + fn from_str(s: &str) -> Result { + match s { + "ERC1155" => Ok(ContractType::Erc1155), + "ERC721" => Ok(ContractType::Erc721), + _ => Err(ParseContractTypeError::UnsupportedContractType), + } + } +} + +#[derive(Debug, Serialize)] +pub struct Nft { + pub(crate) chain: Chain, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) amount: BigDecimal, + pub(crate) owner_of: String, + pub(crate) token_hash: String, + pub(crate) block_number_minted: u64, + pub(crate) block_number: u64, + pub(crate) contract_type: Option, + pub(crate) name: Option, + pub(crate) symbol: Option, + pub(crate) token_uri: Option, + pub(crate) metadata: Option, + pub(crate) last_token_uri_sync: Option, + pub(crate) last_metadata_sync: Option, + pub(crate) minter_address: Option, +} + +/// This structure is for deserializing NFT json to struct. +/// Its needed to convert fields properly, because all fields in json have string type. +#[derive(Debug, Deserialize)] +pub(crate) struct NftWrapper { + pub(crate) token_address: String, + pub(crate) token_id: SerdeStringWrap, + pub(crate) amount: SerdeStringWrap, + pub(crate) owner_of: String, + pub(crate) token_hash: String, + pub(crate) block_number_minted: SerdeStringWrap, + pub(crate) block_number: SerdeStringWrap, + pub(crate) contract_type: Option>, + pub(crate) name: Option, + pub(crate) symbol: Option, + pub(crate) token_uri: Option, + pub(crate) metadata: Option, + pub(crate) last_token_uri_sync: Option, + pub(crate) last_metadata_sync: Option, + pub(crate) minter_address: Option, +} + +#[derive(Debug)] +pub(crate) struct SerdeStringWrap(pub(crate) T); + +impl<'de, T> Deserialize<'de> for SerdeStringWrap +where + T: std::str::FromStr, + T::Err: std::fmt::Debug + std::fmt::Display, +{ + fn deserialize>(deserializer: D) -> Result { + let value: &str = Deserialize::deserialize(deserializer)?; + let value: T = match value.parse() { + Ok(v) => v, + Err(e) => return Err(::custom(e)), + }; + Ok(SerdeStringWrap(value)) + } +} + +impl std::ops::Deref for SerdeStringWrap { + type Target = T; + fn deref(&self) -> &T { &self.0 } +} + +#[derive(Debug, Serialize)] +pub struct NftList { + pub(crate) count: u64, + pub(crate) nfts: Vec, +} + +#[allow(dead_code)] +#[derive(Clone, Deserialize)] +pub struct WithdrawErc1155 { + pub(crate) chain: Chain, + from: String, + to: String, + token_address: String, + token_id: BigDecimal, + amount: BigDecimal, + #[serde(default)] + max: bool, + fee: Option, +} + +#[derive(Clone, Deserialize)] +pub struct WithdrawErc721 { + pub(crate) chain: Chain, + pub(crate) from: String, + pub(crate) to: String, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) fee: Option, +} + +#[derive(Clone, Deserialize)] +#[serde(tag = "type", content = "withdraw_data")] +pub enum WithdrawNftReq { + WithdrawErc1155(WithdrawErc1155), + WithdrawErc721(WithdrawErc721), +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TransactionNftDetails { + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction + pub(crate) tx_hex: BytesJson, + pub(crate) tx_hash: String, + /// NFTs are sent from these addresses + pub(crate) from: Vec, + /// NFTs are sent to these addresses + pub(crate) to: Vec, + pub(crate) contract_type: ContractType, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) amount: BigDecimal, + pub(crate) fee_details: Option, + /// The coin transaction belongs to + pub(crate) coin: String, + /// Block height + pub(crate) block_height: u64, + /// Transaction timestamp + pub(crate) timestamp: u64, + /// Internal MM2 id used for internal transaction identification, for some coins it might be equal to transaction hash + pub(crate) internal_id: i64, + /// Type of transactions, default is StandardTransfer + #[serde(default)] + pub(crate) transaction_type: TransactionType, +} + +#[derive(Debug, Deserialize)] +pub struct NftTransfersReq { + pub(crate) chains: Vec, +} + +#[derive(Debug, Serialize)] +pub(crate) struct NftTransferHistory { + pub(crate) chain: Chain, + pub(crate) block_number: u64, + pub(crate) block_timestamp: String, + pub(crate) block_hash: String, + /// Transaction hash in hexadecimal format + pub(crate) transaction_hash: String, + pub(crate) transaction_index: u64, + pub(crate) log_index: u64, + pub(crate) value: BigDecimal, + pub(crate) contract_type: ContractType, + pub(crate) transaction_type: String, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) from_address: String, + pub(crate) to_address: String, + pub(crate) amount: BigDecimal, + pub(crate) verified: u64, + pub(crate) operator: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct NftTransferHistoryWrapper { + pub(crate) block_number: SerdeStringWrap, + pub(crate) block_timestamp: String, + pub(crate) block_hash: String, + /// Transaction hash in hexadecimal format + pub(crate) transaction_hash: String, + pub(crate) transaction_index: u64, + pub(crate) log_index: u64, + pub(crate) value: SerdeStringWrap, + pub(crate) contract_type: SerdeStringWrap, + pub(crate) transaction_type: String, + pub(crate) token_address: String, + pub(crate) token_id: SerdeStringWrap, + pub(crate) from_address: String, + pub(crate) to_address: String, + pub(crate) amount: SerdeStringWrap, + pub(crate) verified: u64, + pub(crate) operator: Option, +} + +#[derive(Debug, Serialize)] +pub struct NftsTransferHistoryList { + pub(crate) count: u64, + pub(crate) transfer_history: Vec, +} diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 5a8edbd7da..e2f638c02f 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -174,6 +174,7 @@ cfg_wasm32! { } pub const X_GRPC_WEB: &str = "x-grpc-web"; +pub const X_API_KEY: &str = "X-API-Key"; pub const APPLICATION_JSON: &str = "application/json"; pub const APPLICATION_GRPC_WEB: &str = "application/grpc-web"; pub const APPLICATION_GRPC_WEB_PROTO: &str = "application/grpc-web+proto"; diff --git a/mm2src/common/shared_ref_counter/src/enable.rs b/mm2src/common/shared_ref_counter/src/enable.rs index bc4f6a0a67..205b36aab7 100644 --- a/mm2src/common/shared_ref_counter/src/enable.rs +++ b/mm2src/common/shared_ref_counter/src/enable.rs @@ -77,7 +77,7 @@ impl SharedRc { let existing_pointers = self.existing_pointers.read().expect(LOCKING_ERROR); log!(level, "{} exists at:", ident); for (_idx, location) in existing_pointers.iter() { - log!(level, "\t{}", stringify_location(*location)); + log!(level, "\t{}", stringify_location(location)); } } diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 11f98cb5ea..2272802a1f 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -20,6 +20,7 @@ zhtlc-native-tests = ["coins/zhtlc-native-tests"] # Remove this once the solana integration becomes stable/completed. disable-solana-tests = [] default = ["disable-solana-tests"] +enable-nft-integration = ["coins/enable-nft-integration"] [dependencies] async-std = { version = "1.5", features = ["unstable"] } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index a553ff9c51..ec51a48f15 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -11,6 +11,7 @@ use crate::{mm2::lp_stats::{add_node_to_version_stat, remove_node_from_version_s mm2::rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; +#[cfg(feature = "enable-nft-integration")] use coins::nft; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, get_enabled_coins::get_enabled_coins, @@ -28,8 +29,8 @@ use coins::utxo::bch::BchCoin; use coins::utxo::qtum::QtumCoin; use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; -use coins::{add_delegation, get_raw_transaction, get_staking_infos, remove_delegation, sign_message, verify_message, - withdraw}; +use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_infos, remove_delegation, sign_message, + verify_message, withdraw}; #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] use coins::{SolanaCoin, SplToken}; use coins_activation::{cancel_init_l2, cancel_init_standalone_coin, enable_platform_coin_with_tokens, enable_token, @@ -42,6 +43,8 @@ use http::Response; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_rpc::mm_protocol::{MmRpcBuilder, MmRpcRequest, MmRpcVersion}; +#[cfg(feature = "enable-nft-integration")] +use nft::{get_nft_list, get_nft_metadata, get_nft_transfers, withdraw_nft}; use serde::de::DeserializeOwned; use serde_json::{self as json, Value as Json}; use std::net::SocketAddr; @@ -159,7 +162,14 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, get_current_mtp_rpc).await, "get_enabled_coins" => handle_mmrpc(ctx, request, get_enabled_coins).await, "get_locked_amount" => handle_mmrpc(ctx, request, get_locked_amount_rpc).await, + "get_my_address" => handle_mmrpc(ctx, request, get_my_address).await, "get_new_address" => handle_mmrpc(ctx, request, get_new_address).await, + #[cfg(feature = "enable-nft-integration")] + "get_nft_list" => handle_mmrpc(ctx, request, get_nft_list).await, + #[cfg(feature = "enable-nft-integration")] + "get_nft_metadata" => handle_mmrpc(ctx, request, get_nft_metadata).await, + #[cfg(feature = "enable-nft-integration")] + "get_nft_transfers" => handle_mmrpc(ctx, request, get_nft_transfers).await, "get_public_key" => handle_mmrpc(ctx, request, get_public_key).await, "get_public_key_hash" => handle_mmrpc(ctx, request, get_public_key_hash).await, "get_raw_transaction" => handle_mmrpc(ctx, request, get_raw_transaction).await, @@ -181,6 +191,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, update_version_stat_collection).await, "verify_message" => handle_mmrpc(ctx, request, verify_message).await, "withdraw" => handle_mmrpc(ctx, request, withdraw).await, + #[cfg(feature = "enable-nft-integration")] + "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, #[cfg(not(target_arch = "wasm32"))] native_only_methods => match native_only_methods { #[cfg(all(not(target_os = "ios"), not(target_os = "android")))] diff --git a/mm2src/mm2_metamask/src/metamask_error.rs b/mm2src/mm2_metamask/src/metamask_error.rs index 29e3201b9a..68db8af025 100644 --- a/mm2src/mm2_metamask/src/metamask_error.rs +++ b/mm2src/mm2_metamask/src/metamask_error.rs @@ -1,7 +1,7 @@ use derive_more::Display; use jsonrpc_core::{Error as RPCError, ErrorCode as RpcErrorCode}; use mm2_err_handle::prelude::*; -use serde_derive::Serialize; +use serde_derive::{Deserialize, Serialize}; use web3::Error as Web3Error; const USER_CANCELLED_ERROR_CODE: RpcErrorCode = RpcErrorCode::ServerError(4001); @@ -55,7 +55,7 @@ impl From for MetamaskError { /// so please extend it if it's required **only**. /// /// Please also note that this enum is fieldless. -#[derive(Clone, Debug, Display, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Display, Serialize, PartialEq)] pub enum MetamaskRpcError { EthProviderNotFound, #[display(fmt = "User cancelled request")] diff --git a/mm2src/mm2_net/src/native_http.rs b/mm2src/mm2_net/src/native_http.rs index ea4b0b2912..641a0cae24 100644 --- a/mm2src/mm2_net/src/native_http.rs +++ b/mm2src/mm2_net/src/native_http.rs @@ -1,10 +1,11 @@ -use crate::transport::{SlurpError, SlurpResult}; +use crate::transport::{SlurpError, SlurpResult, SlurpResultJson}; use common::wio::{drive03, HYPER}; use common::APPLICATION_JSON; use futures::channel::oneshot::Canceled; use http::{header, Request}; use hyper::Body; use mm2_err_handle::prelude::*; +use serde_json::Value as Json; impl From for SlurpError { fn from(_: Canceled) -> Self { SlurpError::Internal("Spawned Slurp future has been canceled".to_owned()) } @@ -49,6 +50,25 @@ pub async fn slurp_req(request: Request>) -> SlurpResult { Ok((status, headers, output.to_vec())) } +/// Executes a Hyper request, requires [`Request`] and return the response status, headers and body as Json. +pub async fn slurp_req_body(request: Request) -> SlurpResultJson { + let uri = request.uri().to_string(); + + let request_f = HYPER.request(request); + let response = drive03(request_f) + .await? + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let status = response.status(); + let headers = response.headers().clone(); + // Get the response body bytes. + let body_bytes = hyper::body::to_bytes(response.into_body()) + .await + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let body_str = String::from_utf8(body_bytes.to_vec()).map_to_mm(|e| SlurpError::Internal(e.to_string()))?; + let body: Json = serde_json::from_str(&body_str)?; + Ok((status, headers, body)) +} + /// Executes a GET request, returning the response status, headers and body. pub async fn slurp_url(url: &str) -> SlurpResult { let req = Request::builder().uri(url).body(Vec::new())?; diff --git a/mm2src/mm2_net/src/transport.rs b/mm2src/mm2_net/src/transport.rs index ad51122993..18e4f4feb6 100644 --- a/mm2src/mm2_net/src/transport.rs +++ b/mm2src/mm2_net/src/transport.rs @@ -4,15 +4,18 @@ use ethkey::Secret; use http::{HeaderMap, StatusCode}; use mm2_err_handle::prelude::*; use serde::{Deserialize, Serialize}; +use serde_json::{Error, Value as Json}; #[cfg(not(target_arch = "wasm32"))] -pub use crate::native_http::{slurp_post_json, slurp_req, slurp_url}; +pub use crate::native_http::{slurp_post_json, slurp_req, slurp_req_body, slurp_url}; #[cfg(target_arch = "wasm32")] pub use crate::wasm_http::{slurp_post_json, slurp_url}; pub type SlurpResult = Result<(StatusCode, HeaderMap, Vec), MmError>; +pub type SlurpResultJson = Result<(StatusCode, HeaderMap, Json), MmError>; + #[derive(Debug, Deserialize, Display, Serialize)] pub enum SlurpError { #[display(fmt = "Error deserializing '{}' response: {}", uri, error)] @@ -27,6 +30,10 @@ pub enum SlurpError { Internal(String), } +impl From for SlurpError { + fn from(e: Error) -> Self { SlurpError::Internal(e.to_string()) } +} + impl From for JsonRpcErrorType { fn from(err: SlurpError) -> Self { match err {