diff --git a/README.md b/README.md index 1d3f1670c..3aeec8610 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,32 @@ webb-relayer -vv -c ./config - [`SubstrateConfig`](https://webb-tools.github.io/relayer/webb_relayer/config/struct.SubstrateConfig.html) - [`EvmChainConfig`](https://webb-tools.github.io/relayer/webb_relayer/config/struct.EvmChainConfig.html) +#### Relayer Common Configuration +| Field | Description | Optionality | +| -------------------------- | ------------------------------------------------------------------------------------------ | ----------- | +| `port` | Relayer port number | Required | +| `features` |Enable required features by setting them to `true` . All featured are enabled by default | Optional| +| `evm-etherscan` | Etherscan api configuration for chains, required if `private-tx` feature is enabled for relayer. | Optional | + +- `Features` Configuration + +``` +[features] +governance-relay = true +data-query = true +private-tx-relay = true +``` +- `Evm-etherscan` Configuration +``` +[evm-etherscan.goerli] +chain-id = 5 +api-key = "$ETHERSCAN_GOERLI_API_KEY" +[evm-etherscan.polygon] +chain-id = 137 +api-key = "$POLYGONSCAN_MAINNET_API_KEY" +``` + + #### Chain Configuration | Field | Description | Optionality | diff --git a/config/README.md b/config/README.md index 5c3e2c5e3..3752d5bad 100644 --- a/config/README.md +++ b/config/README.md @@ -12,6 +12,10 @@ following section we will describe the different configuration entries and how t - [governance-relay](#governance-relay) - [data-query](#data-query) - [private-tx-relay](#private-tx-relay) + - [evm-etherscan](#evm-etherscan) + - [chain-id](#chain-id) + - [api-key](#api-key) + - [EVM Chain Configuration](#evm-chain-configuration) - [name](#name) - [chain-id](#chain-id) @@ -168,6 +172,24 @@ Example: [features] private-tx-relay = true ``` +#### evm-etherscan +Etherscan api configuration for chains. This config is required if [private-tx-relay](#private-tx-relay) is enabled. +example: +```toml +[evm-etherscan.goerli] +chain-id = 5 +api-key = "$ETHERSCAN_GOERLI_API_KEY" +[evm-etherscan.polygon] +chain-id = 137 +api-key = "$POLYGONSCAN_MAINNET_API_KEY" +``` + +#### api-key +Etherscan api key, this are used to fetch gas prices from the explorer. +example: +```toml +api-key = $ETHERSCAN_GOERLI_API_KEY +``` ### EVM Chain Configuration diff --git a/config/development/.env.example b/config/development/.env.example index 08db790de..53c89675a 100644 --- a/config/development/.env.example +++ b/config/development/.env.example @@ -5,3 +5,4 @@ ATHENA_PRIVATE_KEY=${PRIVATE_KEY} DEMETER_PRIVATE_KEY=${PRIVATE_KEY} GOVERNOR_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000001 +ETHERSCAN_API_KEY=YI7KXQGB98XXXXZJ5595E6Q7ZH2PGU32BG7 \ No newline at end of file diff --git a/config/development/evm-blanknet/main.toml b/config/development/evm-blanknet/main.toml index 1e2e1792b..4fc8de9aa 100644 --- a/config/development/evm-blanknet/main.toml +++ b/config/development/evm-blanknet/main.toml @@ -3,3 +3,7 @@ port = 9955 [experimental] smart-anchor-updates = false smart-anchor-updates-retries = 3 + +[evm-etherscan.mainnet] +chain-id = 5001 +api-key = "$ETHERSCAN_API_KEY" \ No newline at end of file diff --git a/config/development/evm-local-tangle/main.toml b/config/development/evm-local-tangle/main.toml new file mode 100644 index 000000000..4fc8de9aa --- /dev/null +++ b/config/development/evm-local-tangle/main.toml @@ -0,0 +1,9 @@ +port = 9955 + +[experimental] +smart-anchor-updates = false +smart-anchor-updates-retries = 3 + +[evm-etherscan.mainnet] +chain-id = 5001 +api-key = "$ETHERSCAN_API_KEY" \ No newline at end of file diff --git a/config/exclusive-strategies/.env.example b/config/exclusive-strategies/.env.example index 695268806..d819995aa 100644 --- a/config/exclusive-strategies/.env.example +++ b/config/exclusive-strategies/.env.example @@ -15,5 +15,5 @@ OPTIMISM_TESTNET_WSS_URL=${EXAMPLE_WSS_URL} OPTIMISM_TESTNET_PRIVATE_KEY=${EXAMPLE_PRIVATE_KEY} - +ETHERSCAN_API_KEY=YI7KXQGB98XXXXZJ5595E6Q7ZH2PGU32BG7 MOCKED_BACKEND_KEY=${EXAMPLE_PRIVATE_KEY} diff --git a/config/exclusive-strategies/private-tx-relaying/main.toml b/config/exclusive-strategies/private-tx-relaying/main.toml index 2282f25ce..803f2bb3b 100644 --- a/config/exclusive-strategies/private-tx-relaying/main.toml +++ b/config/exclusive-strategies/private-tx-relaying/main.toml @@ -5,4 +5,11 @@ port = 9955 [features] governance-relay = false data-query = false -private-tx-relay = true \ No newline at end of file +private-tx-relay = true + +[evm-etherscan.goerli] +chain-id = 5 +api-key = "$ETHERSCAN_API_KEY" +[evm-etherscan.sepolia] +chain-id = 11155111 +api-key = "$ETHERSCAN_API_KEY" \ No newline at end of file diff --git a/crates/relayer-config/src/lib.rs b/crates/relayer-config/src/lib.rs index fe231ccc7..2b48f879f 100644 --- a/crates/relayer-config/src/lib.rs +++ b/crates/relayer-config/src/lib.rs @@ -49,6 +49,8 @@ use evm::EvmChainConfig; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use substrate::SubstrateConfig; +use webb::evm::ethers::types::Chain; +use webb_relayer_types::etherscan_api::EtherscanApiKey; /// The default port the relayer will listen on. Defaults to 9955. const fn default_port() -> u16 { @@ -80,6 +82,9 @@ pub struct WebbRelayerConfig { /// default to 9955 #[serde(default = "default_port", skip_serializing)] pub port: u16, + /// Etherscan API key configuration for evm based chains. + #[serde(default)] + pub evm_etherscan: HashMap, /// EVM based networks and the configuration. /// /// a map between chain name and its configuration. @@ -174,6 +179,16 @@ impl Default for FeaturesConfig { } } +/// Configuration to add etherscan API key +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct EtherscanApiConfig { + /// Chain Id + pub chain_id: u32, + /// A wrapper type around the `String` to allow reading it from the env. + pub api_key: EtherscanApiKey, +} + /// TxQueueConfig is the configuration for the TxQueue. #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] diff --git a/crates/relayer-context/src/lib.rs b/crates/relayer-context/src/lib.rs index 233aa05ca..64dc3bb7e 100644 --- a/crates/relayer-context/src/lib.rs +++ b/crates/relayer-context/src/lib.rs @@ -16,9 +16,9 @@ //! # Relayer Context Module 🕸️ //! //! A module for managing the context of the relayer. -use std::convert::TryFrom; use std::sync::Arc; use std::time::Duration; +use std::{collections::HashMap, convert::TryFrom}; use tokio::sync::{broadcast, Mutex}; @@ -55,8 +55,8 @@ pub struct RelayerContext { store: SledStore, /// API client for https://www.coingecko.com/ coin_gecko_client: Arc, - /// API client for https://etherscan.io/ - etherscan_client: Client, + /// Hashmap of + etherscan_clients: HashMap, } impl RelayerContext { @@ -68,14 +68,20 @@ impl RelayerContext { let (notify_shutdown, _) = broadcast::channel(2); let metrics = Arc::new(Mutex::new(Metrics::new())); let coin_gecko_client = Arc::new(CoinGeckoClient::default()); - let etherscan_client = Client::new_from_env(Chain::Mainnet).unwrap(); + let mut etherscan_clients: HashMap = HashMap::new(); + for (chain, etherscan_config) in config.evm_etherscan.iter() { + let client = + Client::new(*chain, etherscan_config.api_key.to_string()) + .unwrap(); + etherscan_clients.insert(etherscan_config.chain_id, client); + } Self { config, notify_shutdown, metrics, store, coin_gecko_client, - etherscan_client, + etherscan_clients, } } /// Returns a broadcast receiver handle for the shutdown signal. @@ -187,8 +193,17 @@ impl RelayerContext { } /// Returns API client for https://etherscan.io/ - pub fn etherscan_client(&self) -> &Client { - &self.etherscan_client + pub fn etherscan_client( + &self, + chain_id: u32, + ) -> webb_relayer_utils::Result<&Client> { + let client = + self.etherscan_clients.get(&chain_id).ok_or_else(|| { + webb_relayer_utils::Error::EtherscanConfigNotFound { + chain_id: chain_id.to_string(), + } + })?; + Ok(client) } } diff --git a/crates/relayer-types/src/etherscan_api.rs b/crates/relayer-types/src/etherscan_api.rs new file mode 100644 index 000000000..2f2973248 --- /dev/null +++ b/crates/relayer-types/src/etherscan_api.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; +#[derive(Clone, Serialize)] +pub struct EtherscanApiKey(String); + +impl std::fmt::Debug for EtherscanApiKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("EtherscanApiKey").finish() + } +} + +impl From for EtherscanApiKey { + fn from(api_key: String) -> Self { + EtherscanApiKey(api_key) + } +} + +impl std::ops::Deref for EtherscanApiKey { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'de> Deserialize<'de> for EtherscanApiKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct EtherscanApiKeyVisitor; + impl<'de> serde::de::Visitor<'de> for EtherscanApiKeyVisitor { + type Value = String; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + formatter.write_str( + "Etherscan api key or an env var containing a etherscan api key in it", + ) + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if value.starts_with('$') { + // env + let var = value.strip_prefix('$').unwrap_or(value); + tracing::trace!("Reading {} from env", var); + let val = std::env::var(var).map_err(|e| { + serde::de::Error::custom(format!( + "error while loading this env {var}: {e}", + )) + })?; + return Ok(val); + } + Ok(value.to_string()) + } + } + + let etherscan_api_key = + deserializer.deserialize_str(EtherscanApiKeyVisitor)?; + Ok(Self(etherscan_api_key)) + } +} diff --git a/crates/relayer-types/src/lib.rs b/crates/relayer-types/src/lib.rs index 7f85bee5c..cd07a09c8 100644 --- a/crates/relayer-types/src/lib.rs +++ b/crates/relayer-types/src/lib.rs @@ -1,4 +1,5 @@ pub mod dynamic_payload; +pub mod etherscan_api; pub mod mnemonic; pub mod private_key; pub mod rpc_url; diff --git a/crates/relayer-utils/src/lib.rs b/crates/relayer-utils/src/lib.rs index 38cc09e6f..00931b481 100644 --- a/crates/relayer-utils/src/lib.rs +++ b/crates/relayer-utils/src/lib.rs @@ -151,6 +151,12 @@ pub enum Error { /// Arkworks Errors. #[error("{}", _0)] ArkworksError(String), + /// Etherscan api configuration not found. + #[error("Etherscan api configuration not found for chain : {}", chain_id)] + EtherscanConfigNotFound { + /// The chain id of the node. + chain_id: String, + }, #[error("No bridge registered with DKG for resource id {:?}", _0)] BridgeNotRegistered(ResourceId), } diff --git a/crates/tx-relay/src/evm/fees.rs b/crates/tx-relay/src/evm/fees.rs index eb3420140..efa74ebad 100644 --- a/crates/tx-relay/src/evm/fees.rs +++ b/crates/tx-relay/src/evm/fees.rs @@ -134,7 +134,10 @@ async fn generate_fee_info( let wrapped_token_price = prices[&wrapped_token.0].usd.unwrap(); // Fetch native gas price estimate from etherscan.io, using "average" value - let gas_oracle = ctx.etherscan_client().gas_oracle().await?; + let gas_oracle = ctx + .etherscan_client(chain_id.underlying_chain_id())? + .gas_oracle() + .await?; let gas_price_gwei = U256::from(gas_oracle.propose_gas_price); let gas_price = parse_units(gas_price_gwei, "gwei")?.into(); diff --git a/tests/lib/webbRelayer.ts b/tests/lib/webbRelayer.ts index a09572e20..c5c374e9b 100644 --- a/tests/lib/webbRelayer.ts +++ b/tests/lib/webbRelayer.ts @@ -31,6 +31,7 @@ import { padHexString } from '../lib/utils.js'; export type CommonConfig = { features?: FeaturesConfig; + evmEtherscan?: EvmEtherscanConfig; port: number; }; @@ -76,6 +77,9 @@ export class WebbRelayer { // Write the folder-wide configuration for this relayer instance type WrittenCommonConfig = { features?: ConvertToKebabCase; + 'evm-etherscan'?: { + [key: string]: ConvertToKebabCase; + }; port: number; }; const commonConfigFile: WrittenCommonConfig = { @@ -84,6 +88,14 @@ export class WebbRelayer { 'governance-relay': opts.commonConfig.features?.governanceRelay ?? true, 'private-tx-relay': opts.commonConfig.features?.privateTxRelay ?? true, }, + 'evm-etherscan': Object.fromEntries( + Object.entries(opts.commonConfig.evmEtherscan ?? {}).map( + ([key, { chainId, apiKey }]) => [ + key, + { ...{ 'chain-id': chainId, 'api-key': apiKey } }, + ] + ) + ), port: opts.commonConfig.port, }; const configString = JSON.stringify(commonConfigFile, null, 2); @@ -590,6 +602,16 @@ export interface FeaturesConfig { governanceRelay?: boolean; privateTxRelay?: boolean; } + +export interface EtherscanApiConfig { + chainId: number; + apiKey: string; +} + +export interface EvmEtherscanConfig { + [key: string]: EtherscanApiConfig; +} + export interface WithdrawConfig { withdrawFeePercentage: number; withdrawGaslimit: `0x${string}`; diff --git a/tests/test/evm/RelayerTxTransfer.test.ts b/tests/test/evm/RelayerTxTransfer.test.ts index 273bb540c..4c2ad6455 100644 --- a/tests/test/evm/RelayerTxTransfer.test.ts +++ b/tests/test/evm/RelayerTxTransfer.test.ts @@ -26,13 +26,14 @@ import { toFixedHex, Utxo, } from '@webb-tools/sdk-core'; - +import dotenv from 'dotenv'; import { BigNumber, ethers } from 'ethers'; import temp from 'temp'; import { LocalChain } from '../../lib/localTestnet.js'; import { defaultWithdrawConfigValue, EnabledContracts, + EvmEtherscanConfig, FeeInfo, WebbRelayer, } from '../../lib/webbRelayer.js'; @@ -41,6 +42,7 @@ import { u8aToHex, hexToU8a } from '@polkadot/util'; import { MintableToken } from '@webb-tools/tokens'; import { formatEther, parseEther } from 'ethers/lib/utils.js'; +dotenv.config({ path: '../.env' }); // This test is meant to prove that utxo transfer flows are possible, and the receiver // can query on-chain data to construct and spend a utxo generated by the sender. describe.skip('Relayer transfer assets', function () { @@ -150,6 +152,13 @@ describe.skip('Relayer transfer assets', function () { // now start the relayer const relayerPort = await getPort({ port: portNumbers(9955, 9999) }); + const evmEtherscan: EvmEtherscanConfig = { + ['mainnet']: { + chainId: localChain1.underlyingChainId, + apiKey: process.env.ETHERSCAN_API_KEY!, + }, + }; + webbRelayer = new WebbRelayer({ commonConfig: { features: { dataQuery: false, governanceRelay: false }, diff --git a/tests/test/evm/vanchorPrivateTransaction.test.ts b/tests/test/evm/vanchorPrivateTransaction.test.ts index ef8ae7e4f..2cfa163e9 100644 --- a/tests/test/evm/vanchorPrivateTransaction.test.ts +++ b/tests/test/evm/vanchorPrivateTransaction.test.ts @@ -19,14 +19,19 @@ import { expect } from 'chai'; import { Tokens, VBridge } from '@webb-tools/protocol-solidity'; -import { CircomUtxo, Keypair, parseTypedChainId } from '@webb-tools/sdk-core'; - +import { + CircomUtxo, + Keypair, + parseTypedChainId, +} from '@webb-tools/sdk-core'; +import dotenv from 'dotenv'; import { BigNumber, ethers } from 'ethers'; import temp from 'temp'; import { LocalChain, setupVanchorEvmTx } from '../../lib/localTestnet.js'; import { defaultWithdrawConfigValue, EnabledContracts, + EvmEtherscanConfig, FeeInfo, ResourceMetricResponse, WebbRelayer, @@ -36,6 +41,8 @@ import { u8aToHex, hexToU8a } from '@polkadot/util'; import { MintableToken } from '@webb-tools/tokens'; import { formatEther, parseEther } from 'ethers/lib/utils.js'; +dotenv.config({ path: '../.env' }); + describe('Vanchor Private Tx relaying with mocked governor', function () { const tmpDirPath = temp.mkdirSync(); let localChain1: LocalChain; @@ -213,9 +220,16 @@ describe('Vanchor Private Tx relaying with mocked governor', function () { // now start the relayer const relayerPort = await getPort({ port: portNumbers(9955, 9999) }); + const evmEtherscan: EvmEtherscanConfig = { + ['mainnet']: { + chainId: localChain2.underlyingChainId, + apiKey: process.env.ETHERSCAN_API_KEY!, + }, + }; webbRelayer = new WebbRelayer({ commonConfig: { features: { dataQuery: false, governanceRelay: false }, + evmEtherscan, port: relayerPort, }, tmp: true,