From f1ecf81fcde0ab5e2967776ea83ec2cc257ba54b Mon Sep 17 00:00:00 2001 From: dimxy Date: Sun, 12 Nov 2023 18:55:53 +0500 Subject: [PATCH] add support for trezor evm initialisation with rpc task manager and withdraw eth from trezor account (WiP) --- Cargo.lock | 3 + mm2src/coins/eth.rs | 129 ++++++- mm2src/coins/eth/eth_hd_wallet.rs | 7 +- mm2src/coins/eth/eth_withdraw.rs | 345 ++++++++++++++++++ mm2src/coins/eth/v2_activation.rs | 118 +++++- mm2src/coins/hd_wallet/pubkey.rs | 50 ++- mm2src/coins/lp_coins.rs | 86 ++++- mm2src/coins/rpc_command/account_balance.rs | 3 +- .../coins/rpc_command/init_create_account.rs | 6 +- mm2src/coins/rpc_command/init_withdraw.rs | 1 + mm2src/coins/utxo.rs | 64 ---- .../utxo/utxo_builder/utxo_coin_builder.rs | 5 +- mm2src/coins/utxo/utxo_common.rs | 6 +- mm2src/coins/utxo/utxo_withdraw.rs | 2 +- mm2src/coins/utxo_signer/src/sign_common.rs | 4 + .../src/bch_with_tokens_activation.rs | 41 ++- mm2src/coins_activation/src/context.rs | 3 + .../src/eth_with_token_activation.rs | 86 +++-- mm2src/coins_activation/src/lib.rs | 1 + .../src/platform_coin_with_tokens.rs | 258 ++++++++++++- .../src/solana_with_tokens_activation.rs | 41 +++ .../src/tendermint_with_assets_activation.rs | 22 +- .../src/utxo_activation/common_impl.rs | 2 +- mm2src/crypto/src/hw_error.rs | 2 +- mm2src/mm2_main/src/wasm_tests.rs | 17 +- .../tests/mm2_tests/mm2_tests_inner.rs | 178 +++++++-- mm2src/mm2_test_helpers/src/for_tests.rs | 17 +- mm2src/trezor/Cargo.toml | 5 + mm2src/trezor/build.rs | 6 +- .../proto/messages-ethereum-definitions.proto | 60 +++ mm2src/trezor/proto/messages-ethereum.proto | 181 +++++++++ mm2src/trezor/src/eth/eth_command.rs | 149 ++++++++ mm2src/trezor/src/eth/mod.rs | 1 + mm2src/trezor/src/lib.rs | 1 + mm2src/trezor/src/proto/messages_ethereum.rs | 141 +++++++ .../proto/messages_ethereum_definitions.rs | 12 + mm2src/trezor/src/proto/mod.rs | 12 + 37 files changed, 1864 insertions(+), 201 deletions(-) create mode 100644 mm2src/coins/eth/eth_withdraw.rs create mode 100644 mm2src/trezor/proto/messages-ethereum-definitions.proto create mode 100644 mm2src/trezor/proto/messages-ethereum.proto create mode 100644 mm2src/trezor/src/eth/eth_command.rs create mode 100644 mm2src/trezor/src/eth/mod.rs create mode 100644 mm2src/trezor/src/proto/messages_ethereum.rs create mode 100644 mm2src/trezor/src/proto/messages_ethereum_definitions.rs diff --git a/Cargo.lock b/Cargo.lock index 9ce2306f90..231a5fa92a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8461,10 +8461,13 @@ dependencies = [ "byteorder", "common", "derive_more", + "ethcore-transaction", + "ethkey", "futures 0.3.28", "hw_common", "js-sys", "mm2_err_handle", + "primitive-types", "prost", "rand 0.7.3", "rpc_task", diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 71dec71286..27d8d9868d 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -28,6 +28,7 @@ use crate::hd_wallet::{HDAccountAddressId, HDAccountOps, HDCoinAddress, HDCoinHD use crate::lp_price::get_base_price_in_rel; use crate::nft::nft_structs::{ContractType, ConvertChain, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; use crate::nft::{find_wallet_nft_amount, WithdrawNftResult}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; use crate::{coin_balance, scan_for_new_addresses_impl, BalanceResult, CoinWithDerivationMethod, DerivationMethod, PrivKeyPolicy, TransactionResult, ValidateWatcherSpendInput, WatcherSpendType}; use async_trait::async_trait; @@ -98,6 +99,7 @@ use eth_hd_wallet::EthHDWallet; #[path = "eth/v2_activation.rs"] pub mod v2_activation; use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; +mod eth_withdraw; mod nonce; use crate::coin_balance::{EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDAddressBalanceScanner, HDBalanceAddress, HDWalletBalance, HDWalletBalanceOps}; @@ -112,6 +114,7 @@ use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, S ScanAddressesResponse}; use crate::rpc_command::{account_balance, get_new_address, init_account_balance, init_create_account, init_scan_for_new_addresses}; +use eth_withdraw::{EthWithdraw, InitEthWithdraw, StandardEthWithdraw}; use nonce::ParityNonce; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol @@ -141,7 +144,7 @@ const DEFAULT_LOGS_BLOCK_RANGE: u64 = 1000; const DEFAULT_REQUIRED_CONFIRMATIONS: u8 = 1; -const ETH_DECIMALS: u8 = 18; +pub const ETH_DECIMALS: u8 = 18; /// Take into account that the dynamic fee may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_START_SWAP: u64 = 3; @@ -157,7 +160,7 @@ const GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE: u64 = 5; /// - it may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE: u64 = 7; -const ETH_GAS: u64 = 150_000; +pub const ETH_GAS: u64 = 150_000; /// Lifetime of generated signed message for gui-auth requests const GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC: i64 = 90; @@ -385,6 +388,7 @@ pub enum EthPrivKeyBuildPolicy { GlobalHDAccount(GlobalHDAccountArc), #[cfg(target_arch = "wasm32")] Metamask(MetamaskArc), + Trezor, } impl EthPrivKeyBuildPolicy { @@ -412,7 +416,7 @@ impl TryFrom for EthPrivKeyBuildPolicy { match policy { PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), - PrivKeyBuildPolicy::Trezor => Err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), + PrivKeyBuildPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), // Err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), } } } @@ -729,7 +733,7 @@ async fn get_tx_hex_by_hash_impl(coin: EthCoin, tx_hash: H256) -> RawTransaction } // Todo: use builder pattern for this function similar to StandardUtxoWithdraw/InitUtxoWithdraw, this will be needed for Trezor implementation -async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { +/*async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { let to_addr = coin .address_from_str(&req.to) .map_to_mm(WithdrawError::InvalidAddress)?; @@ -899,6 +903,23 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { transaction_type: Default::default(), memo: None, }) +}*/ + +async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { + StandardEthWithdraw::new(coin.clone(), req)?.build().await +} + +#[async_trait] +impl InitWithdrawCoin for EthCoin { + async fn init_withdraw( + &self, + ctx: MmArc, + req: WithdrawRequest, + task_handle: &WithdrawTaskHandle, + ) -> Result> { + //init_withdraw(ctx, self.clone(), req, task_handle).await + InitEthWithdraw::new(ctx, self.clone(), req, task_handle)?.build().await + } } /// `withdraw_erc1155` function returns details of `ERC-1155` transaction including tx hex, @@ -1347,7 +1368,7 @@ impl SwapOps for EthCoin { activated_key: ref key_pair, .. } => key_pair_from_secret(key_pair.secret().as_bytes()).expect("valid key"), - EthPrivKeyPolicy::Trezor => todo!(), + EthPrivKeyPolicy::Trezor { .. } => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => todo!(), } @@ -1364,7 +1385,7 @@ impl SwapOps for EthCoin { .expect("valid key") .public_slice() .to_vec(), - EthPrivKeyPolicy::Trezor => todo!(), + EthPrivKeyPolicy::Trezor { .. } => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => metamask_policy.public_key.as_bytes().to_vec(), } @@ -2112,7 +2133,11 @@ impl MarketCoinOps for EthCoin { let uncompressed_without_prefix = hex::encode(key_pair.public()); Ok(format!("04{}", uncompressed_without_prefix)) }, - EthPrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::Trezor), + EthPrivKeyPolicy::Trezor { + ref activated_pubkey, .. + } => activated_pubkey + .clone() + .or_mm_err(|| UnexpectedDerivationMethod::InternalError("no trezor pubkey".to_string())), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => { Ok(format!("{:02x}", metamask_policy.public_key_uncompressed)) @@ -2412,7 +2437,7 @@ impl MarketCoinOps for EthCoin { activated_key: ref key_pair, .. } => Ok(format!("{:#02x}", key_pair.secret())), - EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Trezor yet!"), + EthPrivKeyPolicy::Trezor { .. } => ERR!("'display_priv_key' doesn't support Trezor yet!"), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support MetaMask"), } @@ -2490,6 +2515,63 @@ async fn sign_and_send_transaction_with_keypair( Ok(signed) } +/// TODO: use when Trezor is supported for swaps +#[allow(dead_code)] +async fn sign_and_send_transaction_with_trezor( + ctx: MmArc, + coin: &EthCoin, + derivation_path: DerivationPath, + value: U256, + action: Action, + data: Vec, + gas: U256, +) -> Result { + let mut status = ctx.log.status_handle(); + macro_rules! tags { + () => { + &[&"sign-and-send"] + }; + } + let _nonce_lock = coin.nonce_lock.lock().await; + status.status(tags!(), "get_addr_nonce…"); + let my_address = try_tx_s!(coin.derivation_method.single_addr_or_err().await); + let (nonce, web3_instances_with_latest_nonce) = + try_tx_s!(get_addr_nonce(my_address, coin.web3_instances.clone()).compat().await); + status.status(tags!(), "get_gas_price…"); + let gas_price = try_tx_s!(coin.get_gas_price().compat().await); + + let tx = UnSignedEthTx { + nonce, + gas_price, + gas, + action, + value, + data, + }; + + let crypto_ctx = try_tx_s!(CryptoCtx::from_ctx(&ctx)); + let hw_ctx = try_tx_s!(crypto_ctx + .hw_ctx() + .ok_or_else(|| ERRL!("{}", "no hardware wallet initialized"))); + let mut trezor_session = try_tx_s!(hw_ctx.trezor().await); + let chain_id = try_tx_s!(coin + .chain_id + .ok_or_else(|| ERRL!("{}", "chain_id is required for Trezor wallet"))); + let unverified_tx = try_tx_s!(trezor_session.sign_eth_tx(derivation_path, &tx, chain_id).await); + let signed_tx = try_tx_s!(SignedEthTx::new(unverified_tx)); + let bytes = Bytes(rlp::encode(&signed_tx).to_vec()); + + status.status(tags!(), "send_raw_transaction…"); + let futures = web3_instances_with_latest_nonce + .into_iter() + .map(|web3_instance| web3_instance.web3.eth().send_raw_transaction(bytes.clone())); + try_tx_s!(select_ok(futures).await.map_err(|e| ERRL!("{}", e)), signed_tx); + + status.status(tags!(), "get_addr_nonce…"); + coin.wait_for_addr_nonce_increase(my_address, nonce).await; + Ok(signed_tx) +} + #[cfg(target_arch = "wasm32")] async fn sign_and_send_transaction_with_metamask( coin: EthCoin, @@ -3299,7 +3381,9 @@ impl EthCoin { activated_key: ref key_pair, .. } => sign_and_send_transaction_with_keypair(ctx, &coin, key_pair, value, action, data, gas).await, - EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for EVM yet!"))), + EthPrivKeyPolicy::Trezor { .. } => { + Err(TransactionErr::Plain(ERRL!("Trezor is not supported for EVM yet!"))) + }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { sign_and_send_transaction_with_metamask(coin, value, action, data, gas).await @@ -5450,6 +5534,7 @@ fn rpc_event_handlers_for_eth_transport(ctx: &MmArc, ticker: String) -> Vec Arc> { Arc::new(AsyncMutex::new(())) } +/// Activate eth coin or erc20 token from coin config and private key build policy pub async fn eth_coin_from_conf_and_request( ctx: &MmArc, ticker: &str, @@ -5772,9 +5857,18 @@ pub async fn get_eth_address( ticker: &str, path_to_address: &HDAccountAddressId, ) -> MmResult { - let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(ctx)?; + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let priv_key_policy = if crypto_ctx.hw_ctx().is_some() { + PrivKeyBuildPolicy::Trezor + } else { + PrivKeyBuildPolicy::detect_priv_key_policy(ctx)? + }; // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. let priv_key_policy = EthPrivKeyBuildPolicy::try_from(priv_key_policy)?; + println!( + "eth priv_key_policy is trezor={:?}", + matches!(priv_key_policy, EthPrivKeyBuildPolicy::Trezor) + ); // Todo: This creates an HD wallet different from the ETH one for NFT, we should combine them in the future when implementing NFT HD wallet let (_, derivation_method) = @@ -5782,9 +5876,10 @@ pub async fn get_eth_address( let my_address = match derivation_method { EthDerivationMethod::SingleAddress(my_address) => my_address, EthDerivationMethod::HDWallet(_) => { + //hd_wallet.derive_address(path_to_address.account_id, path_to_address.chain, path_to_address.account_id).await?.address() return Err(MmError::new(GetEthAddressError::UnexpectedDerivationMethod( UnexpectedDerivationMethod::UnsupportedError("HDWallet is not supported for NFT yet!".to_owned()), - ))) + ))); }, }; @@ -5974,3 +6069,15 @@ impl InitCreateAccountRpcOps for EthCoin { init_create_account::common_impl::revert_creating_account(self, account_id).await } } + +pub fn pubkey_from_extended(extended_pubkey: &Secp256k1ExtendedPublicKey) -> Public { + let serialized = extended_pubkey.public_key().serialize_uncompressed(); + let mut pubkey_uncompressed = Public::default(); + pubkey_uncompressed.as_mut().copy_from_slice(&serialized[1..]); + pubkey_uncompressed +} + +pub fn pubkey_from_xpub_str(xpub: &str) -> Option { + let extended_pubkey = Secp256k1ExtendedPublicKey::from_str(xpub).ok()?; + Some(pubkey_from_extended(&extended_pubkey)) +} diff --git a/mm2src/coins/eth/eth_hd_wallet.rs b/mm2src/coins/eth/eth_hd_wallet.rs index 64e3ceb32e..b3b297e039 100644 --- a/mm2src/coins/eth/eth_hd_wallet.rs +++ b/mm2src/coins/eth/eth_hd_wallet.rs @@ -35,13 +35,8 @@ impl HDWalletCoinOps for EthCoin { extended_pubkey: &Secp256k1ExtendedPublicKey, derivation_path: DerivationPath, ) -> HDCoinHDAddress { - let serialized = extended_pubkey.public_key().serialize_uncompressed(); - let mut pubkey = Public::default(); - pubkey.as_mut().copy_from_slice(&serialized[1..65]); - drop_mutability!(pubkey); - + let pubkey = pubkey_from_extended(extended_pubkey); let address = public_to_address(&pubkey); - EthHDAddress { address, pubkey, diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs new file mode 100644 index 0000000000..1f32d4e796 --- /dev/null +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -0,0 +1,345 @@ +use std::ops::Deref; +use super::{checksum_address, get_addr_nonce, get_eth_gas_details, pubkey_from_xpub_str, u256_to_big_decimal, + wei_from_big_decimal, EthCoinType, EthPrivKeyPolicy, WithdrawError, WithdrawRequest, WithdrawResult, + ERC20_CONTRACT}; +use crate::eth::{Action, EthTxFeeDetails, KeyPair, SignedEthTx, UnSignedEthTx}; +use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandle}; +use crate::{BytesJson, EthCoin, TransactionDetails}; +use async_trait::async_trait; +use bip32::DerivationPath; +use common::custom_futures::timeout::FutureTimerExt; +use common::now_sec; +use crypto::{CryptoCtx, HwRpcError}; +use ethabi::Token; +use ethkey::public_to_address; +use futures::compat::Future01CompatExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::{MapToMmResult, MmError, OrMmError}; + +#[async_trait] +pub trait EthWithdraw +where + Self: Sized + Sync, +{ + fn coin(&self) -> &EthCoin; + + fn request(&self) -> &WithdrawRequest; + + #[allow(clippy::result_large_err)] + fn on_generating_transaction(&self) -> Result<(), MmError>; + + #[allow(clippy::result_large_err)] + fn on_finishing(&self) -> Result<(), MmError>; + + async fn sign_tx_with_trezor( + &self, + derivation_path: DerivationPath, + unsigned_tx: &UnSignedEthTx, + ) -> Result>; + + async fn build(self) -> WithdrawResult { + let coin = self.coin(); + let ticker = coin.deref().ticker.clone(); + let req = self.request().clone(); + + let to_addr = coin + .address_from_str(&req.to) + .map_to_mm(WithdrawError::InvalidAddress)?; + let (my_balance, my_address, key_pair, derivation_path) = match req.from { + Some(from) => { + let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; + let path_to_address = from.to_address_path(path_to_coin.coin_type())?; + let derivation_path = path_to_address.to_derivation_path(path_to_coin)?; + let (key_pair, address) = match coin.priv_key_policy { + EthPrivKeyPolicy::Trezor { + ref activated_pubkey, .. + } => { + let my_pubkey = activated_pubkey + .as_ref() + .or_mm_err(|| WithdrawError::InternalError("no pubkey from trezor".to_string()))?; + let my_pubkey = pubkey_from_xpub_str(my_pubkey) + .ok_or_else(|| WithdrawError::InternalError("invalid xpub from trezor".to_string()))?; + let address = public_to_address(&my_pubkey); + (None, address) + }, + _ => { + let raw_priv_key = coin + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(&derivation_path)?; + + let key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let address = key_pair.address(); + (Some(key_pair), address) + }, + }; + let balance = coin.address_balance(address).compat().await?; + (balance, address, key_pair, Some(derivation_path)) + }, + None => { + let my_address = coin.derivation_method.single_addr_or_err().await?; + ( + coin.my_balance().compat().await?, + my_address, + Some(coin.priv_key_policy.activated_key_or_err()?.clone()), + None, + ) + }, + }; + let my_balance_dec = u256_to_big_decimal(my_balance, coin.decimals)?; + + let (mut wei_amount, dec_amount) = if req.max { + (my_balance, my_balance_dec.clone()) + } else { + let wei_amount = wei_from_big_decimal(&req.amount, coin.decimals)?; + (wei_amount, req.amount.clone()) + }; + if wei_amount > my_balance { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: my_balance_dec.clone(), + required: dec_amount, + }); + }; + let (mut eth_value, data, call_addr, fee_coin) = match &coin.coin_type { + EthCoinType::Eth => (wei_amount, vec![], to_addr, ticker.as_str()), + EthCoinType::Erc20 { platform, token_addr } => { + let function = ERC20_CONTRACT.function("transfer")?; + let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(wei_amount)])?; + (0.into(), data, *token_addr, platform.as_str()) + }, + }; + let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; + + let (gas, gas_price) = get_eth_gas_details( + coin, + req.fee, + eth_value, + data.clone().into(), + my_address, + call_addr, + false, + ) + .await?; + let total_fee = gas * gas_price; + let total_fee_dec = u256_to_big_decimal(total_fee, coin.decimals)?; + + if req.max && coin.coin_type == EthCoinType::Eth { + if eth_value < total_fee || wei_amount < total_fee { + return MmError::err(WithdrawError::AmountTooLow { + amount: eth_value_dec, + threshold: total_fee_dec, + }); + } + eth_value -= total_fee; + wei_amount -= total_fee; + }; + + let _nonce_lock = coin.nonce_lock.lock().await; + let (nonce, _) = get_addr_nonce(my_address, 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 (tx_hash, tx_hex) = match coin.priv_key_policy { + EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { + let key_pair = key_pair.ok_or_else(|| WithdrawError::InternalError("no keypair found".to_string()))?; + // Todo: nonce_lock is still global for all addresses but this needs to be per address + let signed = tx.sign(key_pair.secret(), coin.chain_id); + let bytes = rlp::encode(&signed); + + (signed.hash, BytesJson::from(bytes.to_vec())) + }, + EthPrivKeyPolicy::Trezor { .. } => { + let derivation_path = derivation_path.or_mm_err(|| WithdrawError::FromAddressNotFound)?; + let signed = self.sign_tx_with_trezor(derivation_path, &tx).await?; + let bytes = rlp::encode(&signed); + + (signed.hash, BytesJson::from(bytes.to_vec())) + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => { + if !req.broadcast { + let error = + "Set 'broadcast' to generate, sign and broadcast a transaction with MetaMask".to_string(); + return MmError::err(WithdrawError::BroadcastExpected(error)); + } + + let tx_to_send = TransactionRequest { + from: coin.my_address(), + to: Some(to_addr), + gas: Some(gas), + gas_price: Some(gas_price), + value: Some(eth_value), + data: Some(data.clone().into()), + nonce: None, + ..TransactionRequest::default() + }; + + // Wait for 10 seconds for the transaction to appear on the RPC node. + let wait_rpc_timeout = 10_000; + let check_every = 1.; + + // Please note that this method may take a long time + // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. + let tx_hash = coin.web3.eth().send_transaction(tx_to_send).await?; + + let signed_tx = coin + .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) + .await?; + let tx_hex = signed_tx + .map(|tx| BytesJson::from(rlp::encode(&tx).to_vec())) + // Return an empty `tx_hex` if the transaction is still not appeared on the RPC node. + .unwrap_or_default(); + (tx_hash, tx_hex) + }, + }; + + let tx_hash_bytes = BytesJson::from(tx_hash.0.to_vec()); + let tx_hash_str = format!("{:02x}", tx_hash_bytes); + + let amount_decimal = u256_to_big_decimal(wei_amount, coin.decimals)?; + let mut spent_by_me = amount_decimal.clone(); + let received_by_me = if to_addr == my_address { + amount_decimal.clone() + } else { + 0.into() + }; + let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + if coin.coin_type == EthCoinType::Eth { + spent_by_me += &fee_details.total_fee; + } + Ok(TransactionDetails { + to: vec![checksum_address(&format!("{:#02x}", to_addr))], + from: vec![checksum_address(&format!("{:#02x}", my_address))], + total_amount: amount_decimal, + my_balance_change: &received_by_me - &spent_by_me, + spent_by_me, + received_by_me, + tx_hex, + tx_hash: tx_hash_str, + block_height: 0, + fee_details: Some(fee_details.into()), + coin: coin.ticker.clone(), + internal_id: vec![].into(), + timestamp: now_sec(), + kmd_rewards: None, + transaction_type: Default::default(), + memo: None, + }) + } +} + +/// Eth withdraw version with user interaction support +pub struct InitEthWithdraw<'a> { + ctx: MmArc, + coin: EthCoin, + task_handle: &'a WithdrawTaskHandle, + req: WithdrawRequest, +} + +#[async_trait] +impl<'a> EthWithdraw for InitEthWithdraw<'a> { + fn coin(&self) -> &EthCoin { &self.coin } + + fn request(&self) -> &WithdrawRequest { &self.req } + + fn on_generating_transaction(&self) -> Result<(), MmError> { + Ok(self + .task_handle + .update_in_progress_status(WithdrawInProgressStatus::GeneratingTransaction)?) + } + + fn on_finishing(&self) -> Result<(), MmError> { + Ok(self + .task_handle + .update_in_progress_status(WithdrawInProgressStatus::Finishing)?) + } + + async fn sign_tx_with_trezor( + &self, + derivation_path: DerivationPath, + unsigned_tx: &UnSignedEthTx, + ) -> Result> { + let coin = self.coin(); + let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| WithdrawError::HwError(HwRpcError::NoTrezorDeviceAvailable))?; + let mut trezor_session = hw_ctx.trezor().await?; + let chain_id = coin + .chain_id + .or_mm_err(|| WithdrawError::ChainIdRequired(String::from("chain_id is required for Trezor wallet")))?; + let unverified_tx = trezor_session + .sign_eth_tx(derivation_path, unsigned_tx, chain_id) + .await?; + Ok(SignedEthTx::new(unverified_tx).map_err(|err| WithdrawError::InternalError(err.to_string()))?) + } +} + +impl<'a> InitEthWithdraw<'a> { + pub fn new( + ctx: MmArc, + coin: EthCoin, + req: WithdrawRequest, + task_handle: &'a WithdrawTaskHandle, + ) -> Result, MmError> { + + Ok(InitEthWithdraw { + ctx, + coin, + task_handle, + req, + }) + } +} + +/// Simple eth withdraw version without user interaction support +pub struct StandardEthWithdraw { + coin: EthCoin, + req: WithdrawRequest, +} + +#[async_trait] +impl EthWithdraw for StandardEthWithdraw { + fn coin(&self) -> &EthCoin { &self.coin } + + fn request(&self) -> &WithdrawRequest { &self.req } + + fn on_generating_transaction(&self) -> Result<(), MmError> { Ok(()) } + + fn on_finishing(&self) -> Result<(), MmError> { Ok(()) } + + async fn sign_tx_with_trezor( + &self, + _derivation_path: DerivationPath, + _unsigned_tx: &UnSignedEthTx, + ) -> Result> { + async { + Err(MmError::new(WithdrawError::UnsupportedError(String::from( + "Trezor not supported for legacy RPC", + )))) + } + .await + } +} + +impl StandardEthWithdraw { + pub fn new(coin: EthCoin, req: WithdrawRequest) -> Result> { + Ok(StandardEthWithdraw { + coin, + req, + }) + } +} diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 6cadc24fbf..68e73480a5 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -3,11 +3,16 @@ use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountAddressId, HDAcco HDWalletStorageError, DEFAULT_GAP_LIMIT}; #[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; use common::executor::AbortedError; -use crypto::CryptoCtxError; +use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError}; use enum_from::EnumFromTrait; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; +//use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, +// RpcTaskStatusRequest, RpcTaskUserActionError}; +//use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::RpcTaskError; +//use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; #[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -39,6 +44,15 @@ pub enum EthActivationV2Error { #[from_trait(WithInternal::internal)] #[display(fmt = "Internal: {}", _0)] InternalError(String), + CoinDoesntSupportTrezor, + HwContextNotInitialized, + #[display(fmt = "Initialization task has timed out {:?}", duration)] + TaskTimedOut { + duration: Duration, + }, + HwError(HwRpcError), + #[display(fmt = "Hardware wallet must be called within rpc task framework")] + InvalidHardwareWalletCall, } impl From for EthActivationV2Error { @@ -61,6 +75,27 @@ impl From for EthActivationV2Error { fn from(e: HDWalletStorageError) -> Self { EthActivationV2Error::HDWalletStorageError(e.to_string()) } } +impl From for EthActivationV2Error { + fn from(e: HwError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: Bip32Error) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: TrezorError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(rpc_err: RpcTaskError) -> Self { + match rpc_err { + RpcTaskError::Timeout(duration) => EthActivationV2Error::TaskTimedOut { duration }, + internal_error => EthActivationV2Error::InternalError(internal_error.to_string()), + } + } +} + #[cfg(target_arch = "wasm32")] impl From for EthActivationV2Error { fn from(e: MetamaskError) -> Self { from_metamask_error(e) } @@ -72,6 +107,7 @@ pub enum EthPrivKeyActivationPolicy { ContextPrivKey, #[cfg(target_arch = "wasm32")] Metamask, + Trezor, } impl Default for EthPrivKeyActivationPolicy { @@ -244,6 +280,8 @@ impl EthCoin { } } +/// Activate eth coin from coin config and and private key build policy, +/// version 2 with no intrinsic tokens creation pub async fn eth_coin_from_conf_and_request_v2( ctx: &MmArc, ticker: &str, @@ -270,10 +308,24 @@ pub async fn eth_coin_from_conf_and_request_v2( let (priv_key_policy, derivation_method) = build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, &req.path_to_address, req.gap_limit) .await?; - let enabled_address = priv_key_policy - .activated_key_or_err() - .map_err(|e| EthActivationV2Error::PrivKeyPolicyNotAllowed(e.into_inner()))? - .address(); + let enabled_address = match priv_key_policy { + // TODO: stub for test, remove as we do not use eth_coin_from_conf_and_request_v2 with trezor + PrivKeyPolicy::Trezor { + path_to_coin: _, + ref activated_pubkey, + } => { + let my_pubkey = activated_pubkey + .as_ref() + .or_mm_err(|| EthActivationV2Error::InternalError("no pubkey from trezor".to_string()))?; + let my_pubkey = pubkey_from_xpub_str(my_pubkey) + .ok_or_else(|| EthActivationV2Error::InternalError("invalid xpub from trezor".to_string()))?; + public_to_address(&my_pubkey) + }, + _ => priv_key_policy + .activated_key_or_err() + .map_err(|e| EthActivationV2Error::PrivKeyPolicyNotAllowed(e.into_inner()))? + .address(), + }; let enabled_address_str = display_eth_address(&enabled_address); let chain_id = conf["chain_id"].as_u64(); @@ -287,10 +339,16 @@ pub async fn eth_coin_from_conf_and_request_v2( .. }, ) => build_http_transport(ctx, ticker.to_string(), enabled_address_str, key_pair, &req.nodes).await?, - (EthRpcMode::Http, EthPrivKeyPolicy::Trezor) => { - return MmError::err(EthActivationV2Error::PrivKeyPolicyNotAllowed( + (EthRpcMode::Http, EthPrivKeyPolicy::Trezor { .. }) => { + /*return MmError::err(EthActivationV2Error::PrivKeyPolicyNotAllowed( PrivKeyPolicyNotAllowed::HardwareWalletNotSupported, - )); + ));*/ + // for now in-memory privkey which must be always initialised if trezor policy is set + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let secp256k1_key_pair = crypto_ctx.mm2_internal_key_pair(); + let eth_key_pair = eth::KeyPair::from_secret_slice(&secp256k1_key_pair.private_bytes()) + .map_to_mm(|_| EthActivationV2Error::InternalError("could not get internal keypair".to_string()))?; + build_http_transport(ctx, ticker.to_string(), enabled_address_str, ð_key_pair, &req.nodes).await? }, #[cfg(target_arch = "wasm32")] (EthRpcMode::Metamask, EthPrivKeyPolicy::Metamask(_)) => { @@ -418,6 +476,50 @@ pub(crate) async fn build_address_and_priv_key_policy( derivation_method, )) }, + EthPrivKeyBuildPolicy::Trezor => { + let path_to_coin = json::from_value(conf["derivation_path"].clone()) + .map_to_mm(|e| EthActivationV2Error::ErrorDeserializingDerivationPath(e.to_string()))?; + + let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).ok(); + if trezor_coin.is_none() { + return MmError::err(EthActivationV2Error::CoinDoesntSupportTrezor); + } + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| EthActivationV2Error::HwContextNotInitialized)?; + let hd_wallet_rmd160 = hw_ctx.rmd160(); + let hd_wallet_storage = HDWalletCoinStorage::init_with_rmd160(ctx, ticker.to_string(), hd_wallet_rmd160) + .await + .mm_err(EthActivationV2Error::from)?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin).await?; + // Todo: use fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } like UTXO + let gap_limit = DEFAULT_GAP_LIMIT; + // Todo: Maybe we can make a constructor for HDWallet struct + let hd_wallet = EthHDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin.clone(), + accounts: HDAccountsMutex::new(accounts), + enabled_address: Some(*path_to_address), + gap_limit, + }; + let derivation_method = DerivationMethod::HDWallet(hd_wallet); + let derivation_path = path_to_address.to_derivation_path(&path_to_coin)?; + let mut trezor_session = hw_ctx.trezor().await?; + let my_pubkey = trezor_session + .get_eth_public_key(derivation_path, false) + .await? + .ack_all() + .await?; + Ok(( + EthPrivKeyPolicy::Trezor { + path_to_coin: Some(path_to_coin), + activated_pubkey: Some(my_pubkey), + }, + derivation_method, + )) + }, #[cfg(target_arch = "wasm32")] EthPrivKeyBuildPolicy::Metamask(metamask_ctx) => { let address = *metamask_ctx.check_active_eth_account().await?; diff --git a/mm2src/coins/hd_wallet/pubkey.rs b/mm2src/coins/hd_wallet/pubkey.rs index c8a8fdf083..f5189896eb 100644 --- a/mm2src/coins/hd_wallet/pubkey.rs +++ b/mm2src/coins/hd_wallet/pubkey.rs @@ -37,6 +37,7 @@ pub enum RpcTaskXPubExtractor<'task, Task: RpcTask> { hw_ctx: HardwareWalletArc, task_handle: &'task RpcTaskHandle, statuses: HwConnectStatuses, + is_eth: bool, // TODO: maybe CoinProtocol is better? }, } @@ -56,7 +57,11 @@ where hw_ctx, task_handle, statuses, - } => Self::extract_xpub_from_trezor(hw_ctx, task_handle, statuses, trezor_coin, derivation_path).await, + is_eth, + } => { + Self::extract_xpub_from_trezor(hw_ctx, task_handle, statuses, trezor_coin, derivation_path, *is_eth) + .await + }, } } } @@ -70,6 +75,7 @@ where ctx: &MmArc, task_handle: &'task RpcTaskHandle, statuses: HwConnectStatuses, + is_eth: bool, ) -> MmResult, HDExtractPubkeyError> { let crypto_ctx = CryptoCtx::from_ctx(ctx)?; let hw_ctx = crypto_ctx @@ -79,6 +85,7 @@ where hw_ctx, task_handle, statuses, + is_eth, }) } @@ -88,26 +95,35 @@ where statuses: &HwConnectStatuses, trezor_coin: String, derivation_path: DerivationPath, + is_eth: bool, ) -> MmResult { let mut trezor_session = hw_ctx.trezor().await?; let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, statuses.to_trezor_request_statuses()); - let xpub = trezor_session - .get_public_key( - derivation_path, - trezor_coin, - EcdsaCurve::Secp256k1, - SHOW_PUBKEY_ON_DISPLAY, - IGNORE_XPUB_MAGIC, - ) - .await? - .process(&pubkey_processor) - .await?; - - // Despite we pass `IGNORE_XPUB_MAGIC` to the [`TrezorSession::get_public_key`] method, - // Trezor sometimes returns pubkeys with magic prefixes like `dgub` prefix for DOGE coin. - // So we need to replace the magic prefix manually. - XPubConverter::replace_magic_prefix(xpub).mm_err(HDExtractPubkeyError::from) + if !is_eth { + let xpub = trezor_session + .get_public_key( + derivation_path, + trezor_coin, + EcdsaCurve::Secp256k1, + SHOW_PUBKEY_ON_DISPLAY, + IGNORE_XPUB_MAGIC, + ) + .await? + .process(&pubkey_processor) + .await?; + // Despite we pass `IGNORE_XPUB_MAGIC` to the [`TrezorSession::get_public_key`] method, + // Trezor sometimes returns pubkeys with magic prefixes like `dgub` prefix for DOGE coin. + // So we need to replace the magic prefix manually. + XPubConverter::replace_magic_prefix(xpub).mm_err(HDExtractPubkeyError::from) + } else { + let xpub = trezor_session + .get_eth_public_key(derivation_path, SHOW_PUBKEY_ON_DISPLAY) + .await? + .process(&pubkey_processor) + .await?; + Ok(xpub) + } } } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index f33daa366f..161bb844c8 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2217,6 +2217,8 @@ pub enum WithdrawError { }, #[display(fmt = "DB error {}", _0)] DbError(String), + #[display(fmt = "chain id not set: {}", _0)] + ChainIdRequired(String), } impl HttpStatusCode for WithdrawError { @@ -2242,7 +2244,8 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::AddressMismatchError { .. } | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) | WithdrawError::CoinDoesntSupportNftWithdraw { .. } - | WithdrawError::NotEnoughNftsAmount { .. } => StatusCode::BAD_REQUEST, + | WithdrawError::NotEnoughNftsAmount { .. } + | WithdrawError::ChainIdRequired(_) => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, @@ -3058,7 +3061,14 @@ pub enum PrivKeyPolicy { /// /// Details about how the keys are managed with the Trezor device /// are abstracted away and are not directly managed by this policy. - Trezor, + Trezor { + /// path to coin for Trezor, user only for eth. TODO: could we get this from trezor itself? + path_to_coin: Option, + /// pubkey for initially derived account, used for Eth only + /// TODO: maybe better get pubkey each time when it is needed instead of storing here. + /// Also now it is stored as base58 encodes. Maybe better to store as a binary type Secp256k1ExtendedPublicKey + activated_pubkey: Option, + }, /// The Metamask private key policy, specific to the WASM target architecture. /// /// This variant encapsulates details about how keys are managed when interfacing @@ -3086,7 +3096,7 @@ impl PrivKeyPolicy { activated_key: activated_key_pair, .. } => Some(activated_key_pair), - PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::Trezor { .. } => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -3106,7 +3116,7 @@ impl PrivKeyPolicy { PrivKeyPolicy::HDWallet { bip39_secp_priv_key, .. } => Some(bip39_secp_priv_key), - PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor { .. } => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -3128,7 +3138,8 @@ impl PrivKeyPolicy { path_to_coin: derivation_path, .. } => Some(derivation_path), - PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::Trezor { path_to_coin, .. } => path_to_coin.as_ref(), + PrivKeyPolicy::Iguana(_) => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -3151,7 +3162,12 @@ impl PrivKeyPolicy { .mm_err(|e| PrivKeyPolicyNotAllowed::InternalError(e.to_string())) } - fn is_trezor(&self) -> bool { matches!(self, PrivKeyPolicy::Trezor) } + fn is_trezor(&self) -> bool { + matches!(self, PrivKeyPolicy::Trezor { + path_to_coin: _, + activated_pubkey: _ + }) + } } /// 'CoinWithPrivKeyPolicy' trait is used to get the private key policy of a coin. @@ -4604,3 +4620,61 @@ mod tests { assert!(matches!(Some(coin), _found)); } } + +#[cfg(not(target_arch = "wasm32"))] +pub mod for_tests { + use crate::rpc_command::init_withdraw::WithdrawStatusRequest; + use crate::rpc_command::init_withdraw::{init_withdraw, withdraw_status}; + use crate::{TransactionDetails, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawRequest}; + use common::executor::Timer; + use common::{now_ms, wait_until_ms}; + use mm2_core::mm_ctx::MmArc; + use mm2_err_handle::prelude::MmResult; + use mm2_number::BigDecimal; + use rpc_task::RpcTaskStatus; + use std::str::FromStr; + + /// Helper to call init_withdraw and wait for completion + pub async fn test_withdraw_init_loop( + ctx: MmArc, + //fields: UtxoCoinFields, + ticker: &str, + to: &str, + amount: &str, + from_derivation_path: &str, + fee: Option, + ) -> MmResult { + let withdraw_req = WithdrawRequest { + amount: BigDecimal::from_str(amount).unwrap(), + from: Some(WithdrawFrom::DerivationPath { + derivation_path: from_derivation_path.to_owned(), + }), + to: to.to_owned(), + coin: ticker.to_owned(), + max: false, + fee, + memo: None, + }; + let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); + let timeout = wait_until_ms(150000); + loop { + if now_ms() > timeout { + panic!("{} init_withdraw timed out", ticker); + } + let status = withdraw_status(ctx.clone(), WithdrawStatusRequest { + task_id: init.task_id, + forget_if_finished: true, + }) + .await; + if let Ok(status) = status { + match status { + RpcTaskStatus::Ok(tx_details) => break Ok(tx_details), + RpcTaskStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } else { + panic!("{} could not get withdraw_status", ticker) + } + } + } +} diff --git a/mm2src/coins/rpc_command/account_balance.rs b/mm2src/coins/rpc_command/account_balance.rs index 28edddc899..7c636f0bc3 100644 --- a/mm2src/coins/rpc_command/account_balance.rs +++ b/mm2src/coins/rpc_command/account_balance.rs @@ -87,8 +87,7 @@ pub mod common_impl { PagingOptionsEnum::PageNumber(page_number) => ((page_number.get() - 1) * params.limit) as u32, }; let to_address_id = std::cmp::min(from_address_id + params.limit as u32, total_addresses_number); - - let addresses = coin + let addresses: Vec = coin .known_addresses_balances_with_ids(&hd_account, params.chain, from_address_id..to_address_id) .await?; let page_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 0870f6b659..06ddff3926 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -249,6 +249,7 @@ impl RpcTask for InitCreateAccountTask { state: CreateAccountState, task_handle: &CreateAccountTaskHandle, is_trezor: bool, + is_eth: bool, ) -> MmResult where Coin: InitCreateAccountRpcOps + Send + Sync, @@ -263,7 +264,7 @@ impl RpcTask for InitCreateAccountTask { on_passphrase_request: CreateAccountAwaitingStatus::EnterTrezorPassphrase, on_ready: CreateAccountInProgressStatus::RequestingAccountBalance, }; - Some(CreateAccountXPubExtractor::new(ctx, task_handle, hw_statuses)?) + Some(CreateAccountXPubExtractor::new(ctx, task_handle, hw_statuses, is_eth)?) } else { None }; @@ -279,6 +280,7 @@ impl RpcTask for InitCreateAccountTask { self.task_state.clone(), task_handle, utxo.is_trezor(), + false, ) .await }, @@ -290,6 +292,7 @@ impl RpcTask for InitCreateAccountTask { self.task_state.clone(), task_handle, qtum.is_trezor(), + false, ) .await }, @@ -301,6 +304,7 @@ impl RpcTask for InitCreateAccountTask { self.task_state.clone(), task_handle, false, + true, ) .await }, diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs index c9ba606250..aa885a8ad0 100644 --- a/mm2src/coins/rpc_command/init_withdraw.rs +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -134,6 +134,7 @@ impl RpcTask for WithdrawTask { MmCoinEnum::QtumCoin(ref qtum) => qtum.init_withdraw(ctx, request, task_handle).await, #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::ZCoin(ref z) => z.init_withdraw(ctx, request, task_handle).await, + MmCoinEnum::EthCoin(ref eth) => eth.init_withdraw(ctx, request, task_handle).await, _ => MmError::err(WithdrawError::CoinDoesntSupportInitWithdraw { coin: self.coin.ticker().to_owned(), }), diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 4b48ced3c2..4b1bbfb4e6 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -1787,70 +1787,6 @@ fn parse_hex_encoded_u32(hex_encoded: &str) -> Result> { Ok(u32::from_be_bytes(be_bytes)) } -#[cfg(not(target_arch = "wasm32"))] -pub mod for_tests { - use super::UtxoCoinFields; - use crate::rpc_command::init_withdraw::WithdrawStatusRequest; - use crate::rpc_command::init_withdraw::{init_withdraw, withdraw_status}; - use crate::WithdrawFrom; - use crate::{utxo::{utxo_standard::UtxoStandardCoin, UtxoArc}, - WithdrawRequest}; - use crate::{MarketCoinOps, TransactionDetails, WithdrawError}; - use common::executor::Timer; - use common::{now_ms, wait_until_ms}; - use mm2_core::mm_ctx::MmArc; - use mm2_err_handle::prelude::MmResult; - use mm2_number::BigDecimal; - use rpc_task::RpcTaskStatus; - use std::str::FromStr; - - /// Helper to call init_withdraw and wait for completion - pub async fn test_withdraw_init_loop( - ctx: MmArc, - fields: UtxoCoinFields, - ticker: &str, - to: &str, - amount: &str, - from_derivation_path: &str, - ) -> MmResult { - let arc: UtxoArc = fields.into(); - let coin: UtxoStandardCoin = arc.into(); - - let withdraw_req = WithdrawRequest { - amount: BigDecimal::from_str(amount).unwrap(), - from: Some(WithdrawFrom::DerivationPath { - derivation_path: from_derivation_path.to_owned(), - }), - to: to.to_owned(), - coin: ticker.to_owned(), - max: false, - fee: None, - memo: None, - }; - let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); - let timeout = wait_until_ms(150000); - loop { - if now_ms() > timeout { - panic!("{} init_withdraw timed out", coin.ticker()); - } - let status = withdraw_status(ctx.clone(), WithdrawStatusRequest { - task_id: init.task_id, - forget_if_finished: true, - }) - .await; - if let Ok(status) = status { - match status { - RpcTaskStatus::Ok(tx_details) => break Ok(tx_details), - RpcTaskStatus::Error(e) => break Err(e), - _ => Timer::sleep(1.).await, - } - } else { - panic!("{} could not get withdraw_status", coin.ticker()) - } - } - } -} - #[test] fn test_parse_hex_encoded_u32() { assert_eq!(parse_hex_encoded_u32("0x892f2085"), Ok(2301567109)); diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 94dbef1eea..70d4f7e6bb 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -351,7 +351,10 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { decimals, dust_amount, rpc_client, - priv_key_policy: PrivKeyPolicy::Trezor, + priv_key_policy: PrivKeyPolicy::Trezor { + path_to_coin: None, + activated_pubkey: None, + }, derivation_method: DerivationMethod::HDWallet(hd_wallet), history_sync_state: Mutex::new(initial_history_state), tx_cache, diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index ef7e01f2a7..0a925ef954 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -381,7 +381,7 @@ pub fn my_public_key(coin: &UtxoCoinFields) -> Result<&Public, MmError Ok(activated_key_pair.public()), // Hardware Wallets requires BIP32/BIP44 derivation path to extract a public key. - PrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::Trezor), + PrivKeyPolicy::Trezor { .. } => MmError::err(UnexpectedDerivationMethod::Trezor), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => MmError::err(UnexpectedDerivationMethod::UnsupportedError( "`PrivKeyPolicy::Metamask` is not supported in this context".to_string(), @@ -2370,7 +2370,7 @@ pub fn display_priv_key(coin: &UtxoCoinFields) -> Result { activated_key: ref activated_key_pair, .. } => Ok(activated_key_pair.private().to_string()), - PrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Hardware Wallets"), + PrivKeyPolicy::Trezor { .. } => ERR!("'display_priv_key' doesn't support Hardware Wallets"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support Metamask"), } @@ -4173,7 +4173,7 @@ pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> activated_key: activated_key_pair, .. } => activated_key_pair, - PrivKeyPolicy::Trezor => todo!(), + PrivKeyPolicy::Trezor { .. } => todo!(), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => panic!("`PrivKeyPolicy::Metamask` is not supported for UTXO coins"), } diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 1e5a9d6c7b..8546c72d26 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -317,7 +317,7 @@ where activated_key: ref activated_key_pair, .. } => SignPolicy::WithKeyPair(activated_key_pair), - PrivKeyPolicy::Trezor => { + PrivKeyPolicy::Trezor { .. } => { let trezor_session = hw_ctx.trezor().await?; SignPolicy::WithTrezor(trezor_session) }, diff --git a/mm2src/coins/utxo_signer/src/sign_common.rs b/mm2src/coins/utxo_signer/src/sign_common.rs index 85732dd900..d681aed5f0 100644 --- a/mm2src/coins/utxo_signer/src/sign_common.rs +++ b/mm2src/coins/utxo_signer/src/sign_common.rs @@ -29,6 +29,7 @@ pub(crate) fn complete_tx(unsigned: TransactionInputSigner, signed_inputs: Vec>, @@ -208,6 +211,10 @@ impl PlatformWithTokensActivationOps for BchCoin { type ActivationResult = BchWithTokensActivationResult; type ActivationError = BchWithTokensActivationError; + type InProgressStatus = InitPlatformCoinWithTokensStandardInProgressStatus; + type AwaitingStatus = InitPlatformCoinWithTokensStandardAwaitingStatus; + type UserAction = InitPlatformCoinWithTokensStandardUserAction; + async fn enable_platform_coin( ctx: MmArc, ticker: String, @@ -258,6 +265,7 @@ impl PlatformWithTokensActivationOps for BchCoin { async fn get_activation_result( &self, + _task_handle: Option<&RpcTaskHandle>>, activation_request: &Self::ActivationRequest, ) -> Result> { let current_block = self.as_ref().rpc_client.get_block_count().compat().await?; @@ -331,4 +339,35 @@ impl PlatformWithTokensActivationOps for BchCoin { let settings = AbortSettings::info_on_abort(format!("bch_and_slp_history_loop stopped for {}", self.ticker())); self.spawner().spawn_with_settings(fut, settings); } + + fn rpc_task_manager(_activation_ctx: &CoinsActivationContext) -> &InitPlatformTaskManagerShared { + unimplemented!() + } +} + +/* +pub struct InitBchTask {} + +pub type BchTaskManagerShared = RpcTaskManagerShared; +pub type BchActivationV2AwaitingStatus = HwRpcTaskAwaitingStatus; +pub type BchActivationV2UserAction = HwRpcTaskUserAction; + +impl RpcTaskTypes for InitBchTask { + type Item = (); + type Error = (); + type InProgressStatus = (); + type AwaitingStatus = BchActivationV2AwaitingStatus; + type UserAction = BchActivationV2UserAction; +} + +#[async_trait] +impl RpcTask for InitBchTask { + fn initial_status(&self) -> Self::InProgressStatus { todo!() } + + async fn cancel(self) { todo!() } + + async fn run(&mut self, _task_handle: &RpcTaskHandle) -> Result> { + Ok(()) + } } +*/ diff --git a/mm2src/coins_activation/src/context.rs b/mm2src/coins_activation/src/context.rs index a86869e7ab..fd3543eae4 100644 --- a/mm2src/coins_activation/src/context.rs +++ b/mm2src/coins_activation/src/context.rs @@ -1,3 +1,4 @@ +use crate::eth_with_token_activation::EthTaskManagerShared; #[cfg(not(target_arch = "wasm32"))] use crate::lightning_activation::LightningTaskManagerShared; use crate::utxo_activation::{QtumTaskManagerShared, UtxoStandardTaskManagerShared}; @@ -14,6 +15,7 @@ pub struct CoinsActivationContext { pub(crate) init_z_coin_task_manager: ZcoinTaskManagerShared, #[cfg(not(target_arch = "wasm32"))] pub(crate) init_lightning_task_manager: LightningTaskManagerShared, + pub(crate) init_eth_task_manager: EthTaskManagerShared, } impl CoinsActivationContext { @@ -27,6 +29,7 @@ impl CoinsActivationContext { init_z_coin_task_manager: RpcTaskManager::new_shared(), #[cfg(not(target_arch = "wasm32"))] init_lightning_task_manager: RpcTaskManager::new_shared(), + init_eth_task_manager: RpcTaskManager::new_shared(), }) }) } diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 6e8a0b1baa..75044ef752 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -1,4 +1,8 @@ +use crate::context::CoinsActivationContext; use crate::platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPlatformBalance, + InitPlatformCoinWithTokensStandardAwaitingStatus, + InitPlatformCoinWithTokensStandardInProgressStatus, + InitPlatformCoinWithTokensStandardUserAction, InitPlatformTaskManagerShared, InitTokensAsMmCoinsError, PlatformWithTokensActivationOps, RegisterTokenInfo, TokenActivationParams, TokenActivationRequest, TokenAsMmCoinInitializer, TokenInitializer, TokenOf}; @@ -12,15 +16,18 @@ use coins::eth::{display_eth_address, Erc20TokenInfo, EthCoin, EthCoinType, EthP use coins::hd_wallet::RpcTaskXPubExtractor; use coins::my_tx_history_v2::TxHistoryStorage; use coins::{CoinBalance, CoinProtocol, CoinWithDerivationMethod, MarketCoinOps, MmCoin, MmCoinEnum}; + +use crate::platform_coin_with_tokens::InitPlatformCoinWithTokensTask; use common::Future01CompatExt; use common::{drop_mutability, true_f}; -use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; +use crypto::hw_rpc_task::HwConnectStatuses; +use crypto::HwRpcError; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; #[cfg(target_arch = "wasm32")] use mm2_metamask::MetamaskRpcError; use mm2_number::BigDecimal; -use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskTypes}; +use rpc_task::RpcTaskHandle; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -59,6 +66,19 @@ impl From for EnablePlatformCoinWithTokensError { EnablePlatformCoinWithTokensError::Transport(metamask.to_string()) }, EthActivationV2Error::InternalError(e) => EnablePlatformCoinWithTokensError::Internal(e), + EthActivationV2Error::HwContextNotInitialized => { + EnablePlatformCoinWithTokensError::Internal("Hardware wallet is not initalised".to_string()) + }, + EthActivationV2Error::CoinDoesntSupportTrezor => { + EnablePlatformCoinWithTokensError::Internal("Coin does not support Trezor wallet".to_string()) + }, + EthActivationV2Error::TaskTimedOut { .. } => { + EnablePlatformCoinWithTokensError::Internal("Coin activation timed out".to_string()) + }, + EthActivationV2Error::HwError(e) => EnablePlatformCoinWithTokensError::Internal(e.to_string()), + EthActivationV2Error::InvalidHardwareWalletCall => EnablePlatformCoinWithTokensError::Internal( + "Hardware wallet must be used within rpc task manager".to_string(), + ), } } } @@ -149,7 +169,7 @@ impl RegisterTokenInfo for EthCoin { } } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct EthWithTokensActivationResult { current_block: u64, eth_addresses_infos: HashMap>, @@ -177,6 +197,10 @@ impl PlatformWithTokensActivationOps for EthCoin { type ActivationResult = EthWithTokensActivationResult; type ActivationError = EthActivationV2Error; + type InProgressStatus = InitPlatformCoinWithTokensStandardInProgressStatus; + type AwaitingStatus = InitPlatformCoinWithTokensStandardAwaitingStatus; + type UserAction = InitPlatformCoinWithTokensStandardUserAction; + async fn enable_platform_coin( ctx: MmArc, ticker: String, @@ -218,6 +242,7 @@ impl PlatformWithTokensActivationOps for EthCoin { async fn get_activation_result( &self, + task_handle: Option<&RpcTaskHandle>>, activation_request: &Self::ActivationRequest, ) -> Result> { let current_block = self @@ -228,7 +253,20 @@ impl PlatformWithTokensActivationOps for EthCoin { // Todo: support for Trezor should be added in a similar place in init_platform_coin_with_token method when implemented // Todo: check utxo implementation for reference - let xpub_extractor: Option> = None; + // let xpub_extractor: Option> = None; + let xpub_extractor = if self.is_trezor() { + let ctx = MmArc::from_weak(&self.ctx).ok_or_else(|| EthActivationV2Error::InvalidHardwareWalletCall)?; + let task_handle = task_handle.ok_or_else(|| { + EthActivationV2Error::InternalError("Hardware wallet must be accessed under task manager".to_string()) + })?; + Some( + RpcTaskXPubExtractor::new(&ctx, task_handle, eth_xpub_extractor_rpc_statuses(), true) + .map_err(|_| MmError::new(EthActivationV2Error::HwError(HwRpcError::NotInitialized)))?, + ) + } else { + None + }; + let mut enable_params = activation_request.platform_request.enable_params.clone(); enable_params.scan_policy = EnableCoinScanPolicy::DoNotScan; drop_mutability!(enable_params); @@ -303,27 +341,9 @@ impl PlatformWithTokensActivationOps for EthCoin { _initial_balance: Option, ) { } -} -// Todo: this is just an empty implementation that is used in get_activation_result, should be removed when proper init_platform_coin_with_token method is implemented -pub struct InitEthTask {} - -impl RpcTaskTypes for InitEthTask { - type Item = (); - type Error = EthActivationV2Error; - type InProgressStatus = (); - type AwaitingStatus = HwRpcTaskAwaitingStatus; - type UserAction = HwRpcTaskUserAction; -} - -#[async_trait] -impl RpcTask for InitEthTask { - fn initial_status(&self) -> Self::InProgressStatus { todo!() } - - async fn cancel(self) { todo!() } - - async fn run(&mut self, _task_handle: &RpcTaskHandle) -> Result> { - todo!() + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitPlatformTaskManagerShared { + &activation_ctx.init_eth_task_manager } } @@ -340,5 +360,23 @@ fn eth_priv_key_build_policy( .or_mm_err(|| EthActivationV2Error::MetamaskError(MetamaskRpcError::MetamaskCtxNotInitialized))?; Ok(EthPrivKeyBuildPolicy::Metamask(metamask_ctx)) }, + EthPrivKeyActivationPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), + } +} + +pub type EthTaskManagerShared = InitPlatformTaskManagerShared; + +pub(crate) fn eth_xpub_extractor_rpc_statuses() -> HwConnectStatuses< + InitPlatformCoinWithTokensStandardInProgressStatus, + InitPlatformCoinWithTokensStandardAwaitingStatus, +> { + HwConnectStatuses { + on_connect: InitPlatformCoinWithTokensStandardInProgressStatus::WaitingForTrezorToConnect, + on_connected: InitPlatformCoinWithTokensStandardInProgressStatus::ActivatingCoin, + on_connection_failed: InitPlatformCoinWithTokensStandardInProgressStatus::Finishing, + on_button_request: InitPlatformCoinWithTokensStandardInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: InitPlatformCoinWithTokensStandardAwaitingStatus::EnterTrezorPin, + on_passphrase_request: InitPlatformCoinWithTokensStandardAwaitingStatus::EnterTrezorPassphrase, + on_ready: InitPlatformCoinWithTokensStandardInProgressStatus::ActivatingCoin, } } diff --git a/mm2src/coins_activation/src/lib.rs b/mm2src/coins_activation/src/lib.rs index 89cc5aca85..e6a59ca41b 100644 --- a/mm2src/coins_activation/src/lib.rs +++ b/mm2src/coins_activation/src/lib.rs @@ -32,6 +32,7 @@ pub use utxo_activation::for_tests; pub use l2::{cancel_init_l2, init_l2, init_l2_status, init_l2_user_action}; pub use platform_coin_with_tokens::enable_platform_coin_with_tokens; +pub use platform_coin_with_tokens::for_tests as platform_for_tests; pub use standalone_coin::{cancel_init_standalone_coin, init_standalone_coin, init_standalone_coin_status, init_standalone_coin_user_action, InitStandaloneCoinReq, InitStandaloneCoinStatusRequest}; pub use token::enable_token; diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index c816b34cff..0b5f01b12f 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -1,15 +1,22 @@ +use std::time::Duration; + +use crate::context::CoinsActivationContext; use crate::prelude::*; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tx_history_storage::{CreateTxHistoryStorageError, TxHistoryStorageBuilder}; -use coins::{lp_coinfind_any, CoinProtocol, CoinsContext, MmCoin, MmCoinEnum, PrivKeyPolicyNotAllowed, +use coins::{lp_coinfind, lp_coinfind_any, CoinProtocol, CoinsContext, MmCoin, MmCoinEnum, PrivKeyPolicyNotAllowed, UnexpectedDerivationMethod}; use common::{log, HttpStatusCode, StatusCode}; +use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoCtxError; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; +use rpc_task::rpc_common::{InitRpcTaskResponse, RpcTaskStatusRequest}; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -133,11 +140,15 @@ pub trait GetPlatformBalance { } #[async_trait] -pub trait PlatformWithTokensActivationOps: Into { +pub trait PlatformWithTokensActivationOps: Into + Send + Sync + 'static { type ActivationRequest: Clone + Send + Sync + TxHistory; - type PlatformProtocolInfo: TryFromCoinProtocol; - type ActivationResult: GetPlatformBalance + CurrentBlock; - type ActivationError: NotMmError + std::fmt::Debug; + type PlatformProtocolInfo: TryFromCoinProtocol + Send; + type ActivationResult: GetPlatformBalance + CurrentBlock + serde::Serialize + Send + Clone + Sync + 'static; + type ActivationError: NotMmError + std::fmt::Debug + NotEqual + Into; + + type InProgressStatus: InitPlatformWithTokensInitialStatus + serde::Serialize + Clone + Send + Sync + 'static; + type AwaitingStatus: serde::Serialize + Clone + Send + Sync + 'static; + type UserAction: serde::de::DeserializeOwned + NotMmError + Send + Sync + 'static; /// Initializes the platform coin itself async fn enable_platform_coin( @@ -158,8 +169,12 @@ pub trait PlatformWithTokensActivationOps: Into { async fn get_activation_result( &self, + task_handle: Option<&RpcTaskHandle>>, activation_request: &Self::ActivationRequest, - ) -> Result>; + ) -> Result> + where + Self: MmCoin + Clone, + EnablePlatformCoinWithTokensError: From; fn start_history_background_fetching( &self, @@ -167,16 +182,21 @@ pub trait PlatformWithTokensActivationOps: Into { storage: impl TxHistoryStorage, initial_balance: Option, ); + + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitPlatformTaskManagerShared + where + Self: MmCoin + Clone, + EnablePlatformCoinWithTokensError: From<::ActivationError>; } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct EnablePlatformCoinWithTokensReq { ticker: String, #[serde(flatten)] request: T, } -#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType, Clone)] #[serde(tag = "error_type", content = "error_data")] pub enum EnablePlatformCoinWithTokensError { PlatformIsAlreadyActivated(String), @@ -217,6 +237,12 @@ pub enum EnablePlatformCoinWithTokensError { AtLeastOneNodeRequired(String), InvalidPayload(String), Internal(String), + #[display(fmt = "No such task '{}'", _0)] + NoSuchTask(TaskId), + #[display(fmt = "Initialization task has timed out {:?}", duration)] + TaskTimedOut { + duration: Duration, + }, } impl From for EnablePlatformCoinWithTokensError { @@ -271,6 +297,16 @@ impl From for EnablePlatformCoinWithTokensError { fn from(e: CryptoCtxError) -> Self { EnablePlatformCoinWithTokensError::Internal(e.to_string()) } } +impl From for EnablePlatformCoinWithTokensError { + fn from(e: RpcTaskError) -> Self { + match e { + RpcTaskError::NoSuchTask(task_id) => EnablePlatformCoinWithTokensError::NoSuchTask(task_id), + RpcTaskError::Timeout(duration) => EnablePlatformCoinWithTokensError::TaskTimedOut { duration }, + rpc_internal => EnablePlatformCoinWithTokensError::Internal(rpc_internal.to_string()), + } + } +} + impl HttpStatusCode for EnablePlatformCoinWithTokensError { fn status_code(&self) -> StatusCode { match self { @@ -280,14 +316,16 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::PrivKeyPolicyNotAllowed(_) | EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(_) | EnablePlatformCoinWithTokensError::Transport(_) - | EnablePlatformCoinWithTokensError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + | EnablePlatformCoinWithTokensError::Internal(_) + | EnablePlatformCoinWithTokensError::TaskTimedOut { .. } => StatusCode::INTERNAL_SERVER_ERROR, EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated(_) | EnablePlatformCoinWithTokensError::PlatformConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::TokenConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::UnexpectedPlatformProtocol { .. } | EnablePlatformCoinWithTokensError::InvalidPayload { .. } | EnablePlatformCoinWithTokensError::AtLeastOneNodeRequired(_) - | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } => StatusCode::BAD_REQUEST, + | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } + | EnablePlatformCoinWithTokensError::NoSuchTask(_) => StatusCode::BAD_REQUEST, } } } @@ -308,7 +346,7 @@ where mm_tokens.extend(tokens); } - let activation_result = platform_coin.get_activation_result(&req.request).await?; + let activation_result = platform_coin.get_activation_result(None, &req.request).await?; log::info!("{} current block {}", req.ticker, activation_result.current_block()); let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); @@ -324,6 +362,19 @@ pub async fn enable_platform_coin_with_tokens( ctx: MmArc, req: EnablePlatformCoinWithTokensReq, ) -> Result> +where + Platform: PlatformWithTokensActivationOps + MmCoin + Clone, + EnablePlatformCoinWithTokensError: From, + (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, +{ + enable_platform_coin_with_tokens_within_rpc::(ctx, None, req).await +} + +pub async fn enable_platform_coin_with_tokens_within_rpc( + ctx: MmArc, + task_handle: Option<&RpcTaskHandle>>, + req: EnablePlatformCoinWithTokensReq, +) -> Result> where Platform: PlatformWithTokensActivationOps + MmCoin + Clone, EnablePlatformCoinWithTokensError: From, @@ -358,7 +409,7 @@ where mm_tokens.extend(tokens); } - let activation_result = platform_coin.get_activation_result(&req.request).await?; + let activation_result = platform_coin.get_activation_result(task_handle, &req.request).await?; log::info!("{} current block {}", req.ticker, activation_result.current_block()); if req.request.tx_history() { @@ -377,3 +428,186 @@ where Ok(activation_result) } + +pub struct InitPlatformCoinWithTokensTask { + ctx: MmArc, + request: EnablePlatformCoinWithTokensReq, + /*coin_conf: Json, + protocol_info: Platform::PlatformProtocolInfo,*/ +} + +impl RpcTaskTypes for InitPlatformCoinWithTokensTask { + type Item = Platform::ActivationResult; + type Error = EnablePlatformCoinWithTokensError; + type InProgressStatus = Platform::InProgressStatus; + type AwaitingStatus = Platform::AwaitingStatus; + type UserAction = Platform::UserAction; +} + +#[async_trait] +impl RpcTask for InitPlatformCoinWithTokensTask +where + Platform: PlatformWithTokensActivationOps + MmCoin + Clone + Send + 'static, + //Platform::ActivationError: Into, + EnablePlatformCoinWithTokensError: From<::ActivationError>, +{ + fn initial_status(&self) -> Self::InProgressStatus { + ::initial_status() + } + + /// Try to disable the coin in case if we managed to register it already. + async fn cancel(self) {} + + async fn run(&mut self, task_handle: &RpcTaskHandle) -> Result> { + enable_platform_coin_with_tokens_within_rpc::( + self.ctx.clone(), + Some(task_handle), + self.request.clone(), + ) + .await + } +} + +pub trait InitPlatformWithTokensInitialStatus { + fn initial_status() -> Self; +} + +//use serde_derive::Serialize; + +pub type InitPlatformCoinWithTokensStandardAwaitingStatus = HwRpcTaskAwaitingStatus; +pub type InitPlatformCoinWithTokensStandardUserAction = HwRpcTaskUserAction; +pub type EnablePlatformCoinWithTokensResponse = InitRpcTaskResponse; +pub type EnablePlatformCoinWithTokensStatusRequest = RpcTaskStatusRequest; + +pub type InitPlatformTaskManagerShared = RpcTaskManagerShared>; + +#[derive(Clone, Serialize)] +pub enum InitPlatformCoinWithTokensStandardInProgressStatus { + ActivatingCoin, + SyncingBlockHeaders { + current_scanned_block: u64, + last_block: u64, + }, + TemporaryError(String), + RequestingWalletBalance, + Finishing, + /// This status doesn't require the user to send `UserAction`, + /// but it tells the user that he should confirm/decline an address on his device. + WaitingForTrezorToConnect, + FollowHwDeviceInstructions, +} + +impl InitPlatformWithTokensInitialStatus for InitPlatformCoinWithTokensStandardInProgressStatus { + fn initial_status() -> Self { InitPlatformCoinWithTokensStandardInProgressStatus::ActivatingCoin } +} + +pub async fn init_platform_coin_with_tokens( + ctx: MmArc, + request: EnablePlatformCoinWithTokensReq, +) -> MmResult +where + Platform: PlatformWithTokensActivationOps + MmCoin + /*TryFromCoinProtocol +*/ Send + Sync + 'static + Clone, + Platform::InProgressStatus: InitPlatformWithTokensInitialStatus, + EnablePlatformCoinWithTokensError: From, // TODO: check if Into also works + (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, +{ + if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { + return MmError::err(EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated( + request.ticker, + )); + } + + //let (coin_conf, protocol_info) = coin_conf_with_protocol::(&ctx, &request.ticker)?; + + let coins_act_ctx = + CoinsActivationContext::from_ctx(&ctx).map_to_mm(EnablePlatformCoinWithTokensError::Internal)?; + let spawner = ctx.spawner(); + let task = InitPlatformCoinWithTokensTask:: { + ctx, + request, + //coin_conf, + //protocol_info, + }; + let task_manager = Platform::rpc_task_manager(&coins_act_ctx); + + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + .mm_err(|e| EnablePlatformCoinWithTokensError::Internal(e.to_string()))?; + + Ok(EnablePlatformCoinWithTokensResponse { task_id }) +} + +pub async fn init_platform_coin_with_tokens_status( + ctx: MmArc, + req: EnablePlatformCoinWithTokensStatusRequest, +) -> MmResult< + RpcTaskStatus< + Platform::ActivationResult, + EnablePlatformCoinWithTokensError, + Platform::InProgressStatus, + Platform::AwaitingStatus, + >, + EnablePlatformCoinWithTokensError, +> +where + Platform: PlatformWithTokensActivationOps + MmCoin + /*TryFromCoinProtocol +*/ Send + Sync + 'static + Clone, + EnablePlatformCoinWithTokensError: From, // + SerMmErrorType, +{ + let coins_act_ctx = + CoinsActivationContext::from_ctx(&ctx).map_to_mm(EnablePlatformCoinWithTokensError::Internal)?; + let mut task_manager = Platform::rpc_task_manager(&coins_act_ctx) + .lock() + .map_to_mm(|poison| EnablePlatformCoinWithTokensError::Internal(poison.to_string()))?; + task_manager + .task_status(req.task_id, req.forget_if_finished) + .or_mm_err(|| EnablePlatformCoinWithTokensError::NoSuchTask(req.task_id)) + .map(|rpc_task| rpc_task.map_err(|e| e)) +} + +pub mod for_tests { + use coins::MmCoin; + use common::{executor::Timer, now_ms, wait_until_ms}; + use mm2_core::mm_ctx::MmArc; + use mm2_err_handle::prelude::MmResult; + use rpc_task::RpcTaskStatus; + + use super::{init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, + EnablePlatformCoinWithTokensError, EnablePlatformCoinWithTokensReq, + EnablePlatformCoinWithTokensStatusRequest, InitPlatformWithTokensInitialStatus, NotEqual, + PlatformWithTokensActivationOps}; + + /// test helper to activate platform coin with waiting for the result + pub async fn init_platform_coin_with_tokens_loop( + ctx: MmArc, + request: EnablePlatformCoinWithTokensReq, + ) -> MmResult + where + Platform: PlatformWithTokensActivationOps + MmCoin + /*TryFromCoinProtocol +*/ Clone + Send + Sync + 'static, + Platform::InProgressStatus: InitPlatformWithTokensInitialStatus, + EnablePlatformCoinWithTokensError: From, + (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, + { + let init_result = init_platform_coin_with_tokens::(ctx.clone(), request) + .await + .unwrap(); + let timeout = wait_until_ms(150000); + loop { + if now_ms() > timeout { + panic!("init_standalone_coin timed out"); + } + let status_req = EnablePlatformCoinWithTokensStatusRequest { + task_id: init_result.task_id, + forget_if_finished: true, + }; + let status_res = init_platform_coin_with_tokens_status::(ctx.clone(), status_req).await; + if let Ok(status) = status_res { + match status { + RpcTaskStatus::Ok(result) => break Ok(result), + RpcTaskStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } else { + panic!("could not get init_standalone_coin status"); + } + } + } +} diff --git a/mm2src/coins_activation/src/solana_with_tokens_activation.rs b/mm2src/coins_activation/src/solana_with_tokens_activation.rs index b6bd7b123d..96bf21a08f 100644 --- a/mm2src/coins_activation/src/solana_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/solana_with_tokens_activation.rs @@ -1,4 +1,6 @@ +use crate::context::CoinsActivationContext; use crate::platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPlatformBalance, + InitPlatformCoinWithTokensTask, InitPlatformTaskManagerShared, InitTokensAsMmCoinsError, PlatformWithTokensActivationOps, RegisterTokenInfo, TokenActivationParams, TokenActivationRequest, TokenAsMmCoinInitializer, TokenInitializer, TokenOf}; @@ -14,11 +16,13 @@ use coins::{BalanceError, CoinBalance, CoinProtocol, MarketCoinOps, MmCoinEnum, SolanaActivationParams, SolanaCoin, SplToken}; use common::Future01CompatExt; use common::{drop_mutability, true_f}; +use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoCtxError; use futures::future::try_join_all; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; +use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManagerShared, RpcTaskTypes}; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; use std::collections::HashMap; @@ -182,6 +186,10 @@ impl PlatformWithTokensActivationOps for SolanaCoin { type ActivationResult = SolanaWithTokensActivationResult; type ActivationError = SolanaWithTokensActivationError; + type InProgressStatus = InitPlatformCoinWithTokensStandardInProgressStatus; + type AwaitingStatus = InitPlatformCoinWithTokensStandardAwaitingStatus; + type UserAction = InitPlatformCoinWithTokensStandardUserAction; + async fn enable_platform_coin( ctx: MmArc, ticker: String, @@ -221,6 +229,7 @@ impl PlatformWithTokensActivationOps for SolanaCoin { async fn get_activation_result( &self, + task_handle: Option<&RpcTaskHandle>>, activation_request: &Self::ActivationRequest, ) -> Result> { let current_block = self @@ -288,4 +297,36 @@ impl PlatformWithTokensActivationOps for SolanaCoin { _initial_balance: Option, ) { } + + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitPlatformTaskManagerShared { + unimplemented!() + } +} + +/* +pub struct InitSolanaTask {} + +pub type SolanaTaskManagerShared = RpcTaskManagerShared; +pub type SolanaActivationV2AwaitingStatus = HwRpcTaskAwaitingStatus; +pub type SolanaActivationV2UserAction = HwRpcTaskUserAction; + +impl RpcTaskTypes for InitSolanaTask { + type Item = (); + type Error = (); + type InProgressStatus = (); + type AwaitingStatus = SolanaActivationV2AwaitingStatus; + type UserAction = SolanaActivationV2UserAction; +} + +#[async_trait] +impl RpcTask for InitSolanaTask { + fn initial_status(&self) -> Self::InProgressStatus { todo!() } + + async fn cancel(self) { todo!() } + + async fn run(&mut self, _task_handle: &RpcTaskHandle) -> Result> { + Ok(()) + } } + +*/ diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 92369caf38..a8c319077a 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -1,7 +1,11 @@ +use crate::context::CoinsActivationContext; use crate::platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPlatformBalance, - InitTokensAsMmCoinsError, PlatformWithTokensActivationOps, RegisterTokenInfo, - TokenActivationParams, TokenActivationRequest, TokenAsMmCoinInitializer, - TokenInitializer, TokenOf}; + InitPlatformCoinWithTokensStandardAwaitingStatus, + InitPlatformCoinWithTokensStandardInProgressStatus, + InitPlatformCoinWithTokensStandardUserAction, InitPlatformCoinWithTokensTask, + InitPlatformTaskManagerShared, InitTokensAsMmCoinsError, + PlatformWithTokensActivationOps, RegisterTokenInfo, TokenActivationParams, + TokenActivationRequest, TokenAsMmCoinInitializer, TokenInitializer, TokenOf}; use crate::prelude::*; use async_trait::async_trait; use coins::hd_wallet::HDAccountAddressId; @@ -16,6 +20,7 @@ use common::{true_f, Future01CompatExt}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; +use rpc_task::RpcTaskHandle; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -126,7 +131,7 @@ impl From for InitTokensAsMmCoinsError { } } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct TendermintActivationResult { ticker: String, address: String, @@ -163,6 +168,10 @@ impl PlatformWithTokensActivationOps for TendermintCoin { type ActivationResult = TendermintActivationResult; type ActivationError = TendermintInitError; + type InProgressStatus = InitPlatformCoinWithTokensStandardInProgressStatus; + type AwaitingStatus = InitPlatformCoinWithTokensStandardAwaitingStatus; + type UserAction = InitPlatformCoinWithTokensStandardUserAction; + async fn enable_platform_coin( ctx: MmArc, ticker: String, @@ -217,6 +226,7 @@ impl PlatformWithTokensActivationOps for TendermintCoin { async fn get_activation_result( &self, + _task_handle: Option<&RpcTaskHandle>>, activation_request: &Self::ActivationRequest, ) -> Result> { let current_block = self.current_block().compat().await.map_to_mm(|e| TendermintInitError { @@ -275,4 +285,8 @@ impl PlatformWithTokensActivationOps for TendermintCoin { let settings = AbortSettings::info_on_abort(format!("tendermint_history_loop stopped for {}", self.ticker())); self.spawner().spawn_with_settings(fut, settings); } + + fn rpc_task_manager(_activation_ctx: &CoinsActivationContext) -> &InitPlatformTaskManagerShared { + unimplemented!() + } } diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 6d5c621019..56e0a6399e 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -43,7 +43,7 @@ where let xpub_extractor = if coin.is_trezor() { Some( - RpcTaskXPubExtractor::new(ctx, task_handle, xpub_extractor_rpc_statuses()) + RpcTaskXPubExtractor::new(ctx, task_handle, xpub_extractor_rpc_statuses(), false) .mm_err(|_| InitUtxoStandardError::HwError(HwRpcError::NotInitialized))?, ) } else { diff --git a/mm2src/crypto/src/hw_error.rs b/mm2src/crypto/src/hw_error.rs index 1efd4b243f..eccfeb69b8 100644 --- a/mm2src/crypto/src/hw_error.rs +++ b/mm2src/crypto/src/hw_error.rs @@ -90,7 +90,7 @@ impl From for HwError { /// 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, Display, Serialize, PartialEq, Deserialize)] pub enum HwRpcError { #[display(fmt = "No Trezor device available")] NoTrezorDeviceAvailable = 0, diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index 55fed01770..eebf00fd1f 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -122,7 +122,8 @@ async fn trade_base_rel_electrum( log!("enable MORTY (bob): {:?}", rc); }, Mm2InitPrivKeyPolicy::GlobalHDAccount => { - let rc = enable_utxo_v2_electrum(&mm_bob, "RICK", doc_electrums(), bob_path_to_address.clone(), 60, None).await; + let rc = + enable_utxo_v2_electrum(&mm_bob, "RICK", doc_electrums(), bob_path_to_address.clone(), 60, None).await; log!("enable RICK (bob): {:?}", rc); let rc = enable_utxo_v2_electrum(&mm_bob, "MORTY", marty_electrums(), bob_path_to_address, 60, None).await; log!("enable MORTY (bob): {:?}", rc); @@ -139,10 +140,18 @@ async fn trade_base_rel_electrum( log!("enable MORTY (alice): {:?}", rc); }, Mm2InitPrivKeyPolicy::GlobalHDAccount => { - let rc = - enable_utxo_v2_electrum(&mm_alice, "RICK", doc_electrums(), alice_path_to_address.clone(), 60, None).await; + let rc = enable_utxo_v2_electrum( + &mm_alice, + "RICK", + doc_electrums(), + alice_path_to_address.clone(), + 60, + None, + ) + .await; log!("enable RICK (alice): {:?}", rc); - let rc = enable_utxo_v2_electrum(&mm_alice, "MORTY", marty_electrums(), alice_path_to_address, 60, None).await; + let rc = + enable_utxo_v2_electrum(&mm_alice, "MORTY", marty_electrums(), alice_path_to_address, 60, None).await; log!("enable MORTY (alice): {:?}", rc); }, } diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 814e95fff1..137d8b292e 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -1,11 +1,12 @@ #[cfg(all(feature = "zhtlc-native-tests", not(target_arch = "wasm32")))] use super::enable_z_coin; use crate::integration_tests_common::*; -use coins::utxo::for_tests::test_withdraw_init_loop; -use coins::utxo::{utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}, - utxo_standard::UtxoStandardCoin, - UtxoActivationParams}; -use coins::PrivKeyBuildPolicy; +use coins::eth::{eth_coin_from_conf_and_request, EthCoin, ETH_GAS}; +use coins::for_tests::test_withdraw_init_loop; +use coins::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps}; +use coins::utxo::{utxo_standard::UtxoStandardCoin, UtxoActivationParams}; +use coins::{lp_coinfind, CoinProtocol, MmCoinEnum, PrivKeyBuildPolicy}; +use coins_activation::platform_for_tests::init_platform_coin_with_tokens_loop; use coins_activation::{for_tests::init_standalone_coin_loop, InitStandaloneCoinReq}; use common::executor::Timer; use common::now_ms; @@ -28,19 +29,20 @@ use mm2_test_helpers::for_tests::init_trezor_rpc; use mm2_test_helpers::for_tests::init_trezor_status_rpc; #[cfg(all(not(target_arch = "wasm32")))] use mm2_test_helpers::for_tests::mm_ctx_with_custom_db_with_conf; -use mm2_test_helpers::for_tests::{btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, - check_recent_swaps, enable_eth_coin, enable_qrc20, eth_jst_testnet_conf, - eth_testnet_conf, find_metrics_in_json, from_env_file, get_shared_db_id, - init_withdraw, mm_spat, morty_conf, rick_conf, sign_message, start_swaps, - tbtc_legacy_conf, tbtc_segwit_conf, tbtc_with_spv_conf, test_qrc20_history_impl, - tqrc20_conf, verify_message, wait_for_swap_contract_negotiation, +use mm2_test_helpers::for_tests::{account_balance, btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, + check_recent_swaps, enable_eth_coin, enable_eth_with_tokens, enable_qrc20, + enable_utxo_v2_electrum, eth_jst_testnet_conf, + eth_sepolia_trezor_firmware_compat_conf, eth_testnet_conf, find_metrics_in_json, + from_env_file, get_new_address, get_shared_db_id, init_withdraw, mm_spat, + morty_conf, rick_conf, sign_message, start_swaps, tbtc_legacy_conf, + tbtc_segwit_conf, tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, + verify_message, wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, wait_for_swaps_finish_and_check_status, wait_till_history_has_records, withdraw_status, MarketMakerIt, Mm2InitPrivKeyPolicy, - Mm2TestConf, Mm2TestConfForSwap, RaiiDump, ETH_DEV_NODES, ETH_DEV_SWAP_CONTRACT, - ETH_DEV_TOKEN_CONTRACT, ETH_MAINNET_NODE, ETH_MAINNET_SWAP_CONTRACT, MORTY, - QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS, enable_utxo_v2_electrum, - enable_eth_with_tokens, account_balance, get_new_address, - DOC_ELECTRUM_ADDRS, MARTY_ELECTRUM_ADDRS, T_BCH_ELECTRUMS}; + Mm2TestConf, Mm2TestConfForSwap, RaiiDump, DOC_ELECTRUM_ADDRS, ETH_DEV_NODES, + ETH_DEV_SWAP_CONTRACT, ETH_DEV_TOKEN_CONTRACT, ETH_MAINNET_NODE, + ETH_MAINNET_SWAP_CONTRACT, ETH_SEPOLIA_NODE, MARTY_ELECTRUM_ADDRS, MORTY, + QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS, T_BCH_ELECTRUMS}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::*; use rpc_task::{rpc_common::RpcTaskStatusRequest, RpcTaskStatus}; @@ -841,8 +843,15 @@ async fn trade_base_rel_electrum( log!("enable_morty (alice): {:?}", enable_morty); }, Mm2InitPrivKeyPolicy::GlobalHDAccount => { - let enable_rick = - enable_utxo_v2_electrum(&mm_alice, "RICK", doc_electrums(), alice_path_to_address.clone(), 60, None).await; + let enable_rick = enable_utxo_v2_electrum( + &mm_alice, + "RICK", + doc_electrums(), + alice_path_to_address.clone(), + 60, + None, + ) + .await; log!("enable_rick (alice): {:?}", enable_rick); let enable_morty = enable_utxo_v2_electrum(&mm_alice, "MORTY", marty_electrums(), alice_path_to_address, 60, None).await; @@ -7351,7 +7360,14 @@ fn test_btc_block_header_sync() { let (_dump_log, _dump_dashboard) = mm_bob.mm_dump(); log!("log path: {}", mm_bob.log_path.display()); - let utxo_bob = block_on(enable_utxo_v2_electrum(&mm_bob, "BTC", btc_electrums(), None, 600, None)); + let utxo_bob = block_on(enable_utxo_v2_electrum( + &mm_bob, + "BTC", + btc_electrums(), + None, + 600, + None, + )); log!("enable UTXO bob {:?}", utxo_bob); block_on(mm_bob.stop()).unwrap(); @@ -7456,7 +7472,8 @@ fn test_enable_utxo_with_enable_hd() { for _ in 0..8 { block_on(get_new_address(&mm_hd_0, "BTC-segwit", 77, Some(Bip44Chain::External))); } - let account_balance = block_on(account_balance(&mm_hd_0, "BTC-segwit", 77, Bip44Chain::External)); + let account_balance: HDAccountBalanceResponse = + block_on(account_balance(&mm_hd_0, "BTC-segwit", 77, Bip44Chain::External)); assert_eq!( account_balance.addresses[7].address, "bc1q0dxnd7afj997a40j86a8a6dq3xs3dwm7rkzams" @@ -7809,7 +7826,9 @@ pub enum InitTrezorStatus { pub async fn mm_ctx_with_trezor(conf: Json) -> MmArc { let ctx = mm_ctx_with_custom_db_with_conf(Some(conf)); - CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123456").unwrap(); // for now we need passphrase seed for init + //CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123456").unwrap(); // for now we need passphrase seed for init + //CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "spice describe gravity federal blast come thank unfair canal monkey style afraid").unwrap(); // for now we need passphrase seed for init + CryptoCtx::init_with_global_hd_account(ctx.clone(), "nothing tail captain royal canoe pencil pair arch ice west vintage thumb party task scrub ridge shift argue churn always forget island jelly trumpet").unwrap(); let req: InitHwRequest = serde_json::from_value(json!({ "device_pubkey": null })).unwrap(); let res = match init_trezor(ctx.clone(), req).await { Ok(res) => res, @@ -7861,7 +7880,6 @@ fn test_withdraw_from_trezor_segwit_no_rpc() { let mm_conf = json!({ "coins": [coin_conf] }); let ctx = block_on(mm_ctx_with_trezor(mm_conf)); - let priv_key_policy = PrivKeyBuildPolicy::Trezor; let enable_req = json!({ "method": "electrum", "coin": ticker, @@ -7878,22 +7896,14 @@ fn test_withdraw_from_trezor_segwit_no_rpc() { block_on(init_standalone_coin_loop::(ctx.clone(), request)) .expect("coin activation must be successful"); - let builder = UtxoArcBuilder::new( - &ctx, - ticker, - &coin_conf, - &activation_params, - priv_key_policy, - UtxoStandardCoin::from, - ); - let fields = block_on(builder.build_utxo_fields()).unwrap(); let tx_details = block_on(test_withdraw_init_loop( ctx.clone(), - fields, + //fields, ticker, "tb1q3zkv6g29ku3jh9vdkhxlpyek44se2s0zrv7ctn", "0.00001", "m/84'/1'/0'/0/0", + None, )) .expect("withdraw must end successfully"); log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); @@ -8035,3 +8045,107 @@ fn test_withdraw_from_trezor_p2pkh_rpc() { )); log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); } + +/// We cannot put this code in coins/eth_tests.rs as trezor init needs some structs in mm2_main +#[test] +pub fn eth_my_balance() { + let req = json!({ + "method": "enable", + "coin": "ETH", + "urls": ETH_SEPOLIA_NODE, + "swap_contract_address": ETH_DEV_SWAP_CONTRACT, + "priv_key_policy": "Trezor", + }); + + let mut eth_conf = eth_sepolia_trezor_firmware_compat_conf(); + eth_conf["mm2"] = 2.into(); + let mm_conf = json!({ "coins": [eth_conf] }); + + let ctx = block_on(mm_ctx_with_trezor(mm_conf)); + let priv_key_policy = PrivKeyBuildPolicy::Trezor; + // this activate method does not create a default hd wallet account what is needed for trezor + // maybe make a new account as a separate call? + // for that we need get_activation_result() to be called (which calls enable_balance and then create_new_account) + let eth_coin = block_on(eth_coin_from_conf_and_request( + &ctx, + "ETH", + ð_conf, + &req, + CoinProtocol::ETH, + priv_key_policy, + )) + .unwrap(); + + let account_balance = block_on(eth_coin.account_balance_rpc(AccountBalanceParams { + account_index: 0, + chain: crypto::Bip44Chain::External, + limit: Default::default(), + paging_options: Default::default(), + })) + .unwrap(); + println!("account_balance={:?}", account_balance); +} + +/// Tool to run eth withdraw directly with trezor device or emulator (no rpc version, added for easier debugging) +/// run cargo test with '--ignored' option +/// to use trezor emulator add '--features trezor-udp' option to cargo test params +#[test] +#[ignore] +#[cfg(all(not(target_arch = "wasm32")))] +fn test_eth_withdraw_from_trezor_no_rpc() { + use coins::WithdrawFee; + + let ticker = "ETH"; + + let mut eth_conf = eth_sepolia_trezor_firmware_compat_conf(); + eth_conf["mm2"] = 2.into(); + let mm_conf = json!({ "coins": [eth_conf] }); + + let ctx = block_on(mm_ctx_with_trezor(mm_conf)); + block_on(init_platform_coin_with_tokens_loop::( + ctx.clone(), + json::from_value(json!({ + "ticker": ticker, + "rpc_mode": "Http", + "nodes": [ + {"url": "https://rpc2.sepolia.org"}, + {"url": "https://rpc.sepolia.org/"} + ], + "swap_contract_address": ETH_DEV_SWAP_CONTRACT, + "erc20_tokens_requests": [], + "priv_key_policy": "Trezor" + })) + .unwrap(), + )) + .unwrap(); + + let coin = block_on(lp_coinfind(&ctx, "ETH")).unwrap(); + let eth_coin = if let Some(MmCoinEnum::EthCoin(eth_coin)) = coin { + eth_coin + } else { + panic!("eth coin not enabled"); + }; + let account_balance = block_on(eth_coin.account_balance_rpc(AccountBalanceParams { + account_index: 0, + chain: crypto::Bip44Chain::External, + limit: 1, + paging_options: Default::default(), + })) + .unwrap(); + println!("account_balance={:?}", account_balance); + + let tx_details = block_on(test_withdraw_init_loop( + ctx.clone(), + ticker, + "0xc06eFafa6527fc4b3C8F69Afb173964A3780a104", + "0.00001", + "m/44'/1'/0'/0/0", + Some(WithdrawFee::EthGas { + gas: ETH_GAS, + gas_price: 100_000_000_000_i64.into(), + }), + )) + .expect("withdraw must end successfully"); + log!("tx_hex={}", serde_json::to_string(&tx_details.tx_hex).unwrap()); + // TODO: check maybe we need to disconnect trezor somehow +} diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index a94b0485b1..41f0455fbb 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -745,10 +745,25 @@ pub fn eth_sepolia_conf() -> Json { json!({ "coin": "ETH", "name": "ethereum", + "derivation_path": "m/44'/60'", // Note: 'coin type' for eth main and testnet are the same "chain_id": 11155111, "protocol": { "type": "ETH" - } + }, + "trezor_coin": "Ethereum" + }) +} + +pub fn eth_sepolia_trezor_firmware_compat_conf() -> Json { + json!({ + "coin": "ETH", + "name": "ethereum", + "derivation_path": "m/44'/1'", // Note: trezor use coin type 1' for eth for testnet (SLIP44_TESTNET) + "chain_id": 11155111, + "protocol": { + "type": "ETH" + }, + "trezor_coin": "tETH" }) } diff --git a/mm2src/trezor/Cargo.toml b/mm2src/trezor/Cargo.toml index bc7fca098c..42019448b8 100644 --- a/mm2src/trezor/Cargo.toml +++ b/mm2src/trezor/Cargo.toml @@ -19,6 +19,11 @@ rand = { version = "0.7", features = ["std", "wasm-bindgen"] } rpc_task = { path = "../rpc_task" } serde = "1.0" serde_derive = "1.0" +ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +# rlp = { version = "0.5" } +# ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +primitive-types = "0.11.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } diff --git a/mm2src/trezor/build.rs b/mm2src/trezor/build.rs index c40792c165..01634fc3dc 100644 --- a/mm2src/trezor/build.rs +++ b/mm2src/trezor/build.rs @@ -1,11 +1,15 @@ #[allow(dead_code)] -const PROTOS: [&str; 4] = [ +const PROTOS: [&str; 6] = [ "proto/messages.proto", "proto/messages-common.proto", "proto/messages-management.proto", "proto/messages-bitcoin.proto", + "proto/messages-ethereum-definitions.proto", + "proto/messages-ethereum.proto", ]; +/// Note this builder is not used and .proto files are just for info of message layouts. +/// Instead message structs are created manually and auto derivation macro prost::Message was added fn main() { // prost_build::compile_protos(&PROTOS, &["proto"]).unwrap(); } diff --git a/mm2src/trezor/proto/messages-ethereum-definitions.proto b/mm2src/trezor/proto/messages-ethereum-definitions.proto new file mode 100644 index 0000000000..d770032db0 --- /dev/null +++ b/mm2src/trezor/proto/messages-ethereum-definitions.proto @@ -0,0 +1,60 @@ +syntax = "proto2"; +package hw.trezor.messages.ethereum_definitions; + +// Sugar for easier handling in Java +option java_package = "com.satoshilabs.trezor.lib.protobuf"; +option java_outer_classname = "TrezorMessageEthereumDefinitions"; + + +/** + * Ethereum definitions type enum. + * Used to check the encoded EthereumNetworkInfo or EthereumTokenInfo message. + */ + enum EthereumDefinitionType { + NETWORK = 0; + TOKEN = 1; +} + +/** + * Ethereum network definition. Used to (de)serialize the definition. + * + * Definition types should not be cross-parseable, i.e., it should not be possible to + * incorrectly parse network info as token info or vice versa. + * To achieve that, the first field is wire type varint while the second field is wire type + * length-delimited. Both are a mismatch for the token definition. + * + * @embed + */ +message EthereumNetworkInfo { + required uint64 chain_id = 1; + required string symbol = 2; + required uint32 slip44 = 3; + required string name = 4; +} + +/** + * Ethereum token definition. Used to (de)serialize the definition. + * + * Definition types should not be cross-parseable, i.e., it should not be possible to + * incorrectly parse network info as token info or vice versa. + * To achieve that, the first field is wire type length-delimited while the second field + * is wire type varint. Both are a mismatch for the network definition. + * + * @embed + */ +message EthereumTokenInfo { + required bytes address = 1; + required uint64 chain_id = 2; + required string symbol = 3; + required uint32 decimals = 4; + required string name = 5; +} + +/** + * Contains an encoded Ethereum network and/or token definition. See ethereum-definitions.md for details. + * @embed + */ +message EthereumDefinitions { + optional bytes encoded_network = 1; // encoded Ethereum network + optional bytes encoded_token = 2; // encoded Ethereum token +} diff --git a/mm2src/trezor/proto/messages-ethereum.proto b/mm2src/trezor/proto/messages-ethereum.proto new file mode 100644 index 0000000000..dae98de869 --- /dev/null +++ b/mm2src/trezor/proto/messages-ethereum.proto @@ -0,0 +1,181 @@ +syntax = "proto2"; +package hw.trezor.messages.ethereum; + +// Sugar for easier handling in Java +option java_package = "com.satoshilabs.trezor.lib.protobuf"; +option java_outer_classname = "TrezorMessageEthereum"; + +import "messages-common.proto"; +import "messages-ethereum-definitions.proto"; + + +/** + * Request: Ask device for public key corresponding to address_n path + * @start + * @next EthereumPublicKey + * @next Failure + */ +message EthereumGetPublicKey { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + optional bool show_display = 2; // optionally show on display before sending the result +} + +/** + * Response: Contains public key derived from device private seed + * @end + */ +message EthereumPublicKey { + required hw.trezor.messages.common.HDNodeType node = 1; // BIP32 public node + required string xpub = 2; // serialized form of public node +} + +/** + * Request: Ask device for Ethereum address corresponding to address_n path + * @start + * @next EthereumAddress + * @next Failure + */ +message EthereumGetAddress { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + optional bool show_display = 2; // optionally show on display before sending the result + optional bytes encoded_network = 3; // encoded Ethereum network, see ethereum-definitions.md for details + optional bool chunkify = 4; // display the address in chunks of 4 characters +} + +/** + * Response: Contains an Ethereum address derived from device private seed + * @end + */ +message EthereumAddress { + optional bytes _old_address = 1 [deprecated=true]; // trezor <1.8.0, <2.1.0 - raw bytes of Ethereum address + optional string address = 2; // Ethereum address as hex-encoded string +} + +/** + * Request: Ask device to sign transaction + * gas_price, gas_limit and chain_id must be provided and non-zero. + * All other fields are optional and default to value `0` if missing. + * Note: the first at most 1024 bytes of data MUST be transmitted as part of this message. + * @start + * @next EthereumTxRequest + * @next Failure + */ +message EthereumSignTx { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + optional bytes nonce = 2 [default='']; // <=256 bit unsigned big endian + required bytes gas_price = 3; // <=256 bit unsigned big endian (in wei) + required bytes gas_limit = 4; // <=256 bit unsigned big endian + optional string to = 11 [default='']; // recipient address + optional bytes value = 6 [default='']; // <=256 bit unsigned big endian (in wei) + optional bytes data_initial_chunk = 7 [default='']; // The initial data chunk (<= 1024 bytes) + optional uint32 data_length = 8 [default=0]; // Length of transaction payload + required uint64 chain_id = 9; // Chain Id for EIP 155 + optional uint32 tx_type = 10; // Used for Wanchain + optional ethereum_definitions.EthereumDefinitions definitions = 12; // network and/or token definitions for tx + optional bool chunkify = 13; // display the address in chunks of 4 characters +} + +/** + * Request: Ask device to sign EIP1559 transaction + * Note: the first at most 1024 bytes of data MUST be transmitted as part of this message. + * @start + * @next EthereumTxRequest + * @next Failure + */ +message EthereumSignTxEIP1559 { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes nonce = 2; // <=256 bit unsigned big endian + required bytes max_gas_fee = 3; // <=256 bit unsigned big endian (in wei) + required bytes max_priority_fee = 4; // <=256 bit unsigned big endian (in wei) + required bytes gas_limit = 5; // <=256 bit unsigned big endian + optional string to = 6 [default='']; // recipient address + required bytes value = 7; // <=256 bit unsigned big endian (in wei) + optional bytes data_initial_chunk = 8 [default='']; // The initial data chunk (<= 1024 bytes) + required uint32 data_length = 9; // Length of transaction payload + required uint64 chain_id = 10; // Chain Id for EIP 155 + repeated EthereumAccessList access_list = 11; // Access List + optional ethereum_definitions.EthereumDefinitions definitions = 12; // network and/or token definitions for tx + optional bool chunkify = 13; // display the address in chunks of 4 characters + + message EthereumAccessList { + required string address = 1; + repeated bytes storage_keys = 2; + } +} + +/** + * Response: Device asks for more data from transaction payload, or returns the signature. + * If data_length is set, device awaits that many more bytes of payload. + * Otherwise, the signature_* fields contain the computed transaction signature. All three fields will be present. + * @end + * @next EthereumTxAck + */ +message EthereumTxRequest { + optional uint32 data_length = 1; // Number of bytes being requested (<= 1024) + optional uint32 signature_v = 2; // Computed signature (recovery parameter, limited to 27 or 28) + optional bytes signature_r = 3; // Computed signature R component (256 bit) + optional bytes signature_s = 4; // Computed signature S component (256 bit) +} + +/** + * Request: Transaction payload data. + * @next EthereumTxRequest + */ +message EthereumTxAck { + required bytes data_chunk = 1; // Bytes from transaction payload (<= 1024 bytes) +} + +/** + * Request: Ask device to sign message + * @start + * @next EthereumMessageSignature + * @next Failure + */ +message EthereumSignMessage { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes message = 2; // message to be signed + optional bytes encoded_network = 3; // encoded Ethereum network, see ethereum-definitions.md for details +} + +/** + * Response: Signed message + * @end + */ +message EthereumMessageSignature { + required bytes signature = 2; // signature of the message + required string address = 3; // address used to sign the message +} + +/** + * Request: Ask device to verify message + * @start + * @next Success + * @next Failure + */ +message EthereumVerifyMessage { + required bytes signature = 2; // signature to verify + required bytes message = 3; // message to verify + required string address = 4; // address to verify +} + +/** + * Request: Ask device to sign hash of typed data + * @start + * @next EthereumTypedDataSignature + * @next Failure + */ +message EthereumSignTypedHash { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes domain_separator_hash = 2; // Hash of domainSeparator of typed data to be signed + optional bytes message_hash = 3; // Hash of the data of typed data to be signed (empty if domain-only data) + optional bytes encoded_network = 4; // encoded Ethereum network, see ethereum-definitions.md for details +} + +/** + * Response: Signed typed data + * @end + */ +message EthereumTypedDataSignature { + required bytes signature = 1; // signature of the typed data + required string address = 2; // address used to sign the typed data +} diff --git a/mm2src/trezor/src/eth/eth_command.rs b/mm2src/trezor/src/eth/eth_command.rs new file mode 100644 index 0000000000..d6a7322ada --- /dev/null +++ b/mm2src/trezor/src/eth/eth_command.rs @@ -0,0 +1,149 @@ +use crate::proto::messages_ethereum as proto_ethereum; +use crate::result_handler::ResultHandler; +use crate::{serialize_derivation_path, OperationFailure, TrezorError, TrezorResponse, TrezorResult, TrezorSession}; +use bip32::DerivationPath; +use ethcore_transaction::{Action, Transaction as UnSignedEthTx, UnverifiedTransaction as UnverifiedEthTx}; +use ethkey::Signature; +use hw_common::primitives::XPub; +use mm2_err_handle::prelude::MmError; +use primitive_types::H256; + +impl<'a> TrezorSession<'a> { + pub async fn get_eth_address(&mut self, derivation_path: DerivationPath) -> TrezorResult> { + let req = proto_ethereum::EthereumGetAddress { + address_n: derivation_path.iter().map(|child| child.0).collect(), + show_display: None, + encoded_network: None, + chunkify: None, + }; + //let mut tx_request = self.send_get_eth_address_req(req).await?.ack_all().await?; + let result_handler = ResultHandler::::new(Ok); + let result = self.call(req, result_handler).await?.ack_all().await?; + Ok(result.address) + } + + pub async fn get_eth_public_key<'b>( + &'b mut self, + derivation_path: DerivationPath, + show_display: bool, + ) -> TrezorResult> { + let req = proto_ethereum::EthereumGetPublicKey { + address_n: serialize_derivation_path(&derivation_path), + show_display: Some(show_display), + }; + let result_handler = ResultHandler::new(|m: proto_ethereum::EthereumPublicKey| Ok(m.xpub)); + self.call(req, result_handler).await + } + + pub async fn sign_eth_tx( + &mut self, + derivation_path: DerivationPath, + unsigned_tx: &UnSignedEthTx, + chain_id: u64, + ) -> TrezorResult { + let mut data: Vec = vec![]; + let req = to_sign_eth_message(unsigned_tx, &derivation_path, chain_id, &mut data); + let mut tx_request = self.send_sign_eth_tx(req).await?.ack_all().await?; + + while let Some(data_length) = tx_request.data_length { + if data_length > 0 { + println!("data_length={}", data_length); + let req = proto_ethereum::EthereumTxAck { + data_chunk: data.splice(..std::cmp::min(1024, data.len()), []).collect(), + }; + //ack.set_data_chunk(data.splice(..std::cmp::min(1024, data.len()), []).collect()); + + //resp = self.call(ack, Box::new(|_, m: proto_ethereum::EthereumTxRequest| Ok(m)))?.ok()?; + tx_request = self.send_eth_tx_ack(req).await?.ack_all().await?; + } else { + break; + } + } + + let sig = extract_eth_signature(&tx_request, chain_id)?; + Ok(unsigned_tx.clone().with_signature(sig, Some(chain_id))) + } + + async fn send_sign_eth_tx<'b>( + &'b mut self, + req: proto_ethereum::EthereumSignTx, + ) -> TrezorResult> { + let result_handler = ResultHandler::::new(Ok); + self.call(req, result_handler).await + } + + async fn send_eth_tx_ack<'b>( + &'b mut self, + req: proto_ethereum::EthereumTxAck, + ) -> TrezorResult> { + let result_handler = ResultHandler::::new(Ok); + self.call(req, result_handler).await + } +} + +/// TODO: maybe there is a more standard way +fn my_trim_u8(arr: &[u8]) -> Vec { + let mut z = 0; + for i in arr { + if i == &0 { + z += 1; + } + } + arr[z..].to_vec() +} + +fn to_sign_eth_message( + unsigned_tx: &UnSignedEthTx, + derivation_path: &DerivationPath, + chain_id: u64, + data: &mut Vec, +) -> proto_ethereum::EthereumSignTx { + let mut nonce: [u8; 32] = [0; 32]; + let mut gas_price: [u8; 32] = [0; 32]; + let mut gas_limit: [u8; 32] = [0; 32]; + let mut value: [u8; 32] = [0; 32]; + + unsigned_tx.nonce.to_big_endian(&mut nonce); + unsigned_tx.gas_price.to_big_endian(&mut gas_price); + unsigned_tx.gas.to_big_endian(&mut gas_limit); + unsigned_tx.value.to_big_endian(&mut value); + + *data = unsigned_tx.data.clone(); + let addr_hex = if let Action::Call(addr) = unsigned_tx.action { + Some(format!("{:X}", addr)) + } else { + None + }; + proto_ethereum::EthereumSignTx { + address_n: serialize_derivation_path(derivation_path), // derivation_path.iter().map(|child| child.0).collect(), + nonce: Some(my_trim_u8(&nonce)), + gas_price: my_trim_u8(&gas_price), + gas_limit: my_trim_u8(&gas_limit), + to: addr_hex, + value: Some(my_trim_u8(&value)), + data_initial_chunk: Some(data.splice(..std::cmp::min(1024, data.len()), []).collect()), + data_length: if data.is_empty() { None } else { Some(data.len() as u32) }, + chain_id, + tx_type: None, + definitions: None, + chunkify: if data.is_empty() { None } else { Some(true) }, + } +} + +fn extract_eth_signature(tx_request: &proto_ethereum::EthereumTxRequest, chain_id: u64) -> TrezorResult { + match ( + tx_request.signature_r.as_ref(), + tx_request.signature_s.as_ref(), + tx_request.signature_v, + ) { + (Some(r), Some(s), Some(v)) => { + let v_fixed = if v <= 1 { v + 2 * (chain_id as u32) + 35 } else { v }; + Ok(Signature::from_rsv( + &H256::from_slice(r.as_slice()), + &H256::from_slice(s.as_slice()), + v_fixed as u8, + )) + }, + (_, _, _) => Err(MmError::new(TrezorError::Failure(OperationFailure::InvalidSignature))), + } +} diff --git a/mm2src/trezor/src/eth/mod.rs b/mm2src/trezor/src/eth/mod.rs new file mode 100644 index 0000000000..29e80663d6 --- /dev/null +++ b/mm2src/trezor/src/eth/mod.rs @@ -0,0 +1 @@ +mod eth_command; diff --git a/mm2src/trezor/src/lib.rs b/mm2src/trezor/src/lib.rs index c3847c0542..00d7c639d3 100644 --- a/mm2src/trezor/src/lib.rs +++ b/mm2src/trezor/src/lib.rs @@ -3,6 +3,7 @@ pub mod client; pub mod device_info; pub mod error; +pub mod eth; mod proto; pub mod response; mod response_processor; diff --git a/mm2src/trezor/src/proto/messages_ethereum.rs b/mm2src/trezor/src/proto/messages_ethereum.rs new file mode 100644 index 0000000000..0593bed546 --- /dev/null +++ b/mm2src/trezor/src/proto/messages_ethereum.rs @@ -0,0 +1,141 @@ +///* +/// Request: Ask device for Ethereum address corresponding to address_n path +/// @start +/// @next EthereumAddress +/// @next Failure +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumGetAddress { + /// BIP-32 path to derive the key from master node + #[prost(uint32, repeated, tag = "1")] + pub address_n: ::prost::alloc::vec::Vec, + /// optionally show on display before sending the result + #[prost(bool, optional, tag = "2")] + pub show_display: ::std::option::Option, + /// encoded Ethereum network, see ethereum-definitions.md for details + #[prost(bytes = "vec", optional, tag = "3")] + pub encoded_network: ::std::option::Option<::prost::alloc::vec::Vec>, + /// display the address in chunks of 4 characters + #[prost(bool, optional, tag = "4")] + pub chunkify: ::std::option::Option, +} + +///* +/// Response: Contains an Ethereum address derived from device private seed +/// @end +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumAddress { + /// trezor <1.8.0, <2.1.0 - raw bytes of Ethereum address + #[prost(bytes = "vec", optional, tag = "1")] + pub encoded_network: ::std::option::Option<::prost::alloc::vec::Vec>, + /// Ethereum address as hex-encoded string + #[prost(string, optional, tag = "2")] + pub address: ::core::option::Option<::prost::alloc::string::String>, +} + +///* +/// Request: Ask device for public key corresponding to address_n path +/// @start +/// @next EthereumPublicKey +/// @next Failure +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumGetPublicKey { + // BIP-32 path to derive the key from master node + #[prost(uint32, repeated, tag = "1")] + pub address_n: ::prost::alloc::vec::Vec, // repeated uint32 address_n = 1; + // optionally show on display before sending the result + #[prost(bool, optional, tag = "2")] + pub show_display: ::std::option::Option, // optional bool show_display = 2; +} + +///* +/// Response: Contains public key derived from device private seed +/// @end +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumPublicKey { + // BIP32 public node + #[prost(message, required, tag = "1")] + pub node: super::messages_common::HdNodeType, // required hw.trezor.messages.common.HDNodeType node = 1; + // serialized form of public node + #[prost(string, required, tag = "2")] + pub xpub: ::prost::alloc::string::String, // required string xpub = 2; +} + +///* +/// Request: Ask device to sign transaction +/// gas_price, gas_limit and chain_id must be provided and non-zero. +/// All other fields are optional and default to value `0` if missing. +/// Note: the first at most 1024 bytes of data MUST be transmitted as part of this message. +/// @start +/// @next EthereumTxRequest +/// @next Failure +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumSignTx { + /// BIP-32 path to derive the key from master node + #[prost(uint32, repeated, tag = "1")] + pub address_n: ::prost::alloc::vec::Vec, + /// <=256 bit unsigned big endian + #[prost(bytes = "vec", optional, tag = "2", default = "b\"\"")] + pub nonce: ::core::option::Option<::prost::alloc::vec::Vec>, + /// <=256 bit unsigned big endian (in wei) + #[prost(bytes = "vec", required, tag = "3")] + pub gas_price: ::prost::alloc::vec::Vec, + /// <=256 bit unsigned big endian + #[prost(bytes = "vec", required, tag = "4")] + pub gas_limit: ::prost::alloc::vec::Vec, + /// recipient address + #[prost(string, optional, tag = "11", default = "")] + pub to: ::core::option::Option<::prost::alloc::string::String>, + /// <=256 bit unsigned big endian (in wei) + #[prost(bytes = "vec", optional, tag = "6", default = "b\"\"")] + pub value: ::core::option::Option<::prost::alloc::vec::Vec>, + /// The initial data chunk (<= 1024 bytes) + #[prost(bytes = "vec", optional, tag = "7", default = "b\"\"")] + pub data_initial_chunk: ::core::option::Option<::prost::alloc::vec::Vec>, + /// Length of transaction payload + #[prost(uint32, optional, tag = "8", default = 0)] + pub data_length: ::core::option::Option, + /// Chain Id for EIP 155 + #[prost(uint64, required, tag = "9")] + pub chain_id: u64, + /// Used for Wanchain + #[prost(uint32, optional, tag = "10")] + pub tx_type: ::core::option::Option, + /// network and/or token definitions for tx + #[prost(message, optional, tag = "12")] + pub definitions: ::core::option::Option, + /// display the address in chunks of 4 characters + #[prost(bool, optional, tag = "13")] + pub chunkify: ::std::option::Option, +} + +///* +/// Response: Device asks for more data from transaction payload, or returns the signature. +/// If data_length is set, device awaits that many more bytes of payload. +/// Otherwise, the signature_* fields contain the computed transaction signature. All three fields will be present. +/// @end +/// @next EthereumTxAck +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumTxRequest { + /// Number of bytes being requested (<= 1024) + #[prost(uint32, optional, tag = "1")] + pub data_length: ::std::option::Option, + /// Computed signature (recovery parameter, limited to 27 or 28) + #[prost(uint32, optional, tag = "2")] + pub signature_v: ::std::option::Option, + /// Computed signature R component (256 bit) + #[prost(bytes = "vec", optional, tag = "3")] + pub signature_r: ::std::option::Option<::prost::alloc::vec::Vec>, + /// Computed signature S component (256 bit) + #[prost(bytes = "vec", optional, tag = "4")] + pub signature_s: ::std::option::Option<::prost::alloc::vec::Vec>, +} + +///* +/// Request: Transaction payload data. +/// @next EthereumTxRequest +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumTxAck { + /// Bytes from transaction payload (<= 1024 bytes) + #[prost(bytes = "vec", required, tag = "1")] + pub data_chunk: ::prost::alloc::vec::Vec, +} diff --git a/mm2src/trezor/src/proto/messages_ethereum_definitions.rs b/mm2src/trezor/src/proto/messages_ethereum_definitions.rs new file mode 100644 index 0000000000..bcba590e4c --- /dev/null +++ b/mm2src/trezor/src/proto/messages_ethereum_definitions.rs @@ -0,0 +1,12 @@ +///* +/// Ethereum definitions +/// @embed +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthereumDefinitions { + /// encoded Ethereum network + #[prost(bytes = "vec", optional, tag = "1")] + pub encoded_network: ::core::option::Option<::prost::alloc::vec::Vec>, + /// encoded Ethereum token + #[prost(bytes = "vec", optional, tag = "2")] + pub encoded_token: ::core::option::Option<::prost::alloc::vec::Vec>, +} diff --git a/mm2src/trezor/src/proto/mod.rs b/mm2src/trezor/src/proto/mod.rs index ab80efff8f..fcd3a18efe 100644 --- a/mm2src/trezor/src/proto/mod.rs +++ b/mm2src/trezor/src/proto/mod.rs @@ -6,6 +6,8 @@ use prost::bytes::BytesMut; pub mod messages; pub mod messages_bitcoin; pub mod messages_common; +pub mod messages_ethereum; +pub mod messages_ethereum_definitions; pub mod messages_management; /// This is needed by generated protobuf modules. @@ -14,6 +16,7 @@ pub(crate) use messages_common as common; use messages::MessageType; use messages_bitcoin::*; use messages_common::*; +use messages_ethereum::*; use messages_management::*; /// This macro provides the TrezorMessage trait for a protobuf message. @@ -100,3 +103,12 @@ trezor_message_impl!(TxAckPrevMeta, MessageType::TxAck); trezor_message_impl!(TxAckPrevInput, MessageType::TxAck); trezor_message_impl!(TxAckPrevOutput, MessageType::TxAck); trezor_message_impl!(TxAckPrevExtraData, MessageType::TxAck); + +// Ethereum +trezor_message_impl!(EthereumSignTx, MessageType::EthereumSignTx); +trezor_message_impl!(EthereumTxRequest, MessageType::EthereumTxRequest); +trezor_message_impl!(EthereumTxAck, MessageType::EthereumTxAck); +trezor_message_impl!(EthereumGetAddress, MessageType::EthereumGetAddress); +trezor_message_impl!(EthereumAddress, MessageType::EthereumAddress); +trezor_message_impl!(EthereumGetPublicKey, MessageType::EthereumGetPublicKey); +trezor_message_impl!(EthereumPublicKey, MessageType::EthereumPublicKey);