Skip to content

Commit

Permalink
MetaMask PoC #1167 (#1551)
Browse files Browse the repository at this point in the history
* Design and implement MetaMask provider

* Add MetaMask initialization RPCs

* Activate ETH/ERC20 with Metamask priv_key_policy

* Add `EthPrivKeyPolicy`

* Fix udeps

* Use built-in MetaMask RPC provider

* Add `MetamaskTransport`
* Rename `Web3Transport` to `HttpTransport`
* Add `MetamaskSession` to prevent concurrent requests

* Fix PR issues

* Compile `EthRpcMode::Metamask` in WASM only
* Add `rpc_mode: EthRpcMode` to `EthActivationV2Request`
* Update wasm-timer

* Minor changes

* PR fixes

* Rename `task::init_metamask::*` to `task::connect_metamask::*`

* Fix PR issues
  • Loading branch information
sergeyboyko0791 authored Dec 5, 2022
1 parent 15ff4c5 commit 7ea1436
Show file tree
Hide file tree
Showing 35 changed files with 1,510 additions and 293 deletions.
56 changes: 25 additions & 31 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ members = [
"mm2src/mm2_err_handle",
"mm2src/mm2_io",
"mm2src/mm2_libp2p",
"mm2src/mm2_metamask",
"mm2src/mm2_metrics",
"mm2src/mm2_main",
"mm2src/mm2_net",
Expand Down
144 changes: 117 additions & 27 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, Timer};
use common::log::{error, info, warn};
use common::{get_utc_timestamp, now_ms, small_rng, DEX_FEE_ADDR_RAW_PUBKEY};
use crypto::privkey::key_pair_from_secret;
use crypto::{CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy};
#[cfg(target_arch = "wasm32")]
use crypto::{MetamaskArc, MetamaskWeak};
use derive_more::Display;
use ethabi::{Contract, Token};
pub use ethcore_transaction::SignedTransaction as SignedEthTx;
Expand All @@ -49,6 +52,7 @@ use serde_json::{self as json, Value as Json};
use serialization::{CompactInteger, Serializable, Stream};
use sha3::{Digest, Keccak256};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::ops::Deref;
use std::path::PathBuf;
use std::str::FromStr;
Expand All @@ -57,30 +61,31 @@ use std::sync::{Arc, Mutex};
use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallRequest, FilterBuilder, Log, Trace,
TraceFilterBuilder, Transaction as Web3Transaction, TransactionId};
use web3::{self, Web3};
use web3_transport::{EthFeeHistoryNamespace, Web3Transport, Web3TransportNode};
use web3_transport::{http_transport::HttpTransportNode, EthFeeHistoryNamespace, Web3Transport};

use super::{coin_conf, AsyncMutex, BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner,
CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, FoundSwapTxSpend, HistorySyncState,
MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, NumConversError, NumConversResult,
PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed,
RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult,
RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, SearchForSwapTxSpendInput,
SendMakerPaymentArgs, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs,
SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SignatureError, SignatureResult, SwapOps,
TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction,
TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TxMarshalingErr,
UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr,
ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError,
VerificationResult, WatcherOps, WatcherValidatePaymentInput, WithdrawError, WithdrawFee, WithdrawFut,
WithdrawRequest, WithdrawResult};
IguanaPrivKey, MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, NumConversError,
NumConversResult, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy,
PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes,
RawTransactionResult, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared,
SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerRefundsPaymentArgs,
SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, SendTakerRefundsPaymentArgs,
SendTakerSpendsMakerPaymentArgs, SignatureError, SignatureResult, SwapOps, TradeFee, TradePreimageError,
TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails,
TransactionEnum, TransactionErr, TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod,
ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr,
ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult,
WatcherOps, WatcherValidatePaymentInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest,
WithdrawResult};
pub use rlp;

#[cfg(test)] mod eth_tests;
#[cfg(target_arch = "wasm32")] mod eth_wasm_tests;
mod web3_transport;

#[path = "eth/v2_activation.rs"] pub mod v2_activation;
use v2_activation::key_pair_from_priv_key_policy;
use v2_activation::build_address_and_priv_key_policy;

/// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol
/// Dev chain (195.201.0.6:8565) contract address: 0xa09ad3cd7e96586ebd05a2607ee56b56fb2db8fd
Expand Down Expand Up @@ -293,11 +298,71 @@ pub enum EthCoinType {
Erc20 { platform: String, token_addr: Address },
}

