Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(trezor): add withdraw eth with trezor #2005

Merged
merged 62 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f30842b
Original source for UDP transport to connect to Trezor emulator
dimxy Oct 10, 2023
76c8cc9
added modifications to udp.rs to integrate it
dimxy Oct 29, 2023
d47a43f
add tests for withdraw from witness and p2pkh with trezor, add trezor…
dimxy Oct 29, 2023
a76b189
refactor trezor ConnectableDeviceWrapper (use borrow instead of consu…
dimxy Oct 18, 2023
8e56aa9
add support for trezor evm initialisation with rpc task manager and w…
dimxy Nov 12, 2023
2d2894a
fix fmt
dimxy Nov 13, 2023
599e60a
remove unused commented code
dimxy Nov 13, 2023
9a39110
add Result to pubkey_from_xpub_str
dimxy Nov 13, 2023
1d8df3c
dispose extra chain_id replay protection, fix trimming zeros for trez…
dimxy Nov 15, 2023
aed8ef8
change amount in trezor test
dimxy Nov 15, 2023
da9c6a5
add error check of signature v created by trezor
dimxy Nov 15, 2023
719bff0
fix chunk length calc for trezor eth tx request
dimxy Nov 16, 2023
1b7993a
fix target all for bip32 lib for trezor
dimxy Nov 16, 2023
b0e2f58
add trezor eth network definitions structs
dimxy Nov 17, 2023
b4ccf3f
fix deadcode clippy
dimxy Nov 26, 2023
c9a6f5c
fix clippy errors
dimxy Nov 26, 2023
1c9fa22
add pin input in test trezor init loop, remove unsupported trezor tests
dimxy Nov 26, 2023
d17e605
move trezor tests in a module
dimxy Nov 27, 2023
7cbc46e
fix wasm build
dimxy Nov 27, 2023
958617c
added: init_enable_eth rpc set, check to disable init eth trezor in n…
dimxy Dec 8, 2023
e0a4565
fix fmt
dimxy Dec 10, 2023
da421b4
remove unused comments
dimxy Dec 13, 2023
bd4ec47
refactor xpub extraction by checking coin type
dimxy Dec 13, 2023
00aa339
add test code to try create new eth acc with trezor
dimxy Dec 13, 2023
10bfef1
fix eth gap_limit
dimxy Jan 18, 2024
4988565
fix getting address for derivation_path in eth withdraw for trezor
dimxy Jan 18, 2024
56fba06
allow eth withdraw from default account (no 'from' set) for trezor
dimxy Jan 18, 2024
3004711
fix clippy warnings
dimxy Jan 18, 2024
134c2e9
fix fmt
dimxy Jan 18, 2024
ddcb180
change get_enabled_address() to return HDAddress
dimxy Jan 21, 2024
1569184
change eth withdraw build to use cached from address (instead getting…
dimxy Jan 21, 2024
c7e5971
fix eth withdraw wasm build of refactored code
dimxy Jan 21, 2024
ff45c04
fix fmt
dimxy Jan 21, 2024
3925fe5
fix eth withdraw test mock
dimxy Jan 21, 2024
ad4cec8
fix fmt
dimxy Jan 21, 2024
6e664a9
fix eth withdraw test mock
dimxy Jan 22, 2024
055f9be
reorg imports
dimxy Feb 14, 2024
0e845bb
remove extra pub
dimxy Feb 14, 2024
bfe7a29
eliminate activated_pubkey in Trezor privkey policy; fix address for …
dimxy Feb 14, 2024
29b7835
mark unused trezor proto impl function as reserved for future
dimxy Feb 14, 2024
75c75a6
fix fmt
dimxy Feb 14, 2024
b98d380
fix default privkey policy in test helper activation request
dimxy Feb 14, 2024
5896b77
Merge branch 'evm-hd-wallet' into evm-hd-wallet-trezor
dimxy Feb 18, 2024
7d55c88
fix review notes: update signing status, get_public_key() return, sig…
dimxy Feb 20, 2024
950e349
fix fmt
dimxy Feb 22, 2024
049f025
refactor old DerivationMethod struct
dimxy Feb 22, 2024
1f897d7
new and refactor eth tests:
dimxy Feb 22, 2024
7f50a64
Merge remote-tracking branch 'evm-hd-wallet' into evm-hd-wallet-trezor
shamardy Mar 1, 2024
31295f3
Refactor EthPrivKeyBuildPolicy conversion and error handling in eth.rs
shamardy Mar 2, 2024
a4ac2ca
move eth on_generating_transaction after to and from address are checked
shamardy Mar 2, 2024
50e7933
add todo to add trezor to detect_priv_key_policy
shamardy Mar 2, 2024
58ca5f7
Remove JST distributor and related functions from eth/for_tests.rs
shamardy Mar 4, 2024
b520897
fix and remove some attributes
shamardy Mar 4, 2024
df2fa11
minor refactors
shamardy Mar 4, 2024
7481338
Removed the UnexpectedDerivationMethod variant from WithdrawError enu…
shamardy Mar 4, 2024
a0c33fd
Updated the to_response method in DerivationMethod enum in mm2src/coi…
shamardy Mar 4, 2024
c90489d
use for-tests feature for test_create_new_account_init_loop
shamardy Mar 4, 2024
504bd5e
Refactor coin activation statuses names
shamardy Mar 4, 2024
3dad3bf
fix `ETH_NETWORK_DEFS` typo and minor refactors
shamardy Mar 5, 2024
28bbba7
fix task_handle in re enable platform coin
dimxy Mar 5, 2024
097ba56
fix trezor tests for updated rpc_mode param
dimxy Mar 5, 2024
185dbb0
Merge remote-tracking branch 'evm-hd-wallet' into evm-hd-wallet-trezor
shamardy Mar 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

286 changes: 107 additions & 179 deletions mm2src/coins/eth.rs

Large diffs are not rendered by default.

7 changes: 1 addition & 6 deletions mm2src/coins/eth/eth_hd_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,8 @@ impl HDWalletCoinOps for EthCoin {
extended_pubkey: &Secp256k1ExtendedPublicKey,
derivation_path: DerivationPath,
) -> HDCoinHDAddress<Self> {
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,
Expand Down
352 changes: 352 additions & 0 deletions mm2src/coins/eth/eth_withdraw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
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};
use std::ops::Deref;

shamardy marked this conversation as resolved.
Show resolved Hide resolved
cfg_wasm32! {
use web3::types::TransactionRequest;
}

#[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<WithdrawError>>;

#[allow(clippy::result_large_err)]
fn on_finishing(&self) -> Result<(), MmError<WithdrawError>>;

async fn sign_tx_with_trezor(
&self,
derivation_path: DerivationPath,
unsigned_tx: &UnSignedEthTx,
) -> Result<SignedEthTx, MmError<WithdrawError>>;

async fn build(self) -> WithdrawResult {
shamardy marked this conversation as resolved.
Show resolved Hide resolved
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
.deref()
.derivation_method
.hd_wallet()
.ok_or(WithdrawError::UnexpectedDerivationMethod)?
.derivation_path;
shamardy marked this conversation as resolved.
Show resolved Hide resolved
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)?;
shamardy marked this conversation as resolved.
Show resolved Hide resolved
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("empty trezor xpub".to_string()))?;
let my_pubkey = pubkey_from_xpub_str(my_pubkey)
.map_to_mm(|_| WithdrawError::InternalError("invalid trezor xpub".to_string()))?;
let address = public_to_address(&my_pubkey);
(None, address)
shamardy marked this conversation as resolved.
Show resolved Hide resolved
},
_ => {
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: data.clone(),
gas,
gas_price,
};
shamardy marked this conversation as resolved.
Show resolved Hide resolved

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: my_address,
to: Some(to_addr),
gas: Some(gas),
gas_price: Some(gas_price),
value: Some(eth_value),
data: Some(data.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<WithdrawError>> {
Ok(self
.task_handle
.update_in_progress_status(WithdrawInProgressStatus::GeneratingTransaction)?)
}

fn on_finishing(&self) -> Result<(), MmError<WithdrawError>> {
Ok(self
.task_handle
.update_in_progress_status(WithdrawInProgressStatus::Finishing)?)
}

async fn sign_tx_with_trezor(
&self,
derivation_path: DerivationPath,
unsigned_tx: &UnSignedEthTx,
) -> Result<SignedEthTx, MmError<WithdrawError>> {
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()))?)
}
}

#[allow(clippy::result_large_err)]
impl<'a> InitEthWithdraw<'a> {
pub fn new(
ctx: MmArc,
coin: EthCoin,
req: WithdrawRequest,
task_handle: &'a WithdrawTaskHandle,
) -> Result<InitEthWithdraw<'a>, MmError<WithdrawError>> {
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<WithdrawError>> { Ok(()) }

fn on_finishing(&self) -> Result<(), MmError<WithdrawError>> { Ok(()) }

async fn sign_tx_with_trezor(
&self,
_derivation_path: DerivationPath,
_unsigned_tx: &UnSignedEthTx,
) -> Result<SignedEthTx, MmError<WithdrawError>> {
async {
Err(MmError::new(WithdrawError::UnsupportedError(String::from(
"Trezor not supported for legacy RPC",
))))
}
.await
}
}

#[allow(clippy::result_large_err)]
impl StandardEthWithdraw {
pub fn new(coin: EthCoin, req: WithdrawRequest) -> Result<StandardEthWithdraw, MmError<WithdrawError>> {
Ok(StandardEthWithdraw { coin, req })
}
}
Loading
Loading