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

[r2r] MetaMask PoC #1551

Merged
merged 12 commits into from
Dec 5, 2022
21 changes: 21 additions & 0 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 @@ -27,6 +27,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
168 changes: 141 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,8 @@ 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::fmt;
use std::ops::Deref;
use std::path::PathBuf;
use std::str::FromStr;
Expand All @@ -57,30 +62,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 +299,94 @@ 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())),
}
}

pub fn is_metamask(&self) -> bool {
#[cfg(not(target_arch = "wasm32"))]
return false;

#[cfg(target_arch = "wasm32")]
matches!(self, EthPrivKeyBuildPolicy::Metamask(_))
}
}

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 fmt::Debug for EthPrivKeyPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EthPrivKeyPolicy::KeyPair(_) => write!(f, "KeyPair"),
#[cfg(target_arch = "wasm32")]
EthPrivKeyPolicy::Metamask(_) => write!(f, "Metamask"),
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't used. Should we keep it implemented?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, looks like it used to be used before. Will remove it, thanks

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


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

impl EthPrivKeyPolicy {
pub fn key_pair(&self) -> Option<&KeyPair> {
match self {
EthPrivKeyPolicy::KeyPair(key_pair) => Some(key_pair),
#[cfg(target_arch = "wasm32")]
EthPrivKeyPolicy::Metamask(_) => None,
}
}

pub fn key_pair_or_err(&self) -> MmResult<&KeyPair, PrivKeyPolicyNotAllowed> {
self.key_pair()
.or_mm_err(|| PrivKeyPolicyNotAllowed::HardwareWalletNotSupported)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn key_pair itself isn't used anywhere. What do you think returning MmResult directly in key_pair fn and remove key_pair_or_err?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I repeated the same methods from lp_coins::PrivKeyPolicy, so how about removing EthPrivKeyPolicy::key_pair method, but leaving EthPrivKeyPolicy::key_pair_or_err as it is? If renaming it to key_pair, it's also worth to rename PrivKeyPolicy::key_pair_or_err. But this will result in changes to files not related to this task.
What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about removing EthPrivKeyPolicy::key_pair method, but leaving EthPrivKeyPolicy::key_pair_or_err as it is?

I think it's also fine.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

/// 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 +769,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 +1191,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 +1278,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 +1306,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 +1564,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"),
}
onur-ozkan marked this conversation as resolved.
Show resolved Hide resolved
}

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

Expand Down Expand Up @@ -1514,7 +1625,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 +3695,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 +3707,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 +3726,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 +3750,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 +3809,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