/// An alternative to `crate::PrivKeyBuildPolicy`, typical only for ETH coin.
pub enum EthPrivKeyBuildPolicy {
IguanaPrivKey(IguanaPrivKey),
GlobalHDAccount(GlobalHDAccountArc),
#[cfg(target_arch = "wasm32")]
Metamask(MetamaskArc),
}

impl EthPrivKeyBuildPolicy {
/// Detects the `EthPrivKeyBuildPolicy` with which the given `MmArc` is initialized.
pub fn detect_priv_key_policy(ctx: &MmArc) -> MmResult<EthPrivKeyBuildPolicy, CryptoCtxError> {
let crypto_ctx = CryptoCtx::from_ctx(ctx)?;

match crypto_ctx.key_pair_policy() {
KeyPairPolicy::Iguana => {
// Use an internal private key as the coin secret.
let priv_key = crypto_ctx.mm2_internal_privkey_secret();
Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(priv_key))
},
KeyPairPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd.clone())),
}
}
}

impl TryFrom<PrivKeyBuildPolicy> for EthPrivKeyBuildPolicy {
type Error = PrivKeyPolicyNotAllowed;

/// Converts `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy`
/// taking into account that ETH doesn't support `Trezor` yet.
fn try_from(policy: PrivKeyBuildPolicy) -> Result<Self, Self::Error> {
match policy {
PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)),
PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)),
PrivKeyBuildPolicy::Trezor => Err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported),
}
}
}

/// An alternative to `crate::PrivKeyPolicy`, typical only for ETH coin.
#[derive(Clone)]
pub enum EthPrivKeyPolicy {
KeyPair(KeyPair),
#[cfg(target_arch = "wasm32")]
Metamask(MetamaskWeak),
}

impl From<KeyPair> for EthPrivKeyPolicy {
fn from(key_pair: KeyPair) -> Self { EthPrivKeyPolicy::KeyPair(key_pair) }
}

impl EthPrivKeyPolicy {
pub fn key_pair_or_err(&self) -> MmResult<&KeyPair, PrivKeyPolicyNotAllowed> {
match self {
EthPrivKeyPolicy::KeyPair(key_pair) => Ok(key_pair),
#[cfg(target_arch = "wasm32")]
EthPrivKeyPolicy::Metamask(_) => MmError::err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported),
}
}
}

