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(tokens): custom token activation for evm #2141

Open
wants to merge 40 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c7638a5
custom token activation as wallet only for evm and tendermint
shamardy Jun 12, 2024
228b862
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Aug 2, 2024
7e95c7f
fix typo in ProtocolMissMatch
shamardy Aug 2, 2024
8c489cf
review fix: use `ok_or_else` instead of matching `protocol_from_request`
shamardy Aug 2, 2024
3038aaa
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Aug 2, 2024
2ae59f0
Remove getting `coin_conf` inside `initialize_erc20_token` and pass a…
shamardy Aug 2, 2024
b0bdc82
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Aug 5, 2024
126570a
wip: make custom token work for EVM only and add some validation steps
shamardy Oct 7, 2024
37b4531
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Oct 7, 2024
545b87e
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Oct 13, 2024
8238ff8
default `required_confirmations` should be from platform coin
shamardy Oct 14, 2024
12a9461
wip: add `get_custom_token_info` RPC
shamardy Oct 18, 2024
c888f8a
use `CoinProtocol` in `CustomTokenInfoRequest`
shamardy Oct 25, 2024
245c106
remove some todos that are not relevant anymore to the PR
shamardy Oct 25, 2024
b7c37ee
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Oct 25, 2024
38a010e
Enhance `get_custom_token_info` with config lookup + move some erc20 …
shamardy Oct 28, 2024
5fb0bdd
Rename `EnableTokenError::TokenIsAlreadyActivated` to `TickerAlreadyI…
shamardy Oct 28, 2024
f676212
move custom_token.rs to lp_commands
shamardy Oct 28, 2024
f05b672
Add test for enabling and disabling custom erc20 tokens
shamardy Oct 29, 2024
eef94b2
Make a coin not in config as wallet only and add test for it
shamardy Oct 29, 2024
c62f4a7
rename `test_enable_disable_custom_erc20` to `test_custom_erc20` and …
shamardy Oct 29, 2024
551b6a7
Add test to verify error handling for enabling a custom token with a …
shamardy Oct 29, 2024
a8396b0
Add test coverage for `get_custom_token_info` RPC if the same contrac…
shamardy Oct 29, 2024
d2a678f
Add test coverage for enabling a custom token with a contract that ha…
shamardy Oct 30, 2024
05949db
move new custom erc20 tests to eth_docker_tests.rs
shamardy Oct 30, 2024
b7648e7
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Oct 30, 2024
c6a4ea8
review fix: remove unused TokenAlreadyActivated error variants
shamardy Nov 4, 2024
a20c3f0
review fixes: rename `test_custom_erc20` to `test_enable_custom_erc20`
shamardy Nov 4, 2024
c721a50
review fixes: move coin conf inside `TokenActivationParams`
shamardy Nov 4, 2024
bb2146f
review fixes: refactor `coin_conf_with_protocol`
shamardy Nov 4, 2024
1c07703
review fixes: rename `get_custom_token_info` to `get_token_info` alon…
shamardy Nov 4, 2024
d647242
- fix `coin_conf_with_protocol`
shamardy Nov 5, 2024
e9b6dc7
review fix: remove `is_custom` from requests
shamardy Nov 5, 2024
a362e5c
review fixes: remove redundant `PartialEq`s from `CoinProtocol` neste…
shamardy Nov 11, 2024
2ff5448
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Nov 11, 2024
36e28b5
review fix: add display to all variants of `CustomTokenError`
shamardy Nov 11, 2024
84847ed
review fix: rename `Erc20TokenBasicInfo` to `Erc20TokenInfo`
shamardy Nov 11, 2024
f38fc3f
review fix: avoid direct access of index `0` in `get_token_decimals` …
shamardy Nov 13, 2024
8b11a6c
review fix: minor refactors
shamardy Nov 14, 2024
3b94972
Merge remote-tracking branch 'origin/dev' into feat-custom-evm-token
shamardy Nov 14, 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
37 changes: 7 additions & 30 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ pub(crate) use eip1559_gas_fee::FeePerGasEstimated;
use eip1559_gas_fee::{BlocknativeGasApiCaller, FeePerGasSimpleEstimator, GasApiConfig, GasApiProvider,
InfuraGasApiCaller};

pub mod erc20;
use erc20::get_token_decimals;

pub(crate) mod eth_swap_v2;
use eth_swap_v2::{EthPaymentType, PaymentMethod};

Expand Down Expand Up @@ -883,7 +886,7 @@ pub struct EthCoinImpl {
/// and unlocked once the transaction is confirmed. This prevents nonce conflicts when multiple transactions
/// are initiated concurrently from the same address.
address_nonce_locks: Arc<AsyncMutex<HashMap<String, Arc<AsyncMutex<()>>>>>,
erc20_tokens_infos: Arc<Mutex<HashMap<String, Erc20TokenInfo>>>,
erc20_tokens_infos: Arc<Mutex<HashMap<String, Erc20TokenDetails>>>,
/// Stores information about NFTs owned by the user. Each entry in the HashMap is uniquely identified by a composite key
/// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets
/// information (chain & contract type, amount etc.), where ownership and amount, in ERC1155 case, might change over time.
Expand All @@ -907,7 +910,7 @@ pub struct Web3Instance {

/// Information about a token that follows the ERC20 protocol on an EVM-based network.
#[derive(Clone, Debug)]
pub struct Erc20TokenInfo {
pub struct Erc20TokenDetails {
/// The contract address of the token on the EVM-based network.
pub token_address: Address,
/// The number of decimal places the token uses.
Expand Down Expand Up @@ -1068,14 +1071,14 @@ impl EthCoinImpl {
}
}

pub fn add_erc_token_info(&self, ticker: String, info: Erc20TokenInfo) {
pub fn add_erc_token_info(&self, ticker: String, info: Erc20TokenDetails) {
self.erc20_tokens_infos.lock().unwrap().insert(ticker, info);
}

/// # Warning
/// Be very careful using this function since it returns dereferenced clone
/// of value behind the MutexGuard and makes it non-thread-safe.
pub fn get_erc_tokens_infos(&self) -> HashMap<String, Erc20TokenInfo> {
pub fn get_erc_tokens_infos(&self) -> HashMap<String, Erc20TokenDetails> {
let guard = self.erc20_tokens_infos.lock().unwrap();
(*guard).clone()
}
Expand Down Expand Up @@ -6318,32 +6321,6 @@ fn signed_tx_from_web3_tx(transaction: Web3Transaction) -> Result<SignedEthTx, S
Ok(try_s!(SignedEthTx::new(unverified)))
}

async fn get_token_decimals(web3: &Web3<Web3Transport>, token_addr: Address) -> Result<u8, String> {
let function = try_s!(ERC20_CONTRACT.function("decimals"));
let data = try_s!(function.encode_input(&[]));
let request = CallRequest {
from: Some(Address::default()),
to: Some(token_addr),
gas: None,
gas_price: None,
value: Some(0.into()),
data: Some(data.into()),
..CallRequest::default()
};

let res = web3
.eth()
.call(request, Some(BlockId::Number(BlockNumber::Latest)))
.map_err(|e| ERRL!("{}", e))
.await?;
let tokens = try_s!(function.decode_output(&res.0));
let decimals = match tokens[0] {
Token::Uint(dec) => dec.as_u64(),
_ => return ERR!("Invalid decimals type {:?}", tokens),
};
Ok(decimals as u8)
}

pub fn valid_addr_from_str(addr_str: &str) -> Result<Address, String> {
let addr = try_s!(addr_from_str(addr_str));
if !is_valid_checksum_addr(addr_str) {
Expand Down
107 changes: 107 additions & 0 deletions mm2src/coins/eth/erc20.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use crate::eth::web3_transport::Web3Transport;
use crate::eth::{EthCoin, ERC20_CONTRACT};
use crate::{CoinsContext, MmCoinEnum};
use ethabi::Token;
use ethereum_types::Address;
use futures_util::TryFutureExt;
use mm2_core::mm_ctx::MmArc;
use mm2_err_handle::mm_error::MmResult;
use web3::types::{BlockId, BlockNumber, CallRequest};
use web3::{Transport, Web3};

async fn call_erc20_function<T: Transport>(
web3: &Web3<T>,
token_addr: Address,
function_name: &str,
) -> Result<Vec<Token>, String> {
let function = try_s!(ERC20_CONTRACT.function(function_name));
let data = try_s!(function.encode_input(&[]));
let request = CallRequest {
from: Some(Address::default()),
to: Some(token_addr),
gas: None,
gas_price: None,
value: Some(0.into()),
data: Some(data.into()),
..CallRequest::default()
};

let res = web3
.eth()
.call(request, Some(BlockId::Number(BlockNumber::Latest)))
.map_err(|e| ERRL!("{}", e))
.await?;
function.decode_output(&res.0).map_err(|e| ERRL!("{}", e))
}

pub(crate) async fn get_token_decimals(web3: &Web3<Web3Transport>, token_addr: Address) -> Result<u8, String> {
let tokens = call_erc20_function(web3, token_addr, "decimals").await?;
let Some(token) = tokens.into_iter().next() else {
return ERR!("No value returned from decimals() call");
};
let Token::Uint(dec) = token else {
return ERR!("Expected Uint token for decimals, got {:?}", token);
};
Ok(dec.as_u64() as u8)
}

async fn get_token_symbol(coin: &EthCoin, token_addr: Address) -> Result<String, String> {
let web3 = try_s!(coin.web3().await);
let tokens = call_erc20_function(&web3, token_addr, "symbol").await?;
let Some(token) = tokens.into_iter().next() else {
return ERR!("No value returned from symbol() call");
};
let Token::String(symbol) = token else {
return ERR!("Expected String token for symbol, got {:?}", token);
};
Ok(symbol)
}

#[derive(Serialize)]
pub struct Erc20TokenInfo {
pub symbol: String,
pub decimals: u8,
}

pub async fn get_erc20_token_info(coin: &EthCoin, token_addr: Address) -> Result<Erc20TokenInfo, String> {
let symbol = get_token_symbol(coin, token_addr).await?;
let web3 = try_s!(coin.web3().await);
let decimals = get_token_decimals(&web3, token_addr).await?;
Ok(Erc20TokenInfo { symbol, decimals })
}

/// Finds if an ERC20 token is in coins config by its contract address and returns its ticker.
pub fn get_erc20_ticker_by_contract_address(ctx: &MmArc, platform: &str, contract_address: &str) -> Option<String> {
ctx.conf["coins"].as_array()?.iter().find_map(|coin| {
let protocol = coin.get("protocol")?;
let protocol_type = protocol.get("type")?.as_str()?;
if protocol_type != "ERC20" {
return None;
}
let protocol_data = protocol.get("protocol_data")?;
let coin_platform = protocol_data.get("platform")?.as_str()?;
let coin_contract_address = protocol_data.get("contract_address")?.as_str()?;

if coin_platform == platform && coin_contract_address == contract_address {
coin.get("coin")?.as_str().map(|s| s.to_string())
} else {
None
}
})
}

/// Finds an enabled ERC20 token by its contract address and returns it as `MmCoinEnum`.
pub async fn get_enabled_erc20_by_contract(
ctx: &MmArc,
contract_address: Address,
) -> MmResult<Option<MmCoinEnum>, String> {
let cctx = CoinsContext::from_ctx(ctx)?;
let coins = cctx.coins.lock().await;

Ok(coins.values().find_map(|coin| match &coin.inner {
MmCoinEnum::EthCoin(eth_coin) if eth_coin.erc20_token_address() == Some(contract_address) => {
Some(coin.inner.clone())
},
_ => None,
}))
}
8 changes: 4 additions & 4 deletions mm2src/coins/eth/eth_balance_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use mm2_number::BigDecimal;
use std::collections::{HashMap, HashSet};

use super::EthCoin;
use crate::{eth::{u256_to_big_decimal, Erc20TokenInfo},
use crate::{eth::{u256_to_big_decimal, Erc20TokenDetails},
BalanceError, CoinWithDerivationMethod, MmCoin};

struct BalanceData {
Expand All @@ -40,9 +40,9 @@ async fn get_all_balance_results_concurrently(coin: &EthCoin, addresses: HashSet
//
// Unlike tokens, the platform coin length is constant (=1). Instead of creating a generic
// type and mapping the platform coin and the entire token list (which can grow at any time), we map
// the platform coin to Erc20TokenInfo so that we can use the token list right away without
// the platform coin to Erc20TokenDetails so that we can use the token list right away without
// additional mapping.
tokens.insert(coin.ticker.clone(), Erc20TokenInfo {
tokens.insert(coin.ticker.clone(), Erc20TokenDetails {
// This is a dummy value, since there is no token address for the platform coin.
// In the fetch_balance function, we check if the token_ticker is equal to this
// coin's ticker to avoid using token_address to fetch the balance
Expand Down Expand Up @@ -72,7 +72,7 @@ async fn fetch_balance(
coin: &EthCoin,
address: Address,
token_ticker: String,
info: &Erc20TokenInfo,
info: &Erc20TokenDetails,
) -> Result<BalanceData, BalanceFetchError> {
let (balance_as_u256, decimals) = if token_ticker == coin.ticker {
(
Expand Down
42 changes: 34 additions & 8 deletions mm2src/coins/eth/v2_activation.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use crate::eth::erc20::{get_enabled_erc20_by_contract, get_token_decimals};
use crate::eth::web3_transport::http_transport::HttpTransport;
use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage,
HDWalletStorageError, DEFAULT_GAP_LIMIT};
Expand Down Expand Up @@ -62,6 +63,8 @@ pub enum EthActivationV2Error {
HwError(HwRpcError),
#[display(fmt = "Hardware wallet must be called within rpc task framework")]
InvalidHardwareWalletCall,
#[display(fmt = "Custom token error: {}", _0)]
CustomTokenError(CustomTokenError),
}

impl From<MyAddressError> for EthActivationV2Error {
Expand Down Expand Up @@ -93,6 +96,7 @@ impl From<EthTokenActivationError> for EthActivationV2Error {
EthActivationV2Error::UnexpectedDerivationMethod(err)
},
EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => EthActivationV2Error::PrivKeyPolicyNotAllowed(e),
EthTokenActivationError::CustomTokenError(e) => EthActivationV2Error::CustomTokenError(e),
}
}
}
Expand Down Expand Up @@ -211,6 +215,7 @@ pub enum EthTokenActivationError {
Transport(String),
UnexpectedDerivationMethod(UnexpectedDerivationMethod),
PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed),
CustomTokenError(CustomTokenError),
}

impl From<AbortedError> for EthTokenActivationError {
Expand Down Expand Up @@ -376,9 +381,11 @@ pub struct NftProtocol {
impl EthCoin {
pub async fn initialize_erc20_token(
&self,
ticker: String,
activation_params: Erc20TokenActivationRequest,
token_conf: Json,
protocol: Erc20Protocol,
ticker: String,
is_custom: bool,
) -> MmResult<EthCoin, EthTokenActivationError> {
// TODO
// Check if ctx is required.
Expand All @@ -387,9 +394,24 @@ impl EthCoin {
.ok_or_else(|| String::from("No context"))
.map_err(EthTokenActivationError::InternalError)?;

let conf = coin_conf(&ctx, &ticker);
// Todo: when custom token config storage is added, this might not be needed
// `is_custom` was added to avoid this unnecessary check for non-custom tokens
if is_custom {
match get_enabled_erc20_by_contract(&ctx, protocol.token_addr).await {
Ok(Some(token)) => {
return MmError::err(EthTokenActivationError::CustomTokenError(
CustomTokenError::TokenWithSameContractAlreadyActivated {
ticker: token.ticker().to_string(),
contract_address: display_eth_address(&protocol.token_addr),
},
));
},
Ok(None) => {},
Err(e) => return MmError::err(EthTokenActivationError::InternalError(e.to_string())),
}
}

let decimals = match conf["decimals"].as_u64() {
let decimals = match token_conf["decimals"].as_u64() {
None | Some(0) => get_token_decimals(
&self
.web3()
Expand All @@ -404,7 +426,11 @@ impl EthCoin {

let required_confirmations = activation_params
.required_confirmations
.unwrap_or_else(|| conf["required_confirmations"].as_u64().unwrap_or(1))
.unwrap_or_else(|| {
token_conf["required_confirmations"]
.as_u64()
.unwrap_or(self.required_confirmations())
})
.into();

// Create an abortable system linked to the `MmCtx` so if the app is stopped on `MmArc::stop`,
Expand All @@ -415,11 +441,11 @@ impl EthCoin {
platform: protocol.platform,
token_addr: protocol.token_addr,
};
let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?;
let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?;
let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&conf)
let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &token_conf, &coin_type).await?;
let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &token_conf, &coin_type).await?;
let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&token_conf)
.map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?;
let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&conf)
let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&token_conf)
.map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?;

let token = EthCoinImpl {
Expand Down
Loading
Loading