2319 lines
84 KiB
Rust
2319 lines
84 KiB
Rust
use crate::{
|
|
call_raw::CallBuilder,
|
|
ens, erc, maybe,
|
|
pubsub::{PubsubClient, SubscriptionStream},
|
|
stream::{FilterWatcher, DEFAULT_LOCAL_POLL_INTERVAL, DEFAULT_POLL_INTERVAL},
|
|
FromErr, Http as HttpProvider, JsonRpcClient, JsonRpcClientWrapper, LogQuery, MockProvider,
|
|
NodeInfo, PeerInfo, PendingTransaction, QuorumProvider, RwClient, SyncingStatus,
|
|
};
|
|
|
|
#[cfg(all(not(target_arch = "wasm32"), feature = "ws"))]
|
|
use crate::transports::Authorization;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use crate::transports::{HttpRateLimitRetryPolicy, RetryClient};
|
|
|
|
#[cfg(feature = "celo")]
|
|
use crate::CeloMiddleware;
|
|
use crate::Middleware;
|
|
use async_trait::async_trait;
|
|
|
|
use ethers_core::{
|
|
abi::{self, Detokenize, ParamType},
|
|
types::{
|
|
transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
|
|
Address, Block, BlockId, BlockNumber, BlockTrace, Bytes, EIP1186ProofResponse, FeeHistory,
|
|
Filter, FilterBlockOption, GethDebugTracingOptions, GethTrace, Log, NameOrAddress,
|
|
Selector, Signature, Trace, TraceFilter, TraceType, Transaction, TransactionReceipt,
|
|
TransactionRequest, TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus, H256, U256, U64,
|
|
},
|
|
utils,
|
|
};
|
|
use hex::FromHex;
|
|
use serde::{de::DeserializeOwned, Serialize};
|
|
use thiserror::Error;
|
|
use url::{ParseError, Url};
|
|
|
|
use ethers_core::types::Chain;
|
|
use futures_util::{lock::Mutex, try_join};
|
|
use std::{
|
|
collections::VecDeque, convert::TryFrom, fmt::Debug, str::FromStr, sync::Arc, time::Duration,
|
|
};
|
|
use tracing::trace;
|
|
use tracing_futures::Instrument;
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub enum NodeClient {
|
|
Geth,
|
|
Erigon,
|
|
OpenEthereum,
|
|
Nethermind,
|
|
Besu,
|
|
}
|
|
|
|
impl FromStr for NodeClient {
|
|
type Err = ProviderError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s.split('/').next().unwrap().to_lowercase().as_str() {
|
|
"geth" => Ok(NodeClient::Geth),
|
|
"erigon" => Ok(NodeClient::Erigon),
|
|
"openethereum" => Ok(NodeClient::OpenEthereum),
|
|
"nethermind" => Ok(NodeClient::Nethermind),
|
|
"besu" => Ok(NodeClient::Besu),
|
|
_ => Err(ProviderError::UnsupportedNodeClient),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An abstract provider for interacting with the [Ethereum JSON RPC
|
|
/// API](https://github.com/ethereum/wiki/wiki/JSON-RPC). Must be instantiated
|
|
/// with a data transport which implements the [`JsonRpcClient`](trait@crate::JsonRpcClient) trait
|
|
/// (e.g. [HTTP](crate::Http), Websockets etc.)
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```no_run
|
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// use ethers_providers::{Middleware, Provider, Http};
|
|
/// use std::convert::TryFrom;
|
|
///
|
|
/// let provider = Provider::<Http>::try_from(
|
|
/// "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27"
|
|
/// ).expect("could not instantiate HTTP Provider");
|
|
///
|
|
/// let block = provider.get_block(100u64).await?;
|
|
/// println!("Got block: {}", serde_json::to_string(&block)?);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[derive(Clone, Debug)]
|
|
pub struct Provider<P> {
|
|
inner: P,
|
|
ens: Option<Address>,
|
|
interval: Option<Duration>,
|
|
from: Option<Address>,
|
|
/// Node client hasn't been checked yet = `None`
|
|
/// Unsupported node client = `Some(None)`
|
|
/// Supported node client = `Some(Some(NodeClient))`
|
|
_node_client: Arc<Mutex<Option<NodeClient>>>,
|
|
}
|
|
|
|
impl<P> AsRef<P> for Provider<P> {
|
|
fn as_ref(&self) -> &P {
|
|
&self.inner
|
|
}
|
|
}
|
|
|
|
impl FromErr<ProviderError> for ProviderError {
|
|
fn from(src: ProviderError) -> Self {
|
|
src
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
/// An error thrown when making a call to the provider
|
|
pub enum ProviderError {
|
|
/// An internal error in the JSON RPC Client
|
|
#[error(transparent)]
|
|
JsonRpcClientError(#[from] Box<dyn std::error::Error + Send + Sync>),
|
|
|
|
/// An error during ENS name resolution
|
|
#[error("ens name not found: {0}")]
|
|
EnsError(String),
|
|
|
|
/// Invalid reverse ENS name
|
|
#[error("reverse ens name not pointing to itself: {0}")]
|
|
EnsNotOwned(String),
|
|
|
|
#[error(transparent)]
|
|
SerdeJson(#[from] serde_json::Error),
|
|
|
|
#[error(transparent)]
|
|
HexError(#[from] hex::FromHexError),
|
|
|
|
#[error(transparent)]
|
|
HTTPError(#[from] reqwest::Error),
|
|
|
|
#[error("custom error: {0}")]
|
|
CustomError(String),
|
|
|
|
#[error("unsupported RPC")]
|
|
UnsupportedRPC,
|
|
|
|
#[error("unsupported node client")]
|
|
UnsupportedNodeClient,
|
|
|
|
#[error("Attempted to sign a transaction with no available signer. Hint: did you mean to use a SignerMiddleware?")]
|
|
SignerUnavailable,
|
|
}
|
|
|
|
/// Types of filters supported by the JSON-RPC.
|
|
#[derive(Clone, Debug)]
|
|
pub enum FilterKind<'a> {
|
|
/// `eth_newBlockFilter`
|
|
Logs(&'a Filter),
|
|
|
|
/// `eth_newBlockFilter` filter
|
|
NewBlocks,
|
|
|
|
/// `eth_newPendingTransactionFilter` filter
|
|
PendingTransactions,
|
|
}
|
|
|
|
// JSON RPC bindings
|
|
impl<P: JsonRpcClient> Provider<P> {
|
|
/// Instantiate a new provider with a backend.
|
|
pub fn new(provider: P) -> Self {
|
|
Self {
|
|
inner: provider,
|
|
ens: None,
|
|
interval: None,
|
|
from: None,
|
|
_node_client: Arc::new(Mutex::new(None)),
|
|
}
|
|
}
|
|
|
|
/// Returns the type of node we're connected to, while also caching the value for use
|
|
/// in other node-specific API calls, such as the get_block_receipts call.
|
|
pub async fn node_client(&self) -> Result<NodeClient, ProviderError> {
|
|
let mut node_client = self._node_client.lock().await;
|
|
|
|
if let Some(node_client) = *node_client {
|
|
Ok(node_client)
|
|
} else {
|
|
let client_version = self.client_version().await?;
|
|
let client_version = match client_version.parse::<NodeClient>() {
|
|
Ok(res) => res,
|
|
Err(_) => return Err(ProviderError::UnsupportedNodeClient),
|
|
};
|
|
*node_client = Some(client_version);
|
|
Ok(client_version)
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn with_sender(mut self, address: impl Into<Address>) -> Self {
|
|
self.from = Some(address.into());
|
|
self
|
|
}
|
|
|
|
pub async fn request<T, R>(&self, method: &str, params: T) -> Result<R, ProviderError>
|
|
where
|
|
T: Debug + Serialize + Send + Sync,
|
|
R: Serialize + DeserializeOwned + Debug,
|
|
{
|
|
let span =
|
|
tracing::trace_span!("rpc", method = method, params = ?serde_json::to_string(¶ms)?);
|
|
// https://docs.rs/tracing/0.1.22/tracing/span/struct.Span.html#in-asynchronous-code
|
|
let res = async move {
|
|
trace!("tx");
|
|
let res: R = self.inner.request(method, params).await.map_err(Into::into)?;
|
|
trace!(rx = ?serde_json::to_string(&res)?);
|
|
Ok::<_, ProviderError>(res)
|
|
}
|
|
.instrument(span)
|
|
.await?;
|
|
Ok(res)
|
|
}
|
|
|
|
async fn get_block_gen<Tx: Default + Serialize + DeserializeOwned + Debug>(
|
|
&self,
|
|
id: BlockId,
|
|
include_txs: bool,
|
|
) -> Result<Option<Block<Tx>>, ProviderError> {
|
|
let include_txs = utils::serialize(&include_txs);
|
|
|
|
Ok(match id {
|
|
BlockId::Hash(hash) => {
|
|
let hash = utils::serialize(&hash);
|
|
self.request("eth_getBlockByHash", [hash, include_txs]).await?
|
|
}
|
|
BlockId::Number(num) => {
|
|
let num = utils::serialize(&num);
|
|
self.request("eth_getBlockByNumber", [num, include_txs]).await?
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Analogous to [`Middleware::call`], but returns a [`CallBuilder`] that can either be
|
|
/// `.await`d or used to override the parameters sent to `eth_call`.
|
|
///
|
|
/// See the [`call_raw::spoof`] for functions to construct state override parameters.
|
|
///
|
|
/// Note: this method _does not_ send a transaction from your account
|
|
///
|
|
/// [`call_raw::spoof`]: crate::call_raw::spoof
|
|
///
|
|
/// # Example
|
|
/// ```no_run
|
|
/// # use ethers_core::{
|
|
/// # types::{Address, TransactionRequest, H256},
|
|
/// # utils::{parse_ether, Geth},
|
|
/// # };
|
|
/// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}};
|
|
/// # use std::convert::TryFrom;
|
|
/// #
|
|
/// # #[tokio::main(flavor = "current_thread")]
|
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let geth = Geth::new().spawn();
|
|
/// let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
|
///
|
|
/// let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?;
|
|
/// let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse()?;
|
|
/// let pay_amt = parse_ether(1u64)?;
|
|
///
|
|
/// // Not enough ether to pay for the transaction
|
|
/// let tx = TransactionRequest::pay(adr2, pay_amt).from(adr1).into();
|
|
///
|
|
/// // override the sender's balance for the call
|
|
/// let mut state = spoof::balance(adr1, pay_amt * 2);
|
|
/// provider.call_raw(&tx).state(&state).await?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn call_raw<'a>(&'a self, tx: &'a TypedTransaction) -> CallBuilder<'a, P> {
|
|
CallBuilder::new(self, tx)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "celo")]
|
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
|
impl<P: JsonRpcClient> CeloMiddleware for Provider<P> {
|
|
async fn get_validators_bls_public_keys<T: Into<BlockId> + Send + Sync>(
|
|
&self,
|
|
block_id: T,
|
|
) -> Result<Vec<String>, ProviderError> {
|
|
let block_id = utils::serialize(&block_id.into());
|
|
self.request("istanbul_getValidatorsBLSPublicKeys", [block_id]).await
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
|
impl<P: JsonRpcClient> Middleware for Provider<P> {
|
|
type Error = ProviderError;
|
|
type Provider = P;
|
|
type Inner = Self;
|
|
|
|
fn inner(&self) -> &Self::Inner {
|
|
unreachable!("There is no inner provider here")
|
|
}
|
|
|
|
fn provider(&self) -> &Provider<Self::Provider> {
|
|
self
|
|
}
|
|
|
|
fn default_sender(&self) -> Option<Address> {
|
|
self.from
|
|
}
|
|
|
|
////// Blockchain Status
|
|
//
|
|
// Functions for querying the state of the blockchain
|
|
|
|
/// Returns the current client version using the `web3_clientVersion` RPC.
|
|
async fn client_version(&self) -> Result<String, Self::Error> {
|
|
self.request("web3_clientVersion", ()).await
|
|
}
|
|
|
|
async fn fill_transaction(
|
|
&self,
|
|
tx: &mut TypedTransaction,
|
|
block: Option<BlockId>,
|
|
) -> Result<(), Self::Error> {
|
|
if let Some(default_sender) = self.default_sender() {
|
|
if tx.from().is_none() {
|
|
tx.set_from(default_sender);
|
|
}
|
|
}
|
|
|
|
// TODO: Join the name resolution and gas price future
|
|
|
|
// set the ENS name
|
|
if let Some(NameOrAddress::Name(ref ens_name)) = tx.to() {
|
|
let addr = self.resolve_name(ens_name).await?;
|
|
tx.set_to(addr);
|
|
}
|
|
|
|
// fill gas price
|
|
match tx {
|
|
TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => {
|
|
let gas_price = maybe(tx.gas_price(), self.get_gas_price()).await?;
|
|
tx.set_gas_price(gas_price);
|
|
}
|
|
TypedTransaction::Eip1559(ref mut inner) => {
|
|
if inner.max_fee_per_gas.is_none() || inner.max_priority_fee_per_gas.is_none() {
|
|
let (max_fee_per_gas, max_priority_fee_per_gas) =
|
|
self.estimate_eip1559_fees(None).await?;
|
|
inner.max_fee_per_gas = Some(max_fee_per_gas);
|
|
inner.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Set gas to estimated value only if it was not set by the caller,
|
|
// even if the access list has been populated and saves gas
|
|
if tx.gas().is_none() {
|
|
let gas_estimate = self.estimate_gas(tx, block).await?;
|
|
tx.set_gas(gas_estimate);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Gets the latest block number via the `eth_BlockNumber` API
|
|
async fn get_block_number(&self) -> Result<U64, ProviderError> {
|
|
self.request("eth_blockNumber", ()).await
|
|
}
|
|
|
|
/// Gets the block at `block_hash_or_number` (transaction hashes only)
|
|
async fn get_block<T: Into<BlockId> + Send + Sync>(
|
|
&self,
|
|
block_hash_or_number: T,
|
|
) -> Result<Option<Block<TxHash>>, Self::Error> {
|
|
self.get_block_gen(block_hash_or_number.into(), false).await
|
|
}
|
|
|
|
/// Gets the block at `block_hash_or_number` (full transactions included)
|
|
async fn get_block_with_txs<T: Into<BlockId> + Send + Sync>(
|
|
&self,
|
|
block_hash_or_number: T,
|
|
) -> Result<Option<Block<Transaction>>, ProviderError> {
|
|
self.get_block_gen(block_hash_or_number.into(), true).await
|
|
}
|
|
|
|
/// Gets the block uncle count at `block_hash_or_number`
|
|
async fn get_uncle_count<T: Into<BlockId> + Send + Sync>(
|
|
&self,
|
|
block_hash_or_number: T,
|
|
) -> Result<U256, Self::Error> {
|
|
let id = block_hash_or_number.into();
|
|
Ok(match id {
|
|
BlockId::Hash(hash) => {
|
|
let hash = utils::serialize(&hash);
|
|
self.request("eth_getUncleCountByBlockHash", [hash]).await?
|
|
}
|
|
BlockId::Number(num) => {
|
|
let num = utils::serialize(&num);
|
|
self.request("eth_getUncleCountByBlockNumber", [num]).await?
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Gets the block uncle at `block_hash_or_number` and `idx`
|
|
async fn get_uncle<T: Into<BlockId> + Send + Sync>(
|
|
&self,
|
|
block_hash_or_number: T,
|
|
idx: U64,
|
|
) -> Result<Option<Block<H256>>, ProviderError> {
|
|
let blk_id = block_hash_or_number.into();
|
|
let idx = utils::serialize(&idx);
|
|
Ok(match blk_id {
|
|
BlockId::Hash(hash) => {
|
|
let hash = utils::serialize(&hash);
|
|
self.request("eth_getUncleByBlockHashAndIndex", [hash, idx]).await?
|
|
}
|
|
BlockId::Number(num) => {
|
|
let num = utils::serialize(&num);
|
|
self.request("eth_getUncleByBlockNumberAndIndex", [num, idx]).await?
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Gets the transaction with `transaction_hash`
|
|
async fn get_transaction<T: Send + Sync + Into<TxHash>>(
|
|
&self,
|
|
transaction_hash: T,
|
|
) -> Result<Option<Transaction>, ProviderError> {
|
|
let hash = transaction_hash.into();
|
|
self.request("eth_getTransactionByHash", [hash]).await
|
|
}
|
|
|
|
/// Gets the transaction receipt with `transaction_hash`
|
|
async fn get_transaction_receipt<T: Send + Sync + Into<TxHash>>(
|
|
&self,
|
|
transaction_hash: T,
|
|
) -> Result<Option<TransactionReceipt>, ProviderError> {
|
|
let hash = transaction_hash.into();
|
|
self.request("eth_getTransactionReceipt", [hash]).await
|
|
}
|
|
|
|
/// Returns all receipts for a block.
|
|
///
|
|
/// Note that this uses the `eth_getBlockReceipts` RPC, which is
|
|
/// non-standard and currently supported by Erigon.
|
|
async fn get_block_receipts<T: Into<BlockNumber> + Send + Sync>(
|
|
&self,
|
|
block: T,
|
|
) -> Result<Vec<TransactionReceipt>, Self::Error> {
|
|
self.request("eth_getBlockReceipts", [block.into()]).await
|
|
}
|
|
|
|
/// Returns all receipts for that block. Must be done on a parity node.
|
|
async fn parity_block_receipts<T: Into<BlockNumber> + Send + Sync>(
|
|
&self,
|
|
block: T,
|
|
) -> Result<Vec<TransactionReceipt>, Self::Error> {
|
|
self.request("parity_getBlockReceipts", vec![block.into()]).await
|
|
}
|
|
|
|
/// Gets the current gas price as estimated by the node
|
|
async fn get_gas_price(&self) -> Result<U256, ProviderError> {
|
|
self.request("eth_gasPrice", ()).await
|
|
}
|
|
|
|
/// Gets a heuristic recommendation of max fee per gas and max priority fee per gas for
|
|
/// EIP-1559 compatible transactions.
|
|
async fn estimate_eip1559_fees(
|
|
&self,
|
|
estimator: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
|
|
) -> Result<(U256, U256), Self::Error> {
|
|
let base_fee_per_gas = self
|
|
.get_block(BlockNumber::Latest)
|
|
.await?
|
|
.ok_or_else(|| ProviderError::CustomError("Latest block not found".into()))?
|
|
.base_fee_per_gas
|
|
.ok_or_else(|| ProviderError::CustomError("EIP-1559 not activated".into()))?;
|
|
|
|
let fee_history = self
|
|
.fee_history(
|
|
utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS,
|
|
BlockNumber::Latest,
|
|
&[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE],
|
|
)
|
|
.await?;
|
|
|
|
// use the provided fee estimator function, or fallback to the default implementation.
|
|
let (max_fee_per_gas, max_priority_fee_per_gas) = if let Some(es) = estimator {
|
|
es(base_fee_per_gas, fee_history.reward)
|
|
} else {
|
|
utils::eip1559_default_estimator(base_fee_per_gas, fee_history.reward)
|
|
};
|
|
|
|
Ok((max_fee_per_gas, max_priority_fee_per_gas))
|
|
}
|
|
|
|
/// Gets the accounts on the node
|
|
async fn get_accounts(&self) -> Result<Vec<Address>, ProviderError> {
|
|
self.request("eth_accounts", ()).await
|
|
}
|
|
|
|
/// Returns the nonce of the address
|
|
async fn get_transaction_count<T: Into<NameOrAddress> + Send + Sync>(
|
|
&self,
|
|
from: T,
|
|
block: Option<BlockId>,
|
|
) -> Result<U256, ProviderError> {
|
|
let from = match from.into() {
|
|
NameOrAddress::Name(ens_name) => self.resolve_name(&ens_name).await?,
|
|
NameOrAddress::Address(addr) => addr,
|
|
};
|
|
|
|
let from = utils::serialize(&from);
|
|
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
|
|
self.request("eth_getTransactionCount", [from, block]).await
|
|
}
|
|
|
|
/// Returns the account's balance
|
|
async fn get_balance<T: Into<NameOrAddress> + Send + Sync>(
|
|
&self,
|
|
from: T,
|
|
block: Option<BlockId>,
|
|
) -> Result<U256, ProviderError> {
|
|
let from = match from.into() {
|
|
NameOrAddress::Name(ens_name) => self.resolve_name(&ens_name).await?,
|
|
NameOrAddress::Address(addr) => addr,
|
|
};
|
|
|
|
let from = utils::serialize(&from);
|
|
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
|
|
self.request("eth_getBalance", [from, block]).await
|
|
}
|
|
|
|
/// Returns the currently configured chain id, a value used in replay-protected
|
|
/// transaction signing as introduced by EIP-155.
|
|
async fn get_chainid(&self) -> Result<U256, ProviderError> {
|
|
self.request("eth_chainId", ()).await
|
|
}
|
|
|
|
/// Return current client syncing status. If IsFalse sync is over.
|
|
async fn syncing(&self) -> Result<SyncingStatus, Self::Error> {
|
|
self.request("eth_syncing", ()).await
|
|
}
|
|
|
|
/// Returns the network version.
|
|
async fn get_net_version(&self) -> Result<String, ProviderError> {
|
|
self.request("net_version", ()).await
|
|
}
|
|
|
|
////// Contract Execution
|
|
//
|
|
// These are relatively low-level calls. The Contracts API should usually be used instead.
|
|
|
|
/// Sends the read-only (constant) transaction to a single Ethereum node and return the result
|
|
/// (as bytes) of executing it. This is free, since it does not change any state on the
|
|
/// blockchain.
|
|
async fn call(
|
|
&self,
|
|
tx: &TypedTransaction,
|
|
block: Option<BlockId>,
|
|
) -> Result<Bytes, ProviderError> {
|
|
let tx = utils::serialize(tx);
|
|
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
|
|
self.request("eth_call", [tx, block]).await
|
|
}
|
|
|
|
/// Sends a transaction to a single Ethereum node and return the estimated amount of gas
|
|
/// required (as a U256) to send it This is free, but only an estimate. Providing too little
|
|
/// gas will result in a transaction being rejected (while still consuming all provided
|
|
/// gas).
|
|
async fn estimate_gas(
|
|
&self,
|
|
tx: &TypedTransaction,
|
|
block: Option<BlockId>,
|
|
) -> Result<U256, ProviderError> {
|
|
let tx = utils::serialize(tx);
|
|
// Some nodes (e.g. old Optimism clients) don't support a block ID being passed as a param,
|
|
// so refrain from defaulting to BlockNumber::Latest.
|
|
let params = if let Some(block_id) = block {
|
|
vec![tx, utils::serialize(&block_id)]
|
|
} else {
|
|
vec![tx]
|
|
};
|
|
self.request("eth_estimateGas", params).await
|
|
}
|
|
|
|
async fn create_access_list(
|
|
&self,
|
|
tx: &TypedTransaction,
|
|
block: Option<BlockId>,
|
|
) -> Result<AccessListWithGasUsed, ProviderError> {
|
|
let tx = utils::serialize(tx);
|
|
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
|
|
self.request("eth_createAccessList", [tx, block]).await
|
|
}
|
|
|
|
/// Sends the transaction to the entire Ethereum network and returns the transaction's hash
|
|
/// This will consume gas from the account that signed the transaction.
|
|
async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
|
|
&self,
|
|
tx: T,
|
|
block: Option<BlockId>,
|
|
) -> Result<PendingTransaction<'_, P>, ProviderError> {
|
|
let mut tx = tx.into();
|
|
self.fill_transaction(&mut tx, block).await?;
|
|
let tx_hash = self.request("eth_sendTransaction", [tx]).await?;
|
|
|
|
Ok(PendingTransaction::new(tx_hash, self))
|
|
}
|
|
|
|
/// Send the raw RLP encoded transaction to the entire Ethereum network and returns the
|
|
/// transaction's hash This will consume gas from the account that signed the transaction.
|
|
async fn send_raw_transaction<'a>(
|
|
&'a self,
|
|
tx: Bytes,
|
|
) -> Result<PendingTransaction<'a, P>, ProviderError> {
|
|
let rlp = utils::serialize(&tx);
|
|
let tx_hash = self.request("eth_sendRawTransaction", [rlp]).await?;
|
|
Ok(PendingTransaction::new(tx_hash, self))
|
|
}
|
|
|
|
/// The JSON-RPC provider is at the bottom-most position in the middleware stack. Here we check
|
|
/// if it has the key for the sender address unlocked, as well as supports the `eth_sign` call.
|
|
async fn is_signer(&self) -> bool {
|
|
match self.from {
|
|
Some(sender) => self.sign(vec![], &sender).await.is_ok(),
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// Signs data using a specific account. This account needs to be unlocked.
|
|
async fn sign<T: Into<Bytes> + Send + Sync>(
|
|
&self,
|
|
data: T,
|
|
from: &Address,
|
|
) -> Result<Signature, ProviderError> {
|
|
let data = utils::serialize(&data.into());
|
|
let from = utils::serialize(from);
|
|
|
|
// get the response from `eth_sign` call and trim the 0x-prefix if present.
|
|
let sig: String = self.request("eth_sign", [from, data]).await?;
|
|
let sig = sig.strip_prefix("0x").unwrap_or(&sig);
|
|
|
|
// decode the signature.
|
|
let sig = hex::decode(sig)?;
|
|
Ok(Signature::try_from(sig.as_slice())
|
|
.map_err(|e| ProviderError::CustomError(e.to_string()))?)
|
|
}
|
|
|
|
/// Sign a transaction via RPC call
|
|
async fn sign_transaction(
|
|
&self,
|
|
_tx: &TypedTransaction,
|
|
_from: Address,
|
|
) -> Result<Signature, Self::Error> {
|
|
Err(ProviderError::SignerUnavailable).map_err(FromErr::from)
|
|
}
|
|
|
|
////// Contract state
|
|
|
|
/// Returns an array (possibly empty) of logs that match the filter
|
|
async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>, ProviderError> {
|
|
self.request("eth_getLogs", [filter]).await
|
|
}
|
|
|
|
fn get_logs_paginated<'a>(&'a self, filter: &Filter, page_size: u64) -> LogQuery<'a, P> {
|
|
LogQuery::new(self, filter).with_page_size(page_size)
|
|
}
|
|
|
|
/// Streams matching filter logs
|
|
async fn watch<'a>(
|
|
&'a self,
|
|
filter: &Filter,
|
|
) -> Result<FilterWatcher<'a, P, Log>, ProviderError> {
|
|
let id = self.new_filter(FilterKind::Logs(filter)).await?;
|
|
let filter = FilterWatcher::new(id, self).interval(self.get_interval());
|
|
Ok(filter)
|
|
}
|
|
|
|
/// Streams new block hashes
|
|
async fn watch_blocks(&self) -> Result<FilterWatcher<'_, P, H256>, ProviderError> {
|
|
let id = self.new_filter(FilterKind::NewBlocks).await?;
|
|
let filter = FilterWatcher::new(id, self).interval(self.get_interval());
|
|
Ok(filter)
|
|
}
|
|
|
|
/// Streams pending transactions
|
|
async fn watch_pending_transactions(
|
|
&self,
|
|
) -> Result<FilterWatcher<'_, P, H256>, ProviderError> {
|
|
let id = self.new_filter(FilterKind::PendingTransactions).await?;
|
|
let filter = FilterWatcher::new(id, self).interval(self.get_interval());
|
|
Ok(filter)
|
|
}
|
|
|
|
/// Creates a filter object, based on filter options, to notify when the state changes (logs).
|
|
/// To check if the state has changed, call `get_filter_changes` with the filter id.
|
|
async fn new_filter(&self, filter: FilterKind<'_>) -> Result<U256, ProviderError> {
|
|
let (method, args) = match filter {
|
|
FilterKind::NewBlocks => ("eth_newBlockFilter", vec![]),
|
|
FilterKind::PendingTransactions => ("eth_newPendingTransactionFilter", vec![]),
|
|
FilterKind::Logs(filter) => ("eth_newFilter", vec![utils::serialize(&filter)]),
|
|
};
|
|
|
|
self.request(method, args).await
|
|
}
|
|
|
|
/// Uninstalls a filter
|
|
async fn uninstall_filter<T: Into<U256> + Send + Sync>(
|
|
&self,
|
|
id: T,
|
|
) -> Result<bool, ProviderError> {
|
|
let id = utils::serialize(&id.into());
|
|
self.request("eth_uninstallFilter", [id]).await
|
|
}
|
|
|
|
/// Polling method for a filter, which returns an array of logs which occurred since last poll.
|
|
///
|
|
/// This method must be called with one of the following return types, depending on the filter
|
|
/// type:
|
|
/// - `eth_newBlockFilter`: [`H256`], returns block hashes
|
|
/// - `eth_newPendingTransactionFilter`: [`H256`], returns transaction hashes
|
|
/// - `eth_newFilter`: [`Log`], returns raw logs
|
|
///
|
|
/// If one of these types is not used, decoding will fail and the method will
|
|
/// return an error.
|
|
///
|
|
/// [`H256`]: ethers_core::types::H256
|
|
/// [`Log`]: ethers_core::types::Log
|
|
async fn get_filter_changes<T, R>(&self, id: T) -> Result<Vec<R>, ProviderError>
|
|
where
|
|
T: Into<U256> + Send + Sync,
|
|
R: Serialize + DeserializeOwned + Send + Sync + Debug,
|
|
{
|
|
let id = utils::serialize(&id.into());
|
|
self.request("eth_getFilterChanges", [id]).await
|
|
}
|
|
|
|
/// Get the storage of an address for a particular slot location
|
|
async fn get_storage_at<T: Into<NameOrAddress> + Send + Sync>(
|
|
&self,
|
|
from: T,
|
|
location: H256,
|
|
block: Option<BlockId>,
|
|
) -> Result<H256, ProviderError> {
|
|
let from = match from.into() {
|
|
NameOrAddress::Name(ens_name) => self.resolve_name(&ens_name).await?,
|
|
NameOrAddress::Address(addr) => addr,
|
|
};
|
|
|
|
// position is a QUANTITY according to the [spec](https://eth.wiki/json-rpc/API#eth_getstorageat): integer of the position in the storage, converting this to a U256
|
|
// will make sure the number is formatted correctly as [quantity](https://eips.ethereum.org/EIPS/eip-1474#quantity)
|
|
let position = U256::from_big_endian(location.as_bytes());
|
|
let position = utils::serialize(&position);
|
|
let from = utils::serialize(&from);
|
|
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
|
|
|
|
// get the hex encoded value.
|
|
let value: String = self.request("eth_getStorageAt", [from, position, block]).await?;
|
|
// get rid of the 0x prefix and left pad it with zeroes.
|
|
let value = format!("{:0>64}", value.replace("0x", ""));
|
|
Ok(H256::from_slice(&Vec::from_hex(value)?))
|
|
}
|
|
|
|
/// Returns the deployed code at a given address
|
|
async fn get_code<T: Into<NameOrAddress> + Send + Sync>(
|
|
&self,
|
|
at: T,
|
|
block: Option<BlockId>,
|
|
) -> Result<Bytes, ProviderError> {
|
|
let at = match at.into() {
|
|
NameOrAddress::Name(ens_name) => self.resolve_name(&ens_name).await?,
|
|
NameOrAddress::Address(addr) => addr,
|
|
};
|
|
|
|
let at = utils::serialize(&at);
|
|
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
|
|
self.request("eth_getCode", [at, block]).await
|
|
}
|
|
|
|
/// Returns the EIP-1186 proof response
|
|
/// <https://github.com/ethereum/EIPs/issues/1186>
|
|
async fn get_proof<T: Into<NameOrAddress> + Send + Sync>(
|
|
&self,
|
|
from: T,
|
|
locations: Vec<H256>,
|
|
block: Option<BlockId>,
|
|
) -> Result<EIP1186ProofResponse, ProviderError> {
|
|
let from = match from.into() {
|
|
NameOrAddress::Name(ens_name) => self.resolve_name(&ens_name).await?,
|
|
NameOrAddress::Address(addr) => addr,
|
|
};
|
|
|
|
let from = utils::serialize(&from);
|
|
let locations = locations.iter().map(|location| utils::serialize(&location)).collect();
|
|
let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
|
|
|
|
self.request("eth_getProof", [from, locations, block]).await
|
|
}
|
|
|
|
// Admin namespace
|
|
|
|
/// Requests adding the given peer, returning a boolean representing whether or not the peer
|
|
/// was accepted for tracking.
|
|
async fn add_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
|
let enode_url = utils::serialize(&enode_url);
|
|
self.request("admin_addPeer", [enode_url]).await
|
|
}
|
|
|
|
/// Requests adding the given peer as a trusted peer, which the node will always connect to
|
|
/// even when its peer slots are full.
|
|
async fn add_trusted_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
|
let enode_url = utils::serialize(&enode_url);
|
|
self.request("admin_addTrustedPeer", [enode_url]).await
|
|
}
|
|
|
|
/// Returns general information about the node as well as information about the running p2p
|
|
/// protocols (e.g. `eth`, `snap`).
|
|
async fn node_info(&self) -> Result<NodeInfo, Self::Error> {
|
|
self.request("admin_nodeInfo", ()).await
|
|
}
|
|
|
|
/// Returns the list of peers currently connected to the node.
|
|
async fn peers(&self) -> Result<Vec<PeerInfo>, Self::Error> {
|
|
self.request("admin_peers", ()).await
|
|
}
|
|
|
|
/// Requests to remove the given peer, returning true if the enode was successfully parsed and
|
|
/// the peer was removed.
|
|
async fn remove_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
|
let enode_url = utils::serialize(&enode_url);
|
|
self.request("admin_removePeer", [enode_url]).await
|
|
}
|
|
|
|
/// Requests to remove the given peer, returning a boolean representing whether or not the
|
|
/// enode url passed was validated. A return value of `true` does not necessarily mean that the
|
|
/// peer was disconnected.
|
|
async fn remove_trusted_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
|
let enode_url = utils::serialize(&enode_url);
|
|
self.request("admin_removeTrustedPeer", [enode_url]).await
|
|
}
|
|
|
|
////// Ethereum Naming Service
|
|
// The Ethereum Naming Service (ENS) allows easy to remember and use names to
|
|
// be assigned to Ethereum addresses. Any provider operation which takes an address
|
|
// may also take an ENS name.
|
|
//
|
|
// ENS also provides the ability for a reverse lookup, which determines the name for an address
|
|
// if it has been configured.
|
|
|
|
/// Returns the address that the `ens_name` resolves to (or None if not configured).
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// If the bytes returned from the ENS registrar/resolver cannot be interpreted as
|
|
/// an address. This should theoretically never happen.
|
|
async fn resolve_name(&self, ens_name: &str) -> Result<Address, ProviderError> {
|
|
self.query_resolver(ParamType::Address, ens_name, ens::ADDR_SELECTOR).await
|
|
}
|
|
|
|
/// Returns the ENS name the `address` resolves to (or None if not configured).
|
|
/// # Panics
|
|
///
|
|
/// If the bytes returned from the ENS registrar/resolver cannot be interpreted as
|
|
/// a string. This should theoretically never happen.
|
|
async fn lookup_address(&self, address: Address) -> Result<String, ProviderError> {
|
|
let ens_name = ens::reverse_address(address);
|
|
let domain: String =
|
|
self.query_resolver(ParamType::String, &ens_name, ens::NAME_SELECTOR).await?;
|
|
let reverse_address = self.resolve_name(&domain).await?;
|
|
if address != reverse_address {
|
|
Err(ProviderError::EnsNotOwned(domain))
|
|
} else {
|
|
Ok(domain)
|
|
}
|
|
}
|
|
|
|
/// Returns the avatar HTTP link of the avatar that the `ens_name` resolves to (or None
|
|
/// if not configured)
|
|
///
|
|
/// # Example
|
|
/// ```no_run
|
|
/// # use ethers_providers::{Provider, Http as HttpProvider, Middleware};
|
|
/// # use std::convert::TryFrom;
|
|
/// # #[tokio::main(flavor = "current_thread")]
|
|
/// # async fn main() {
|
|
/// # let provider = Provider::<HttpProvider>::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").unwrap();
|
|
/// let avatar = provider.resolve_avatar("parishilton.eth").await.unwrap();
|
|
/// assert_eq!(avatar.to_string(), "https://i.imgur.com/YW3Hzph.jpg");
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// If the bytes returned from the ENS registrar/resolver cannot be interpreted as
|
|
/// a string. This should theoretically never happen.
|
|
async fn resolve_avatar(&self, ens_name: &str) -> Result<Url, ProviderError> {
|
|
let (field, owner) =
|
|
try_join!(self.resolve_field(ens_name, "avatar"), self.resolve_name(ens_name))?;
|
|
let url = Url::from_str(&field).map_err(|e| ProviderError::CustomError(e.to_string()))?;
|
|
match url.scheme() {
|
|
"https" | "data" => Ok(url),
|
|
"ipfs" => erc::http_link_ipfs(url).map_err(ProviderError::CustomError),
|
|
"eip155" => {
|
|
let token =
|
|
erc::ERCNFT::from_str(url.path()).map_err(ProviderError::CustomError)?;
|
|
match token.type_ {
|
|
erc::ERCNFTType::ERC721 => {
|
|
let tx = TransactionRequest {
|
|
data: Some(
|
|
[&erc::ERC721_OWNER_SELECTOR[..], &token.id].concat().into(),
|
|
),
|
|
to: Some(NameOrAddress::Address(token.contract)),
|
|
..Default::default()
|
|
};
|
|
let data = self.call(&tx.into(), None).await?;
|
|
if decode_bytes::<Address>(ParamType::Address, data) != owner {
|
|
return Err(ProviderError::CustomError("Incorrect owner.".to_string()))
|
|
}
|
|
}
|
|
erc::ERCNFTType::ERC1155 => {
|
|
let tx = TransactionRequest {
|
|
data: Some(
|
|
[
|
|
&erc::ERC1155_BALANCE_SELECTOR[..],
|
|
&[0x0; 12],
|
|
&owner.0,
|
|
&token.id,
|
|
]
|
|
.concat()
|
|
.into(),
|
|
),
|
|
to: Some(NameOrAddress::Address(token.contract)),
|
|
..Default::default()
|
|
};
|
|
let data = self.call(&tx.into(), None).await?;
|
|
if decode_bytes::<u64>(ParamType::Uint(64), data) == 0 {
|
|
return Err(ProviderError::CustomError("Incorrect balance.".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
let image_url = self.resolve_nft(token).await?;
|
|
match image_url.scheme() {
|
|
"https" | "data" => Ok(image_url),
|
|
"ipfs" => erc::http_link_ipfs(image_url).map_err(ProviderError::CustomError),
|
|
_ => Err(ProviderError::CustomError(
|
|
"Unsupported scheme for the image".to_string(),
|
|
)),
|
|
}
|
|
}
|
|
_ => Err(ProviderError::CustomError("Unsupported scheme".to_string())),
|
|
}
|
|
}
|
|
|
|
/// Returns the URL (not necesserily HTTP) of the image behind a token.
|
|
///
|
|
/// # Example
|
|
/// ```no_run
|
|
/// # use ethers_providers::{Provider, Http as HttpProvider, Middleware};
|
|
/// # use std::{str::FromStr, convert::TryFrom};
|
|
/// # #[tokio::main(flavor = "current_thread")]
|
|
/// # async fn main() {
|
|
/// # let provider = Provider::<HttpProvider>::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").unwrap();
|
|
/// let token = ethers_providers::erc::ERCNFT::from_str("erc721:0xc92ceddfb8dd984a89fb494c376f9a48b999aafc/9018").unwrap();
|
|
/// let token_image = provider.resolve_nft(token).await.unwrap();
|
|
/// assert_eq!(token_image.to_string(), "https://creature.mypinata.cloud/ipfs/QmNwj3aUzXfG4twV3no7hJRYxLLAWNPk6RrfQaqJ6nVJFa/9018.jpg");
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// If the bytes returned from the ENS registrar/resolver cannot be interpreted as
|
|
/// a string. This should theoretically never happen.
|
|
async fn resolve_nft(&self, token: erc::ERCNFT) -> Result<Url, ProviderError> {
|
|
let selector = token.type_.resolution_selector();
|
|
let tx = TransactionRequest {
|
|
data: Some([&selector[..], &token.id].concat().into()),
|
|
to: Some(NameOrAddress::Address(token.contract)),
|
|
..Default::default()
|
|
};
|
|
let data = self.call(&tx.into(), None).await?;
|
|
let mut metadata_url = Url::parse(&decode_bytes::<String>(ParamType::String, data))
|
|
.map_err(|e| ProviderError::CustomError(format!("Invalid metadata url: {e}")))?;
|
|
|
|
if token.type_ == erc::ERCNFTType::ERC1155 {
|
|
metadata_url.set_path(&metadata_url.path().replace("%7Bid%7D", &hex::encode(token.id)));
|
|
}
|
|
if metadata_url.scheme() == "ipfs" {
|
|
metadata_url = erc::http_link_ipfs(metadata_url).map_err(ProviderError::CustomError)?;
|
|
}
|
|
let metadata: erc::Metadata = reqwest::get(metadata_url).await?.json().await?;
|
|
Url::parse(&metadata.image).map_err(|e| ProviderError::CustomError(e.to_string()))
|
|
}
|
|
|
|
/// Fetch a field for the `ens_name` (no None if not configured).
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// If the bytes returned from the ENS registrar/resolver cannot be interpreted as
|
|
/// a string. This should theoretically never happen.
|
|
async fn resolve_field(&self, ens_name: &str, field: &str) -> Result<String, ProviderError> {
|
|
let field: String = self
|
|
.query_resolver_parameters(
|
|
ParamType::String,
|
|
ens_name,
|
|
ens::FIELD_SELECTOR,
|
|
Some(&ens::parameterhash(field)),
|
|
)
|
|
.await?;
|
|
Ok(field)
|
|
}
|
|
|
|
/// Returns the details of all transactions currently pending for inclusion in the next
|
|
/// block(s), as well as the ones that are being scheduled for future execution only.
|
|
/// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_content)
|
|
async fn txpool_content(&self) -> Result<TxpoolContent, ProviderError> {
|
|
self.request("txpool_content", ()).await
|
|
}
|
|
|
|
/// Returns a summary of all the transactions currently pending for inclusion in the next
|
|
/// block(s), as well as the ones that are being scheduled for future execution only.
|
|
/// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_inspect)
|
|
async fn txpool_inspect(&self) -> Result<TxpoolInspect, ProviderError> {
|
|
self.request("txpool_inspect", ()).await
|
|
}
|
|
|
|
/// Returns the number of transactions currently pending for inclusion in the next block(s), as
|
|
/// well as the ones that are being scheduled for future execution only.
|
|
/// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_status)
|
|
async fn txpool_status(&self) -> Result<TxpoolStatus, ProviderError> {
|
|
self.request("txpool_status", ()).await
|
|
}
|
|
|
|
/// Executes the given call and returns a number of possible traces for it
|
|
async fn debug_trace_transaction(
|
|
&self,
|
|
tx_hash: TxHash,
|
|
trace_options: GethDebugTracingOptions,
|
|
) -> Result<GethTrace, ProviderError> {
|
|
let tx_hash = utils::serialize(&tx_hash);
|
|
let trace_options = utils::serialize(&trace_options);
|
|
self.request("debug_traceTransaction", [tx_hash, trace_options]).await
|
|
}
|
|
|
|
/// Executes the given call and returns a number of possible traces for it
|
|
async fn trace_call<T: Into<TypedTransaction> + Send + Sync>(
|
|
&self,
|
|
req: T,
|
|
trace_type: Vec<TraceType>,
|
|
block: Option<BlockNumber>,
|
|
) -> Result<BlockTrace, ProviderError> {
|
|
let req = req.into();
|
|
let req = utils::serialize(&req);
|
|
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
|
|
let trace_type = utils::serialize(&trace_type);
|
|
self.request("trace_call", [req, trace_type, block]).await
|
|
}
|
|
|
|
/// Executes given calls and returns a number of possible traces for each call
|
|
async fn trace_call_many<T: Into<TypedTransaction> + Send + Sync>(
|
|
&self,
|
|
req: Vec<(T, Vec<TraceType>)>,
|
|
block: Option<BlockNumber>,
|
|
) -> Result<Vec<BlockTrace>, ProviderError> {
|
|
let req: Vec<(TypedTransaction, Vec<TraceType>)> =
|
|
req.into_iter().map(|(tx, trace_type)| (tx.into(), trace_type)).collect();
|
|
let req = utils::serialize(&req);
|
|
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
|
|
self.request("trace_callMany", [req, block]).await
|
|
}
|
|
|
|
/// Traces a call to `eth_sendRawTransaction` without making the call, returning the traces
|
|
async fn trace_raw_transaction(
|
|
&self,
|
|
data: Bytes,
|
|
trace_type: Vec<TraceType>,
|
|
) -> Result<BlockTrace, ProviderError> {
|
|
let data = utils::serialize(&data);
|
|
let trace_type = utils::serialize(&trace_type);
|
|
self.request("trace_rawTransaction", [data, trace_type]).await
|
|
}
|
|
|
|
/// Replays a transaction, returning the traces
|
|
async fn trace_replay_transaction(
|
|
&self,
|
|
hash: H256,
|
|
trace_type: Vec<TraceType>,
|
|
) -> Result<BlockTrace, ProviderError> {
|
|
let hash = utils::serialize(&hash);
|
|
let trace_type = utils::serialize(&trace_type);
|
|
self.request("trace_replayTransaction", [hash, trace_type]).await
|
|
}
|
|
|
|
/// Replays all transactions in a block returning the requested traces for each transaction
|
|
async fn trace_replay_block_transactions(
|
|
&self,
|
|
block: BlockNumber,
|
|
trace_type: Vec<TraceType>,
|
|
) -> Result<Vec<BlockTrace>, ProviderError> {
|
|
let block = utils::serialize(&block);
|
|
let trace_type = utils::serialize(&trace_type);
|
|
self.request("trace_replayBlockTransactions", [block, trace_type]).await
|
|
}
|
|
|
|
/// Returns traces created at given block
|
|
async fn trace_block(&self, block: BlockNumber) -> Result<Vec<Trace>, ProviderError> {
|
|
let block = utils::serialize(&block);
|
|
self.request("trace_block", [block]).await
|
|
}
|
|
|
|
/// Return traces matching the given filter
|
|
async fn trace_filter(&self, filter: TraceFilter) -> Result<Vec<Trace>, ProviderError> {
|
|
let filter = utils::serialize(&filter);
|
|
self.request("trace_filter", vec![filter]).await
|
|
}
|
|
|
|
/// Returns trace at the given position
|
|
async fn trace_get<T: Into<U64> + Send + Sync>(
|
|
&self,
|
|
hash: H256,
|
|
index: Vec<T>,
|
|
) -> Result<Trace, ProviderError> {
|
|
let hash = utils::serialize(&hash);
|
|
let index: Vec<U64> = index.into_iter().map(|i| i.into()).collect();
|
|
let index = utils::serialize(&index);
|
|
self.request("trace_get", vec![hash, index]).await
|
|
}
|
|
|
|
/// Returns all traces of a given transaction
|
|
async fn trace_transaction(&self, hash: H256) -> Result<Vec<Trace>, ProviderError> {
|
|
let hash = utils::serialize(&hash);
|
|
self.request("trace_transaction", vec![hash]).await
|
|
}
|
|
|
|
async fn subscribe<T, R>(
|
|
&self,
|
|
params: T,
|
|
) -> Result<SubscriptionStream<'_, P, R>, ProviderError>
|
|
where
|
|
T: Debug + Serialize + Send + Sync,
|
|
R: DeserializeOwned + Send + Sync,
|
|
P: PubsubClient,
|
|
{
|
|
let id: U256 = self.request("eth_subscribe", params).await?;
|
|
SubscriptionStream::new(id, self).map_err(Into::into)
|
|
}
|
|
|
|
async fn unsubscribe<T>(&self, id: T) -> Result<bool, ProviderError>
|
|
where
|
|
T: Into<U256> + Send + Sync,
|
|
P: PubsubClient,
|
|
{
|
|
self.request("eth_unsubscribe", [id.into()]).await
|
|
}
|
|
|
|
async fn subscribe_blocks(
|
|
&self,
|
|
) -> Result<SubscriptionStream<'_, P, Block<TxHash>>, ProviderError>
|
|
where
|
|
P: PubsubClient,
|
|
{
|
|
self.subscribe(["newHeads"]).await
|
|
}
|
|
|
|
async fn subscribe_pending_txs(
|
|
&self,
|
|
) -> Result<SubscriptionStream<'_, P, TxHash>, ProviderError>
|
|
where
|
|
P: PubsubClient,
|
|
{
|
|
self.subscribe(["newPendingTransactions"]).await
|
|
}
|
|
|
|
async fn subscribe_logs<'a>(
|
|
&'a self,
|
|
filter: &Filter,
|
|
) -> Result<SubscriptionStream<'a, P, Log>, ProviderError>
|
|
where
|
|
P: PubsubClient,
|
|
{
|
|
let loaded_logs = match filter.block_option {
|
|
FilterBlockOption::Range { from_block, to_block: _ } => {
|
|
if from_block.is_none() {
|
|
vec![]
|
|
} else {
|
|
self.get_logs(filter).await?
|
|
}
|
|
}
|
|
FilterBlockOption::AtBlockHash(_block_hash) => self.get_logs(filter).await?,
|
|
};
|
|
let loaded_logs = VecDeque::from(loaded_logs);
|
|
|
|
let logs = utils::serialize(&"logs"); // TODO: Make this a static
|
|
let filter = utils::serialize(filter);
|
|
self.subscribe([logs, filter]).await.map(|mut stream| {
|
|
stream.set_loaded_elements(loaded_logs);
|
|
stream
|
|
})
|
|
}
|
|
|
|
async fn fee_history<T: Into<U256> + Send + Sync>(
|
|
&self,
|
|
block_count: T,
|
|
last_block: BlockNumber,
|
|
reward_percentiles: &[f64],
|
|
) -> Result<FeeHistory, Self::Error> {
|
|
let block_count = block_count.into();
|
|
let last_block = utils::serialize(&last_block);
|
|
let reward_percentiles = utils::serialize(&reward_percentiles);
|
|
|
|
// The blockCount param is expected to be an unsigned integer up to geth v1.10.6.
|
|
// Geth v1.10.7 onwards, this has been updated to a hex encoded form. Failure to
|
|
// decode the param from client side would fallback to the old API spec.
|
|
match self
|
|
.request::<_, FeeHistory>(
|
|
"eth_feeHistory",
|
|
[utils::serialize(&block_count), last_block.clone(), reward_percentiles.clone()],
|
|
)
|
|
.await
|
|
{
|
|
success @ Ok(_) => success,
|
|
err @ Err(_) => {
|
|
let fallback = self
|
|
.request::<_, FeeHistory>(
|
|
"eth_feeHistory",
|
|
[utils::serialize(&block_count.as_u64()), last_block, reward_percentiles],
|
|
)
|
|
.await;
|
|
|
|
if fallback.is_err() {
|
|
// if the older fallback also resulted in an error, we return the error from the
|
|
// initial attempt
|
|
return err
|
|
}
|
|
fallback
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<P: JsonRpcClient> Provider<P> {
|
|
async fn query_resolver<T: Detokenize>(
|
|
&self,
|
|
param: ParamType,
|
|
ens_name: &str,
|
|
selector: Selector,
|
|
) -> Result<T, ProviderError> {
|
|
self.query_resolver_parameters(param, ens_name, selector, None).await
|
|
}
|
|
|
|
async fn query_resolver_parameters<T: Detokenize>(
|
|
&self,
|
|
param: ParamType,
|
|
ens_name: &str,
|
|
selector: Selector,
|
|
parameters: Option<&[u8]>,
|
|
) -> Result<T, ProviderError> {
|
|
// Get the ENS address, prioritize the local override variable
|
|
let ens_addr = self.ens.unwrap_or(ens::ENS_ADDRESS);
|
|
|
|
// first get the resolver responsible for this name
|
|
// the call will return a Bytes array which we convert to an address
|
|
let data = self.call(&ens::get_resolver(ens_addr, ens_name).into(), None).await?;
|
|
|
|
// otherwise, decode_bytes panics
|
|
if data.0.is_empty() {
|
|
return Err(ProviderError::EnsError(ens_name.to_string()))
|
|
}
|
|
|
|
let resolver_address: Address = decode_bytes(ParamType::Address, data);
|
|
if resolver_address == Address::zero() {
|
|
return Err(ProviderError::EnsError(ens_name.to_string()))
|
|
}
|
|
|
|
if let ParamType::Address = param {
|
|
// Reverse resolver reverts when calling `supportsInterface(bytes4)`
|
|
self.validate_resolver(resolver_address, selector, ens_name).await?;
|
|
}
|
|
|
|
// resolve
|
|
let data = self
|
|
.call(&ens::resolve(resolver_address, selector, ens_name, parameters).into(), None)
|
|
.await?;
|
|
|
|
Ok(decode_bytes(param, data))
|
|
}
|
|
|
|
/// Validates that the resolver supports `selector`.
|
|
async fn validate_resolver(
|
|
&self,
|
|
resolver_address: Address,
|
|
selector: Selector,
|
|
ens_name: &str,
|
|
) -> Result<(), ProviderError> {
|
|
let data =
|
|
self.call(&ens::supports_interface(resolver_address, selector).into(), None).await?;
|
|
|
|
if data.is_empty() {
|
|
return Err(ProviderError::EnsError(format!(
|
|
"`{}` resolver ({:?}) is invalid.",
|
|
ens_name, resolver_address
|
|
)))
|
|
}
|
|
|
|
let supports_selector = abi::decode(&[ParamType::Bool], data.as_ref())
|
|
.map(|token| token[0].clone().into_bool().unwrap_or_default())
|
|
.unwrap_or_default();
|
|
|
|
if !supports_selector {
|
|
return Err(ProviderError::EnsError(format!(
|
|
"`{}` resolver ({:?}) does not support selector {}.",
|
|
ens_name,
|
|
resolver_address,
|
|
hex::encode(selector)
|
|
)))
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
/// Anvil and Ganache-only function for mining empty blocks
|
|
pub async fn mine(&self, num_blocks: usize) -> Result<(), ProviderError> {
|
|
for _ in 0..num_blocks {
|
|
self.inner.request::<_, U256>("evm_mine", None::<()>).await.map_err(Into::into)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets the ENS Address (default: mainnet)
|
|
#[must_use]
|
|
pub fn ens<T: Into<Address>>(mut self, ens: T) -> Self {
|
|
self.ens = Some(ens.into());
|
|
self
|
|
}
|
|
|
|
/// Sets the default polling interval for event filters and pending transactions
|
|
/// (default: 7 seconds)
|
|
pub fn set_interval<T: Into<Duration>>(&mut self, interval: T) -> &mut Self {
|
|
self.interval = Some(interval.into());
|
|
self
|
|
}
|
|
|
|
/// Sets the default polling interval for event filters and pending transactions
|
|
/// (default: 7 seconds)
|
|
#[must_use]
|
|
pub fn interval<T: Into<Duration>>(mut self, interval: T) -> Self {
|
|
self.set_interval(interval);
|
|
self
|
|
}
|
|
|
|
/// Gets the polling interval which the provider currently uses for event filters
|
|
/// and pending transactions (default: 7 seconds)
|
|
pub fn get_interval(&self) -> Duration {
|
|
self.interval.unwrap_or(DEFAULT_POLL_INTERVAL)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "ws")]
|
|
impl Provider<crate::Ws> {
|
|
/// Direct connection to a websocket endpoint
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub async fn connect(
|
|
url: impl tokio_tungstenite::tungstenite::client::IntoClientRequest + Unpin,
|
|
) -> Result<Self, ProviderError> {
|
|
let ws = crate::Ws::connect(url).await?;
|
|
Ok(Self::new(ws))
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub async fn connect_with_auth(
|
|
url: impl tokio_tungstenite::tungstenite::client::IntoClientRequest + Unpin,
|
|
auth: Authorization,
|
|
) -> Result<Self, ProviderError> {
|
|
let ws = crate::Ws::connect_with_auth(url, auth).await?;
|
|
Ok(Self::new(ws))
|
|
}
|
|
|
|
/// Direct connection to a websocket endpoint
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub async fn connect(url: &str) -> Result<Self, ProviderError> {
|
|
let ws = crate::Ws::connect(url).await?;
|
|
Ok(Self::new(ws))
|
|
}
|
|
}
|
|
|
|
#[cfg(all(target_family = "unix", feature = "ipc"))]
|
|
impl Provider<crate::Ipc> {
|
|
/// Direct connection to an IPC socket.
|
|
pub async fn connect_ipc(path: impl AsRef<std::path::Path>) -> Result<Self, ProviderError> {
|
|
let ipc = crate::Ipc::connect(path).await?;
|
|
Ok(Self::new(ipc))
|
|
}
|
|
}
|
|
|
|
impl Provider<HttpProvider> {
|
|
/// The Url to which requests are made
|
|
pub fn url(&self) -> &Url {
|
|
self.inner.url()
|
|
}
|
|
|
|
/// Mutable access to the Url to which requests are made
|
|
pub fn url_mut(&mut self) -> &mut Url {
|
|
self.inner.url_mut()
|
|
}
|
|
}
|
|
|
|
impl<Read, Write> Provider<RwClient<Read, Write>>
|
|
where
|
|
Read: JsonRpcClient + 'static,
|
|
<Read as JsonRpcClient>::Error: Sync + Send + 'static,
|
|
Write: JsonRpcClient + 'static,
|
|
<Write as JsonRpcClient>::Error: Sync + Send + 'static,
|
|
{
|
|
/// Creates a new [Provider] with a [RwClient]
|
|
pub fn rw(r: Read, w: Write) -> Self {
|
|
Self::new(RwClient::new(r, w))
|
|
}
|
|
}
|
|
|
|
impl<T: JsonRpcClientWrapper> Provider<QuorumProvider<T>> {
|
|
/// Provider that uses a quorum
|
|
pub fn quorum(inner: QuorumProvider<T>) -> Self {
|
|
Self::new(inner)
|
|
}
|
|
}
|
|
|
|
impl Provider<MockProvider> {
|
|
/// Returns a `Provider` instantiated with an internal "mock" transport.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// use ethers_core::types::U64;
|
|
/// use ethers_providers::{Middleware, Provider};
|
|
/// // Instantiate the provider
|
|
/// let (provider, mock) = Provider::mocked();
|
|
/// // Push the mock response
|
|
/// mock.push(U64::from(12))?;
|
|
/// // Make the call
|
|
/// let blk = provider.get_block_number().await.unwrap();
|
|
/// // The response matches
|
|
/// assert_eq!(blk.as_u64(), 12);
|
|
/// // and the request as well!
|
|
/// mock.assert_request("eth_blockNumber", ()).unwrap();
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn mocked() -> (Self, MockProvider) {
|
|
let mock = MockProvider::new();
|
|
let mock_clone = mock.clone();
|
|
(Self::new(mock), mock_clone)
|
|
}
|
|
}
|
|
|
|
/// infallible conversion of Bytes to Address/String
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// If the provided bytes were not an interpretation of an address
|
|
fn decode_bytes<T: Detokenize>(param: ParamType, bytes: Bytes) -> T {
|
|
let tokens = abi::decode(&[param], bytes.as_ref())
|
|
.expect("could not abi-decode bytes to address tokens");
|
|
T::from_tokens(tokens).expect("could not parse tokens as address")
|
|
}
|
|
|
|
impl TryFrom<&str> for Provider<HttpProvider> {
|
|
type Error = ParseError;
|
|
|
|
fn try_from(src: &str) -> Result<Self, Self::Error> {
|
|
Ok(Provider::new(HttpProvider::new(Url::parse(src)?)))
|
|
}
|
|
}
|
|
|
|
impl TryFrom<String> for Provider<HttpProvider> {
|
|
type Error = ParseError;
|
|
|
|
fn try_from(src: String) -> Result<Self, Self::Error> {
|
|
Provider::try_from(src.as_str())
|
|
}
|
|
}
|
|
|
|
impl<'a> TryFrom<&'a String> for Provider<HttpProvider> {
|
|
type Error = ParseError;
|
|
|
|
fn try_from(src: &'a String) -> Result<Self, Self::Error> {
|
|
Provider::try_from(src.as_str())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
impl Provider<RetryClient<HttpProvider>> {
|
|
pub fn new_client(src: &str, max_retry: u32, initial_backoff: u64) -> Result<Self, ParseError> {
|
|
Ok(Provider::new(RetryClient::new(
|
|
HttpProvider::new(Url::parse(src)?),
|
|
Box::new(HttpRateLimitRetryPolicy),
|
|
max_retry,
|
|
initial_backoff,
|
|
)))
|
|
}
|
|
}
|
|
|
|
mod sealed {
|
|
use crate::{Http, Provider};
|
|
/// private trait to ensure extension trait is not implement outside of this crate
|
|
pub trait Sealed {}
|
|
impl Sealed for Provider<Http> {}
|
|
}
|
|
|
|
/// Extension trait for `Provider`
|
|
///
|
|
/// **Note**: this is currently sealed until <https://github.com/gakonst/ethers-rs/pull/1267> is finalized
|
|
///
|
|
/// # Example
|
|
///
|
|
/// Automatically configure poll interval via `eth_getChainId`
|
|
///
|
|
/// Note that this will send an RPC to retrieve the chain id.
|
|
///
|
|
/// ```
|
|
/// # use ethers_providers::{Http, Provider, ProviderExt};
|
|
/// # async fn t() {
|
|
/// let http_provider = Provider::<Http>::connect("https://eth-mainnet.alchemyapi.io/v2/API_KEY").await;
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// This is essentially short for
|
|
///
|
|
/// ```
|
|
/// use std::convert::TryFrom;
|
|
/// use ethers_core::types::Chain;
|
|
/// use ethers_providers::{Http, Provider, ProviderExt};
|
|
/// let http_provider = Provider::<Http>::try_from("https://eth-mainnet.alchemyapi.io/v2/API_KEY").unwrap().set_chain(Chain::Mainnet);
|
|
/// ```
|
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
|
pub trait ProviderExt: sealed::Sealed {
|
|
/// The error type that can occur when creating a provider
|
|
type Error: Debug;
|
|
|
|
/// Creates a new instance connected to the given `url`, exit on error
|
|
async fn connect(url: &str) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
Self::try_connect(url).await.unwrap()
|
|
}
|
|
|
|
/// Try to create a new `Provider`
|
|
async fn try_connect(url: &str) -> Result<Self, Self::Error>
|
|
where
|
|
Self: Sized;
|
|
|
|
/// Customize `Provider` settings for chain.
|
|
///
|
|
/// E.g. [`Chain::average_blocktime_hint()`] returns the average block time which can be used to
|
|
/// tune the polling interval.
|
|
///
|
|
/// Returns the customized `Provider`
|
|
fn for_chain(mut self, chain: impl Into<Chain>) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.set_chain(chain);
|
|
self
|
|
}
|
|
|
|
/// Customized `Provider` settings for chain
|
|
fn set_chain(&mut self, chain: impl Into<Chain>) -> &mut Self;
|
|
}
|
|
|
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
|
impl ProviderExt for Provider<HttpProvider> {
|
|
type Error = ParseError;
|
|
|
|
async fn try_connect(url: &str) -> Result<Self, Self::Error>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
let mut provider = Provider::try_from(url)?;
|
|
if is_local_endpoint(url) {
|
|
provider.set_interval(DEFAULT_LOCAL_POLL_INTERVAL);
|
|
} else if let Some(chain) =
|
|
provider.get_chainid().await.ok().and_then(|id| Chain::try_from(id).ok())
|
|
{
|
|
provider.set_chain(chain);
|
|
}
|
|
|
|
Ok(provider)
|
|
}
|
|
|
|
fn set_chain(&mut self, chain: impl Into<Chain>) -> &mut Self {
|
|
let chain = chain.into();
|
|
if let Some(blocktime) = chain.average_blocktime_hint() {
|
|
// use half of the block time
|
|
self.set_interval(blocktime / 2);
|
|
}
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Returns true if the endpoint is local
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// use ethers_providers::is_local_endpoint;
|
|
/// assert!(is_local_endpoint("http://localhost:8545"));
|
|
/// assert!(is_local_endpoint("http://127.0.0.1:8545"));
|
|
/// ```
|
|
#[inline]
|
|
pub fn is_local_endpoint(url: &str) -> bool {
|
|
url.contains("127.0.0.1") || url.contains("localhost")
|
|
}
|
|
|
|
/// A middleware supporting development-specific JSON RPC methods
|
|
///
|
|
/// # Example
|
|
///
|
|
///```
|
|
/// use ethers_providers::{Provider, Http, Middleware, DevRpcMiddleware};
|
|
/// use ethers_core::types::TransactionRequest;
|
|
/// use ethers_core::utils::Anvil;
|
|
/// use std::convert::TryFrom;
|
|
///
|
|
/// # #[tokio::main(flavor = "current_thread")]
|
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let anvil = Anvil::new().spawn();
|
|
/// let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
|
|
/// let client = DevRpcMiddleware::new(provider);
|
|
///
|
|
/// // snapshot the initial state
|
|
/// let block0 = client.get_block_number().await.unwrap();
|
|
/// let snap_id = client.snapshot().await.unwrap();
|
|
///
|
|
/// // send a transaction
|
|
/// let accounts = client.get_accounts().await?;
|
|
/// let from = accounts[0];
|
|
/// let to = accounts[1];
|
|
/// let balance_before = client.get_balance(to, None).await?;
|
|
/// let tx = TransactionRequest::new().to(to).value(1000).from(from);
|
|
/// client.send_transaction(tx, None).await?.await?;
|
|
/// let balance_after = client.get_balance(to, None).await?;
|
|
/// assert_eq!(balance_after, balance_before + 1000);
|
|
///
|
|
/// // revert to snapshot
|
|
/// client.revert_to_snapshot(snap_id).await.unwrap();
|
|
/// let balance_after_revert = client.get_balance(to, None).await?;
|
|
/// assert_eq!(balance_after_revert, balance_before);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[cfg(feature = "dev-rpc")]
|
|
pub mod dev_rpc {
|
|
use crate::{FromErr, Middleware, ProviderError};
|
|
use async_trait::async_trait;
|
|
use ethers_core::types::U256;
|
|
use thiserror::Error;
|
|
|
|
use std::fmt::Debug;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct DevRpcMiddleware<M>(M);
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum DevRpcMiddlewareError<M: Middleware> {
|
|
#[error("{0}")]
|
|
MiddlewareError(M::Error),
|
|
|
|
#[error("{0}")]
|
|
ProviderError(ProviderError),
|
|
|
|
#[error("Could not revert to snapshot")]
|
|
NoSnapshot,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<M: Middleware> Middleware for DevRpcMiddleware<M> {
|
|
type Error = DevRpcMiddlewareError<M>;
|
|
type Provider = M::Provider;
|
|
type Inner = M;
|
|
|
|
fn inner(&self) -> &M {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl<M: Middleware> FromErr<M::Error> for DevRpcMiddlewareError<M> {
|
|
fn from(src: M::Error) -> DevRpcMiddlewareError<M> {
|
|
DevRpcMiddlewareError::MiddlewareError(src)
|
|
}
|
|
}
|
|
|
|
impl<M> From<ProviderError> for DevRpcMiddlewareError<M>
|
|
where
|
|
M: Middleware,
|
|
{
|
|
fn from(src: ProviderError) -> Self {
|
|
Self::ProviderError(src)
|
|
}
|
|
}
|
|
|
|
impl<M: Middleware> DevRpcMiddleware<M> {
|
|
pub fn new(inner: M) -> Self {
|
|
Self(inner)
|
|
}
|
|
|
|
// Ganache, Hardhat and Anvil increment snapshot ID even if no state has changed
|
|
pub async fn snapshot(&self) -> Result<U256, DevRpcMiddlewareError<M>> {
|
|
self.provider().request::<(), U256>("evm_snapshot", ()).await.map_err(From::from)
|
|
}
|
|
|
|
pub async fn revert_to_snapshot(&self, id: U256) -> Result<(), DevRpcMiddlewareError<M>> {
|
|
let ok = self
|
|
.provider()
|
|
.request::<[U256; 1], bool>("evm_revert", [id])
|
|
.await
|
|
.map_err(DevRpcMiddlewareError::ProviderError)?;
|
|
if ok {
|
|
Ok(())
|
|
} else {
|
|
Err(DevRpcMiddlewareError::NoSnapshot)
|
|
}
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
// Celo blocks can not get parsed when used with Ganache
|
|
#[cfg(not(feature = "celo"))]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{Http, Provider};
|
|
use ethers_core::utils::Anvil;
|
|
use std::convert::TryFrom;
|
|
|
|
#[tokio::test]
|
|
async fn test_snapshot() {
|
|
let anvil = Anvil::new().spawn();
|
|
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
|
|
let client = DevRpcMiddleware::new(provider);
|
|
|
|
// snapshot initial state
|
|
let block0 = client.get_block_number().await.unwrap();
|
|
let time0 = client.get_block(block0).await.unwrap().unwrap().timestamp;
|
|
let snap_id0 = client.snapshot().await.unwrap();
|
|
|
|
// mine a new block
|
|
client.provider().mine(1).await.unwrap();
|
|
|
|
// snapshot state
|
|
let block1 = client.get_block_number().await.unwrap();
|
|
let time1 = client.get_block(block1).await.unwrap().unwrap().timestamp;
|
|
let snap_id1 = client.snapshot().await.unwrap();
|
|
|
|
// mine some blocks
|
|
client.provider().mine(5).await.unwrap();
|
|
|
|
// snapshot state
|
|
let block2 = client.get_block_number().await.unwrap();
|
|
let time2 = client.get_block(block2).await.unwrap().unwrap().timestamp;
|
|
let snap_id2 = client.snapshot().await.unwrap();
|
|
|
|
// mine some blocks
|
|
client.provider().mine(5).await.unwrap();
|
|
|
|
// revert_to_snapshot should reset state to snap id
|
|
client.revert_to_snapshot(snap_id2).await.unwrap();
|
|
let block = client.get_block_number().await.unwrap();
|
|
let time = client.get_block(block).await.unwrap().unwrap().timestamp;
|
|
assert_eq!(block, block2);
|
|
assert_eq!(time, time2);
|
|
|
|
client.revert_to_snapshot(snap_id1).await.unwrap();
|
|
let block = client.get_block_number().await.unwrap();
|
|
let time = client.get_block(block).await.unwrap().unwrap().timestamp;
|
|
assert_eq!(block, block1);
|
|
assert_eq!(time, time1);
|
|
|
|
// revert_to_snapshot should throw given non-existent or
|
|
// previously used snapshot
|
|
let result = client.revert_to_snapshot(snap_id1).await;
|
|
assert!(result.is_err());
|
|
|
|
client.revert_to_snapshot(snap_id0).await.unwrap();
|
|
let block = client.get_block_number().await.unwrap();
|
|
let time = client.get_block(block).await.unwrap().unwrap().timestamp;
|
|
assert_eq!(block, block0);
|
|
assert_eq!(time, time0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
mod tests {
|
|
use std::path::PathBuf;
|
|
|
|
use super::*;
|
|
use crate::Http;
|
|
use ethers_core::{
|
|
types::{
|
|
transaction::eip2930::AccessList, Eip1559TransactionRequest, TransactionRequest, H256,
|
|
},
|
|
utils::{Anvil, Genesis, Geth, GethInstance},
|
|
};
|
|
use futures_util::StreamExt;
|
|
|
|
#[test]
|
|
fn convert_h256_u256_quantity() {
|
|
let hash: H256 = H256::zero();
|
|
let quantity = U256::from_big_endian(hash.as_bytes());
|
|
assert_eq!(format!("{quantity:#x}"), "0x0");
|
|
assert_eq!(utils::serialize(&quantity).to_string(), "\"0x0\"");
|
|
|
|
let address: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse().unwrap();
|
|
let block = BlockNumber::Latest;
|
|
let params =
|
|
[utils::serialize(&address), utils::serialize(&quantity), utils::serialize(&block)];
|
|
|
|
let params = serde_json::to_string(¶ms).unwrap();
|
|
assert_eq!(params, r#"["0x295a70b2de5e3953354a6a8344e616ed314d7251","0x0","latest"]"#);
|
|
}
|
|
|
|
#[tokio::test]
|
|
// Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2
|
|
async fn mainnet_resolve_name() {
|
|
let provider = crate::test_provider::MAINNET.provider();
|
|
|
|
let addr = provider.resolve_name("registrar.firefly.eth").await.unwrap();
|
|
assert_eq!(addr, "6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap());
|
|
|
|
// registrar not found
|
|
provider.resolve_name("asdfasdffads").await.unwrap_err();
|
|
|
|
// name not found
|
|
provider.resolve_name("asdfasdf.registrar.firefly.eth").await.unwrap_err();
|
|
}
|
|
|
|
#[tokio::test]
|
|
// Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2
|
|
async fn mainnet_lookup_address() {
|
|
let provider = crate::MAINNET.provider();
|
|
|
|
let name = provider
|
|
.lookup_address("6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(name, "registrar.firefly.eth");
|
|
|
|
provider
|
|
.lookup_address("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".parse().unwrap())
|
|
.await
|
|
.unwrap_err();
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore]
|
|
async fn mainnet_resolve_avatar() {
|
|
let provider = crate::MAINNET.provider();
|
|
|
|
for (ens_name, res) in &[
|
|
// HTTPS
|
|
("alisha.eth", "https://ipfs.io/ipfs/QmeQm91kAdPGnUKsE74WvkqYKUeHvc2oHd2FW11V3TrqkQ"),
|
|
// ERC-1155
|
|
("nick.eth", "https://img.seadn.io/files/3ae7be6c41ad4767bf3ecbc0493b4bfb.png"),
|
|
// HTTPS
|
|
("parishilton.eth", "https://i.imgur.com/YW3Hzph.jpg"),
|
|
// ERC-721 with IPFS link
|
|
("ikehaya-nft.eth", "https://ipfs.io/ipfs/QmdKkwCE8uVhgYd7tWBfhtHdQZDnbNukWJ8bvQmR6nZKsk"),
|
|
// ERC-1155 with IPFS link
|
|
("vitalik.eth", "https://ipfs.io/ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif"),
|
|
// IPFS
|
|
("cdixon.eth", "https://ipfs.io/ipfs/QmYA6ZpEARgHvRHZQdFPynMMX8NtdL2JCadvyuyG2oA88u"),
|
|
("0age.eth", "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOmJsYWNrIiB2aWV3Qm94PSIwIDAgNTAwIDUwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNTUiIHk9IjYwIiB3aWR0aD0iMTkwIiBoZWlnaHQ9IjM5MCIgZmlsbD0iIzY5ZmYzNyIvPjwvc3ZnPg==")
|
|
] {
|
|
println!("Resolving: {ens_name}");
|
|
assert_eq!(provider.resolve_avatar(ens_name).await.unwrap(), Url::parse(res).unwrap());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg_attr(feature = "celo", ignore)]
|
|
async fn test_new_block_filter() {
|
|
let num_blocks = 3;
|
|
let geth = Anvil::new().block_time(2u64).spawn();
|
|
let provider = Provider::<Http>::try_from(geth.endpoint())
|
|
.unwrap()
|
|
.interval(Duration::from_millis(1000));
|
|
|
|
let start_block = provider.get_block_number().await.unwrap();
|
|
|
|
let stream = provider.watch_blocks().await.unwrap().stream();
|
|
|
|
let hashes: Vec<H256> = stream.take(num_blocks).collect::<Vec<H256>>().await;
|
|
for (i, hash) in hashes.iter().enumerate() {
|
|
let block = provider.get_block(start_block + i as u64 + 1).await.unwrap().unwrap();
|
|
assert_eq!(*hash, block.hash.unwrap());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg_attr(feature = "celo", ignore)]
|
|
async fn test_is_signer() {
|
|
use ethers_core::utils::Anvil;
|
|
use std::str::FromStr;
|
|
|
|
let anvil = Anvil::new().spawn();
|
|
let provider =
|
|
Provider::<Http>::try_from(anvil.endpoint()).unwrap().with_sender(anvil.addresses()[0]);
|
|
assert!(provider.is_signer().await);
|
|
|
|
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
|
|
assert!(!provider.is_signer().await);
|
|
|
|
let sender = Address::from_str("635B4764D1939DfAcD3a8014726159abC277BecC")
|
|
.expect("should be able to parse hex address");
|
|
let provider = Provider::<Http>::try_from(
|
|
"https://ropsten.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778",
|
|
)
|
|
.unwrap()
|
|
.with_sender(sender);
|
|
assert!(!provider.is_signer().await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_new_pending_txs_filter() {
|
|
let num_txs = 5;
|
|
|
|
let geth = Anvil::new().block_time(2u64).spawn();
|
|
let provider = Provider::<Http>::try_from(geth.endpoint())
|
|
.unwrap()
|
|
.interval(Duration::from_millis(1000));
|
|
let accounts = provider.get_accounts().await.unwrap();
|
|
|
|
let stream = provider.watch_pending_transactions().await.unwrap().stream();
|
|
|
|
let mut tx_hashes = Vec::new();
|
|
let tx = TransactionRequest::new().from(accounts[0]).to(accounts[0]).value(1e18 as u64);
|
|
|
|
for _ in 0..num_txs {
|
|
tx_hashes.push(provider.send_transaction(tx.clone(), None).await.unwrap());
|
|
}
|
|
|
|
let hashes: Vec<H256> = stream.take(num_txs).collect::<Vec<H256>>().await;
|
|
assert_eq!(tx_hashes, hashes);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn receipt_on_unmined_tx() {
|
|
use ethers_core::{
|
|
types::TransactionRequest,
|
|
utils::{parse_ether, Anvil},
|
|
};
|
|
let anvil = Anvil::new().block_time(2u64).spawn();
|
|
let provider = Provider::<Http>::try_from(anvil.endpoint()).unwrap();
|
|
|
|
let accounts = provider.get_accounts().await.unwrap();
|
|
let tx = TransactionRequest::pay(accounts[0], parse_ether(1u64).unwrap()).from(accounts[0]);
|
|
let pending_tx = provider.send_transaction(tx, None).await.unwrap();
|
|
|
|
assert!(provider.get_transaction_receipt(*pending_tx).await.unwrap().is_none());
|
|
|
|
let hash = *pending_tx;
|
|
let receipt = pending_tx.await.unwrap().unwrap();
|
|
assert_eq!(receipt.transaction_hash, hash);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn parity_block_receipts() {
|
|
let url = match std::env::var("PARITY") {
|
|
Ok(inner) => inner,
|
|
_ => return,
|
|
};
|
|
let provider = Provider::<Http>::try_from(url.as_str()).unwrap();
|
|
let receipts = provider.parity_block_receipts(10657200).await.unwrap();
|
|
assert!(!receipts.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
// Celo blocks can not get parsed when used with Ganache
|
|
#[cfg(not(feature = "celo"))]
|
|
async fn block_subscribe() {
|
|
use ethers_core::utils::Anvil;
|
|
use futures_util::StreamExt;
|
|
let anvil = Anvil::new().block_time(2u64).spawn();
|
|
let provider = Provider::connect(anvil.ws_endpoint()).await.unwrap();
|
|
|
|
let stream = provider.subscribe_blocks().await.unwrap();
|
|
let blocks = stream.take(3).map(|x| x.number.unwrap().as_u64()).collect::<Vec<_>>().await;
|
|
assert_eq!(blocks, vec![1, 2, 3]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg_attr(feature = "celo", ignore)]
|
|
async fn fee_history() {
|
|
let provider = Provider::<Http>::try_from(
|
|
"https://goerli.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778",
|
|
)
|
|
.unwrap();
|
|
|
|
let history =
|
|
provider.fee_history(10u64, BlockNumber::Latest, &[10.0, 40.0]).await.unwrap();
|
|
dbg!(&history);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore]
|
|
#[cfg(feature = "ws")]
|
|
async fn test_trace_call_many() {
|
|
use ethers_core::types::H160;
|
|
|
|
// TODO: Implement ErigonInstance, so it'd be possible to test this.
|
|
let provider = Provider::new(crate::Ws::connect("ws://127.0.0.1:8545").await.unwrap());
|
|
let traces = provider
|
|
.trace_call_many(
|
|
vec![
|
|
(
|
|
TransactionRequest::new()
|
|
.from(Address::zero())
|
|
.to("0x0000000000000000000000000000000000000001"
|
|
.parse::<H160>()
|
|
.unwrap())
|
|
.value(U256::from(10000000000000000u128)),
|
|
vec![TraceType::StateDiff],
|
|
),
|
|
(
|
|
TransactionRequest::new()
|
|
.from(
|
|
"0x0000000000000000000000000000000000000001"
|
|
.parse::<H160>()
|
|
.unwrap(),
|
|
)
|
|
.to("0x0000000000000000000000000000000000000002"
|
|
.parse::<H160>()
|
|
.unwrap())
|
|
.value(U256::from(10000000000000000u128)),
|
|
vec![TraceType::StateDiff],
|
|
),
|
|
],
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
dbg!(traces);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_fill_transaction_1559() {
|
|
let (mut provider, mock) = Provider::mocked();
|
|
provider.from = Some("0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap());
|
|
|
|
let gas = U256::from(21000_usize);
|
|
let max_fee = U256::from(25_usize);
|
|
let prio_fee = U256::from(25_usize);
|
|
let access_list: AccessList = vec![Default::default()].into();
|
|
|
|
// --- leaves a filled 1559 transaction unchanged, making no requests
|
|
let from: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
|
|
let to: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
|
|
let mut tx = Eip1559TransactionRequest::new()
|
|
.from(from)
|
|
.to(to)
|
|
.gas(gas)
|
|
.max_fee_per_gas(max_fee)
|
|
.max_priority_fee_per_gas(prio_fee)
|
|
.access_list(access_list.clone())
|
|
.into();
|
|
provider.fill_transaction(&mut tx, None).await.unwrap();
|
|
|
|
assert_eq!(tx.from(), Some(&from));
|
|
assert_eq!(tx.to(), Some(&to.into()));
|
|
assert_eq!(tx.gas(), Some(&gas));
|
|
assert_eq!(tx.gas_price(), Some(max_fee));
|
|
assert_eq!(tx.access_list(), Some(&access_list));
|
|
|
|
// --- fills a 1559 transaction, leaving the existing gas limit unchanged,
|
|
// without generating an access-list
|
|
let mut tx = Eip1559TransactionRequest::new()
|
|
.gas(gas)
|
|
.max_fee_per_gas(max_fee)
|
|
.max_priority_fee_per_gas(prio_fee)
|
|
.into();
|
|
|
|
provider.fill_transaction(&mut tx, None).await.unwrap();
|
|
|
|
assert_eq!(tx.from(), provider.from.as_ref());
|
|
assert!(tx.to().is_none());
|
|
assert_eq!(tx.gas(), Some(&gas));
|
|
assert_eq!(tx.access_list(), Some(&Default::default()));
|
|
|
|
// --- fills a 1559 transaction, using estimated gas
|
|
let mut tx = Eip1559TransactionRequest::new()
|
|
.max_fee_per_gas(max_fee)
|
|
.max_priority_fee_per_gas(prio_fee)
|
|
.into();
|
|
|
|
mock.push(gas).unwrap();
|
|
|
|
provider.fill_transaction(&mut tx, None).await.unwrap();
|
|
|
|
assert_eq!(tx.from(), provider.from.as_ref());
|
|
assert!(tx.to().is_none());
|
|
assert_eq!(tx.gas(), Some(&gas));
|
|
assert_eq!(tx.access_list(), Some(&Default::default()));
|
|
|
|
// --- propogates estimate_gas() error
|
|
let mut tx = Eip1559TransactionRequest::new()
|
|
.max_fee_per_gas(max_fee)
|
|
.max_priority_fee_per_gas(prio_fee)
|
|
.into();
|
|
|
|
// bad mock value causes error response for eth_estimateGas
|
|
mock.push(b'b').unwrap();
|
|
|
|
let res = provider.fill_transaction(&mut tx, None).await;
|
|
|
|
assert!(matches!(res, Err(ProviderError::JsonRpcClientError(_))));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_fill_transaction_legacy() {
|
|
let (mut provider, mock) = Provider::mocked();
|
|
provider.from = Some("0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap());
|
|
|
|
let gas = U256::from(21000_usize);
|
|
let gas_price = U256::from(50_usize);
|
|
|
|
// --- leaves a filled legacy transaction unchanged, making no requests
|
|
let from: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
|
|
let to: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
|
|
let mut tx =
|
|
TransactionRequest::new().from(from).to(to).gas(gas).gas_price(gas_price).into();
|
|
provider.fill_transaction(&mut tx, None).await.unwrap();
|
|
|
|
assert_eq!(tx.from(), Some(&from));
|
|
assert_eq!(tx.to(), Some(&to.into()));
|
|
assert_eq!(tx.gas(), Some(&gas));
|
|
assert_eq!(tx.gas_price(), Some(gas_price));
|
|
assert!(tx.access_list().is_none());
|
|
|
|
// --- fills an empty legacy transaction
|
|
let mut tx = TransactionRequest::new().into();
|
|
mock.push(gas).unwrap();
|
|
mock.push(gas_price).unwrap();
|
|
provider.fill_transaction(&mut tx, None).await.unwrap();
|
|
|
|
assert_eq!(tx.from(), provider.from.as_ref());
|
|
assert!(tx.to().is_none());
|
|
assert_eq!(tx.gas(), Some(&gas));
|
|
assert_eq!(tx.gas_price(), Some(gas_price));
|
|
assert!(tx.access_list().is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mainnet_lookup_address_invalid_resolver() {
|
|
let provider = crate::MAINNET.provider();
|
|
|
|
let err = provider
|
|
.lookup_address("0x30c9223d9e3d23e0af1073a38e0834b055bf68ed".parse().unwrap())
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert_eq!(
|
|
&err.to_string(),
|
|
"ens name not found: `ox63616e.eth` resolver (0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) is invalid."
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn geth_admin_nodeinfo() {
|
|
// we can't use the test provider because infura does not expose admin endpoints
|
|
let port = 8546u16;
|
|
let p2p_listener_port = 13337u16;
|
|
let authrpc_port = 8552u16;
|
|
let network = 1337u64;
|
|
let temp_dir = tempfile::tempdir().unwrap().into_path();
|
|
|
|
let (geth, provider) = spawn_geth_and_create_provider(
|
|
network,
|
|
port,
|
|
p2p_listener_port,
|
|
authrpc_port,
|
|
Some(temp_dir),
|
|
None,
|
|
);
|
|
|
|
let info = provider.node_info().await.unwrap();
|
|
drop(geth);
|
|
|
|
// check that the port we set works
|
|
assert_eq!(info.ports.listener, p2p_listener_port);
|
|
|
|
// make sure it is running eth
|
|
assert!(info.protocols.eth.is_some());
|
|
|
|
// check that the network id is correct
|
|
assert_eq!(info.protocols.eth.unwrap().network, network);
|
|
}
|
|
|
|
/// Spawn a new `GethInstance` without discovery and crate a `Provider` for it.
|
|
///
|
|
/// These will all use the same genesis config.
|
|
fn spawn_geth_and_create_provider(
|
|
chain_id: u64,
|
|
rpc_port: u16,
|
|
p2p_port: u16,
|
|
authrpc_port: u16,
|
|
datadir: Option<PathBuf>,
|
|
genesis: Option<Genesis>,
|
|
) -> (GethInstance, Provider<HttpProvider>) {
|
|
let geth = Geth::new()
|
|
.port(rpc_port)
|
|
.p2p_port(p2p_port)
|
|
.authrpc_port(authrpc_port)
|
|
.chain_id(chain_id)
|
|
.disable_discovery();
|
|
|
|
let geth = match genesis {
|
|
Some(genesis) => geth.genesis(genesis),
|
|
None => geth,
|
|
};
|
|
|
|
let geth = match datadir {
|
|
Some(dir) => geth.data_dir(dir),
|
|
None => geth,
|
|
}
|
|
.spawn();
|
|
|
|
let url = format!("http://127.0.0.1:{}", rpc_port);
|
|
let provider = Provider::try_from(url).unwrap();
|
|
(geth, provider)
|
|
}
|
|
|
|
/// Spawn a set of [`GethInstance`]s with the list of given data directories and [`Provider`]s
|
|
/// for those [`GethInstance`]s without discovery, setting sequential ports for their p2p, rpc,
|
|
/// and authrpc ports.
|
|
fn spawn_geth_instances(
|
|
datadirs: Vec<PathBuf>,
|
|
chain_id: u64,
|
|
genesis: Option<Genesis>,
|
|
) -> Vec<(GethInstance, Provider<HttpProvider>)> {
|
|
let mut geths = Vec::new();
|
|
let mut p2p_port = 30303;
|
|
let mut rpc_port = 8545;
|
|
let mut authrpc_port = 8551;
|
|
|
|
for dir in datadirs {
|
|
let (geth, provider) = spawn_geth_and_create_provider(
|
|
chain_id,
|
|
rpc_port,
|
|
p2p_port,
|
|
authrpc_port,
|
|
Some(dir),
|
|
genesis.clone(),
|
|
);
|
|
|
|
geths.push((geth, provider));
|
|
|
|
p2p_port += 1;
|
|
rpc_port += 1;
|
|
authrpc_port += 1;
|
|
}
|
|
|
|
geths
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_second_geth_peer() {
|
|
// init each geth directory
|
|
let dir1 = tempfile::tempdir().unwrap().into_path();
|
|
let dir2 = tempfile::tempdir().unwrap().into_path();
|
|
|
|
// use the default genesis
|
|
let genesis = utils::Genesis::default();
|
|
|
|
// spawn the geths
|
|
let mut geths = spawn_geth_instances(vec![dir1.clone(), dir2.clone()], 1337, Some(genesis));
|
|
let (mut first_geth, first_peer) = geths.pop().unwrap();
|
|
let (second_geth, second_peer) = geths.pop().unwrap();
|
|
|
|
// get nodeinfo for each geth instance
|
|
let first_info = first_peer.node_info().await.unwrap();
|
|
let second_info = second_peer.node_info().await.unwrap();
|
|
let first_port = first_info.ports.listener;
|
|
|
|
// replace the ip in the enode by putting
|
|
let first_prefix = first_info.enode.split('@').collect::<Vec<&str>>();
|
|
|
|
// create enodes for each geth instance using each id and port
|
|
let first_enode = format!("{}@localhost:{}", first_prefix.first().unwrap(), first_port);
|
|
|
|
// add the first geth as a peer for the second
|
|
let res = second_peer.add_peer(first_enode).await.unwrap();
|
|
assert!(res);
|
|
|
|
// wait on the listening peer for an incoming connection
|
|
first_geth.wait_to_add_peer(second_info.id).unwrap();
|
|
|
|
// check that second_geth exists in the first_geth peer list
|
|
let peers = first_peer.peers().await.unwrap();
|
|
|
|
drop(first_geth);
|
|
drop(second_geth);
|
|
|
|
// check that the second peer is in the list (it uses an enr so the enr should be Some)
|
|
assert_eq!(peers.len(), 1);
|
|
|
|
let peer = peers.get(0).unwrap();
|
|
assert_eq!(H256::from_str(&peer.id).unwrap(), second_info.id);
|
|
|
|
// remove directories
|
|
std::fs::remove_dir_all(dir1).unwrap();
|
|
std::fs::remove_dir_all(dir2).unwrap();
|
|
}
|
|
}
|