/// pImpl idiom.
pub struct EthCoinImpl {
ticker: String,
coin_type: EthCoinType,
key_pair: KeyPair,
priv_key_policy: EthPrivKeyPolicy,
my_address: Address,
sign_message_prefix: Option<String>,
swap_contract_address: Address,
Expand Down Expand Up @@ -680,8 +745,10 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult {
gas_price,
};

let signed = tx.sign(coin.key_pair.secret(), coin.chain_id);
let secret = coin.priv_key_policy.key_pair_or_err()?.secret();
let signed = tx.sign(secret, coin.chain_id);
let bytes = rlp::encode(&signed);

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 == coin.my_address {
Expand Down Expand Up @@ -1100,7 +1167,13 @@ impl SwapOps for EthCoin {

#[inline]
fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> keys::KeyPair {
key_pair_from_secret(self.key_pair.secret()).expect("valid key")
#[allow(clippy::infallible_destructuring_match)]
let key_pair = match self.priv_key_policy {
EthPrivKeyPolicy::KeyPair(ref key_pair) => key_pair,
#[cfg(target_arch = "wasm32")]
EthPrivKeyPolicy::Metamask(_) => todo!(),
};
key_pair_from_secret(key_pair.secret()).expect("valid key")
}

fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> {
Expand Down Expand Up @@ -1181,7 +1254,15 @@ impl MarketCoinOps for EthCoin {
}

fn get_public_key(&self) -> Result<String, MmError<UnexpectedDerivationMethod>> {
let uncompressed_without_prefix = hex::encode(self.key_pair.public());
#[allow(clippy::infallible_destructuring_match)]
let key_pair = match self.priv_key_policy {
EthPrivKeyPolicy::KeyPair(ref key_pair) => key_pair,
// Return a default pubkey for a while.
// TODO return a pubkey extracted from `Login to AtomicDEX` signature.
#[cfg(target_arch = "wasm32")]
EthPrivKeyPolicy::Metamask(_) => return Ok("NOT SUPPORTED YET".to_string()),
};
let uncompressed_without_prefix = hex::encode(key_pair.public());
Ok(format!("04{}", uncompressed_without_prefix))
}

Expand All @@ -1201,7 +1282,7 @@ impl MarketCoinOps for EthCoin {

fn sign_message(&self, message: &str) -> SignatureResult<String> {
let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?;
let privkey = &self.key_pair.secret();
let privkey = &self.priv_key_policy.key_pair_or_err()?.secret();
let signature = sign(privkey, &H256::from(message_hash))?;
Ok(format!("0x{}", signature))
}
Expand Down Expand Up @@ -1459,7 +1540,13 @@ impl MarketCoinOps for EthCoin {
)
}

fn display_priv_key(&self) -> Result<String, String> { Ok(format!("{:#02x}", self.key_pair.secret())) }
fn display_priv_key(&self) -> Result<String, String> {
match self.priv_key_policy {
EthPrivKeyPolicy::KeyPair(ref key_pair) => Ok(format!("{:#02x}", key_pair.secret())),
#[cfg(target_arch = "wasm32")]
EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support MetaMask"),
}
}

fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) }

Expand Down Expand Up @@ -1514,7 +1601,8 @@ async fn sign_and_send_transaction_impl(
value,
data,
};
let signed = tx.sign(coin.key_pair.secret(), coin.chain_id);
let key_pair = try_tx_s!(coin.priv_key_policy.key_pair_or_err());
let signed = tx.sign(key_pair.secret(), coin.chain_id);
let bytes = web3::types::Bytes(rlp::encode(&signed).to_vec());
status.status(tags!(), "send_raw_transaction…");

Expand Down Expand Up @@ -3583,6 +3671,9 @@ pub async fn eth_coin_from_conf_and_request(
protocol: CoinProtocol,
priv_key_policy: PrivKeyBuildPolicy,
) -> Result<EthCoin, String> {
// Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible.
let priv_key_policy = try_s!(EthPrivKeyBuildPolicy::try_from(priv_key_policy));

let mut urls: Vec<String> = try_s!(json::from_value(req["urls"].clone()));
if urls.is_empty() {
return ERR!("Enable request for ETH coin must have at least 1 node URL");
Expand All @@ -3592,7 +3683,7 @@ pub async fn eth_coin_from_conf_and_request(

let mut nodes = vec![];
for url in urls.iter() {
nodes.push(Web3TransportNode {
nodes.push(HttpTransportNode {
uri: try_s!(url.parse()),
gui_auth: false,
});
Expand All @@ -3611,13 +3702,12 @@ pub async fn eth_coin_from_conf_and_request(
}
}

let key_pair = try_s!(key_pair_from_priv_key_policy(conf, priv_key_policy));
let my_address = key_pair.address();
let (my_address, key_pair) = try_s!(build_address_and_priv_key_policy(conf, priv_key_policy));

let mut web3_instances = vec![];
let event_handlers = rpc_event_handlers_for_eth_transport(ctx, ticker.to_string());
for node in nodes.iter() {
let transport = Web3Transport::with_event_handlers(vec![node.clone()], event_handlers.clone());
let transport = Web3Transport::new_http(vec![node.clone()], event_handlers.clone());
let web3 = Web3::new(transport);
let version = match web3.web3().client_version().compat().await {
Ok(v) => v,
Expand All @@ -3636,7 +3726,7 @@ pub async fn eth_coin_from_conf_and_request(
return ERR!("Failed to get client version for all urls");
}

let transport = Web3Transport::with_event_handlers(nodes, event_handlers);
let transport = Web3Transport::new_http(nodes, event_handlers);
let web3 = Web3::new(transport);

let (coin_type, decimals) = match protocol {
Expand Down Expand Up @@ -3695,7 +3785,7 @@ pub async fn eth_coin_from_conf_and_request(
let abortable_system = try_s!(ctx.abortable_system.create_subsystem());

let coin = EthCoinImpl {
key_pair,
priv_key_policy: key_pair,
my_address,
coin_type,
sign_message_prefix,
Expand Down
Loading

0 comments on commit 7ea1436

Please sign in to comment.