Geth TxPool API Support (#86)
* feat: support for txpool API * feat: add a geth spawner test helper * fix(txpool): use TxRequest instead of new data struct The `raw` field is no longer present in latest geth's response. * fix(txpool): use proper api response format ref: https://github.com/ethereum/go-ethereum/pull/21720 Also add a Geth test * ci: install geth 1.9.23 Co-authored-by: Rohit Narurkar <rohit.narurkar@protonmail.com>
This commit is contained in:
parent
938972e9cc
commit
a22f1f9aa0
|
@ -33,6 +33,16 @@ jobs:
|
||||||
export PATH=$HOME/bin:$PATH
|
export PATH=$HOME/bin:$PATH
|
||||||
solc --version
|
solc --version
|
||||||
|
|
||||||
|
- name: Install geth
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/bin"
|
||||||
|
wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||||
|
tar -xvf geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||||
|
mv geth-linux-amd64-1.9.23-8c2f2715/geth $HOME/bin/geth
|
||||||
|
chmod u+x "$HOME/bin/geth"
|
||||||
|
export PATH=$HOME/bin:$PATH
|
||||||
|
geth version
|
||||||
|
|
||||||
- name: Install stable toolchain
|
- name: Install stable toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -27,3 +27,6 @@ pub use ens::NameOrAddress;
|
||||||
|
|
||||||
mod signature;
|
mod signature;
|
||||||
pub use signature::*;
|
pub use signature::*;
|
||||||
|
|
||||||
|
mod txpool;
|
||||||
|
pub use txpool::*;
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,9 +1,11 @@
|
||||||
use crate::{types::Address, utils::secret_key_to_address};
|
use crate::{
|
||||||
|
types::Address,
|
||||||
|
utils::{secret_key_to_address, unused_port},
|
||||||
|
};
|
||||||
use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey};
|
use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey};
|
||||||
use rustc_hex::FromHex;
|
use rustc_hex::FromHex;
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufRead, BufReader},
|
io::{BufRead, BufReader},
|
||||||
net::TcpListener,
|
|
||||||
process::{Child, Command},
|
process::{Child, Command},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
@ -178,17 +180,3 @@ impl Ganache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bit of hack to find an unused TCP port.
|
|
||||||
///
|
|
||||||
/// Does not guarantee that the given port is unused after the function exists, just that it was
|
|
||||||
/// unused before the function started (i.e., it does not reserve a port).
|
|
||||||
pub fn unused_port() -> u16 {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0")
|
|
||||||
.expect("Failed to create TCP listener to find unused port");
|
|
||||||
|
|
||||||
let local_addr = listener
|
|
||||||
.local_addr()
|
|
||||||
.expect("Failed to read TCP listener local_addr to find unused port");
|
|
||||||
local_addr.port()
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
use super::unused_port;
|
||||||
|
use std::{
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
process::{Child, Command},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// How long we will wait for geth to indicate that it is ready.
|
||||||
|
const GETH_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
|
||||||
|
|
||||||
|
/// The exposed APIs
|
||||||
|
const API: &str = "eth,net,web3,txpool";
|
||||||
|
|
||||||
|
/// The geth command
|
||||||
|
const GETH: &str = "geth";
|
||||||
|
|
||||||
|
/// A geth instance. Will close the instance when dropped.
|
||||||
|
///
|
||||||
|
/// Construct this using [`Geth`](crate::utils::Geth)
|
||||||
|
pub struct GethInstance {
|
||||||
|
pid: Child,
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GethInstance {
|
||||||
|
/// Returns the port of this instance
|
||||||
|
pub fn port(&self) -> u16 {
|
||||||
|
self.port
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the HTTP endpoint of this instance
|
||||||
|
pub fn endpoint(&self) -> String {
|
||||||
|
format!("http://localhost:{}", self.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Websocket endpoint of this instance
|
||||||
|
pub fn ws_endpoint(&self) -> String {
|
||||||
|
format!("ws://localhost:{}", self.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for GethInstance {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.pid.kill().expect("could not kill geth");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for launching `geth`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If `spawn` is called without `geth` being available in the user's $PATH
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use ethers::utils::Geth;
|
||||||
|
///
|
||||||
|
/// let port = 8545u16;
|
||||||
|
/// let url = format!("http://localhost:{}", port).to_string();
|
||||||
|
///
|
||||||
|
/// let geth = Geth::new()
|
||||||
|
/// .port(port)
|
||||||
|
/// .block_time(5000u64)
|
||||||
|
/// .spawn();
|
||||||
|
///
|
||||||
|
/// drop(geth); // this will kill the instance
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct Geth {
|
||||||
|
port: Option<u16>,
|
||||||
|
block_time: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Geth {
|
||||||
|
/// Creates an empty Geth builder.
|
||||||
|
/// The default port is 8545. The mnemonic is chosen randomly.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the port which will be used when the `geth-cli` instance is launched.
|
||||||
|
pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
|
||||||
|
self.port = Some(port.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the block-time which will be used when the `geth-cli` instance is launched.
|
||||||
|
pub fn block_time<T: Into<u64>>(mut self, block_time: T) -> Self {
|
||||||
|
self.block_time = Some(block_time.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes the builder and spawns `geth` with stdout redirected
|
||||||
|
/// to /dev/null.
|
||||||
|
pub fn spawn(self) -> GethInstance {
|
||||||
|
let mut cmd = Command::new(GETH);
|
||||||
|
// geth uses stderr for its logs
|
||||||
|
cmd.stderr(std::process::Stdio::piped());
|
||||||
|
let port = if let Some(port) = self.port {
|
||||||
|
port
|
||||||
|
} else {
|
||||||
|
unused_port()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the HTTP API
|
||||||
|
cmd.arg("--http");
|
||||||
|
cmd.arg("--http.port").arg(port.to_string());
|
||||||
|
cmd.arg("--http.api").arg(API);
|
||||||
|
|
||||||
|
// Open the WS API
|
||||||
|
cmd.arg("--ws");
|
||||||
|
cmd.arg("--ws.port").arg(port.to_string());
|
||||||
|
cmd.arg("--ws.api").arg(API);
|
||||||
|
|
||||||
|
// Dev mode with custom block time
|
||||||
|
cmd.arg("--dev");
|
||||||
|
if let Some(block_time) = self.block_time {
|
||||||
|
cmd.arg("--dev.period").arg(block_time.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child = cmd.spawn().expect("couldnt start geth");
|
||||||
|
|
||||||
|
let stdout = child
|
||||||
|
.stderr
|
||||||
|
.expect("Unable to get stderr for geth child process");
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut reader = BufReader::new(stdout);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if start + Duration::from_millis(GETH_STARTUP_TIMEOUT_MILLIS) <= Instant::now() {
|
||||||
|
panic!("Timed out waiting for geth to start. Is geth installed?")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line = String::new();
|
||||||
|
reader
|
||||||
|
.read_line(&mut line)
|
||||||
|
.expect("Failed to read line from geth process");
|
||||||
|
|
||||||
|
// geth 1.9.23 uses "server started" while 1.9.18 uses "endpoint opened"
|
||||||
|
if line.contains("HTTP endpoint opened") || line.contains("HTTP server started") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
child.stderr = Some(reader.into_inner());
|
||||||
|
|
||||||
|
GethInstance { pid: child, port }
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,12 @@ mod ganache;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use ganache::{Ganache, GanacheInstance};
|
pub use ganache::{Ganache, GanacheInstance};
|
||||||
|
|
||||||
|
/// Utilities for launching a go-ethereum dev-mode instance
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
mod geth;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub use geth::{Geth, GethInstance};
|
||||||
|
|
||||||
/// Solidity compiler bindings
|
/// Solidity compiler bindings
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod solc;
|
mod solc;
|
||||||
|
@ -126,7 +132,7 @@ pub fn to_checksum(addr: &Address, chain_id: Option<u8>) -> String {
|
||||||
let addr_hex = addr_hex.as_bytes();
|
let addr_hex = addr_hex.as_bytes();
|
||||||
|
|
||||||
addr_hex
|
addr_hex
|
||||||
.into_iter()
|
.iter()
|
||||||
.zip(hash)
|
.zip(hash)
|
||||||
.fold("0x".to_owned(), |mut encoded, (addr, hash)| {
|
.fold("0x".to_owned(), |mut encoded, (addr, hash)| {
|
||||||
encoded.push(if *hash >= 56 {
|
encoded.push(if *hash >= 56 {
|
||||||
|
@ -138,6 +144,20 @@ pub fn to_checksum(addr: &Address, chain_id: Option<u8>) -> String {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A bit of hack to find an unused TCP port.
|
||||||
|
///
|
||||||
|
/// Does not guarantee that the given port is unused after the function exists, just that it was
|
||||||
|
/// unused before the function started (i.e., it does not reserve a port).
|
||||||
|
pub(crate) fn unused_port() -> u16 {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0")
|
||||||
|
.expect("Failed to create TCP listener to find unused port");
|
||||||
|
|
||||||
|
let local_addr = listener
|
||||||
|
.local_addr()
|
||||||
|
.expect("Failed to read TCP listener local_addr to find unused port");
|
||||||
|
local_addr.port()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -358,4 +358,16 @@ pub trait Middleware: Sync + Send + Debug {
|
||||||
fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, Self::Provider> {
|
fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, Self::Provider> {
|
||||||
self.inner().pending_transaction(tx_hash)
|
self.inner().pending_transaction(tx_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn txpool_content(&self) -> Result<TxpoolContent, Self::Error> {
|
||||||
|
self.inner().txpool_content().await.map_err(FromErr::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn txpool_inspect(&self) -> Result<TxpoolInspect, Self::Error> {
|
||||||
|
self.inner().txpool_inspect().await.map_err(FromErr::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn txpool_status(&self) -> Result<TxpoolStatus, Self::Error> {
|
||||||
|
self.inner().txpool_status().await.map_err(FromErr::from)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@ use ethers_core::{
|
||||||
abi::{self, Detokenize, ParamType},
|
abi::{self, Detokenize, ParamType},
|
||||||
types::{
|
types::{
|
||||||
Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, NameOrAddress, Selector,
|
Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, NameOrAddress, Selector,
|
||||||
Signature, Transaction, TransactionReceipt, TransactionRequest, TxHash, H256, U256, U64,
|
Signature, Transaction, TransactionReceipt, TransactionRequest, TxHash, TxpoolContent,
|
||||||
|
TxpoolInspect, TxpoolStatus, H256, U256, U64,
|
||||||
},
|
},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
@ -507,6 +508,39 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
|
||||||
fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, P> {
|
fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, P> {
|
||||||
PendingTransaction::new(tx_hash, self).interval(self.get_interval())
|
PendingTransaction::new(tx_hash, self).interval(self.get_interval())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.request("txpool_content", ())
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.request("txpool_inspect", ())
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.request("txpool_status", ())
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: JsonRpcClient> Provider<P> {
|
impl<P: JsonRpcClient> Provider<P> {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#![allow(unused_braces)]
|
|
||||||
use ethers::providers::{Http, Middleware, Provider};
|
use ethers::providers::{Http, Middleware, Provider};
|
||||||
use std::{convert::TryFrom, time::Duration};
|
use std::{convert::TryFrom, time::Duration};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
use ethers::{
|
||||||
|
providers::{Http, Middleware, Provider},
|
||||||
|
types::TransactionRequest,
|
||||||
|
utils::Geth,
|
||||||
|
};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn txpool() {
|
||||||
|
let geth = Geth::new().block_time(20u64).spawn();
|
||||||
|
let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
|
||||||
|
let account = provider.get_accounts().await.unwrap()[0].clone();
|
||||||
|
let value: u64 = 42;
|
||||||
|
let gas_price = ethers::types::U256::from_dec_str("221435145689").unwrap();
|
||||||
|
let mut tx = TransactionRequest::new()
|
||||||
|
.to(account)
|
||||||
|
.from(account)
|
||||||
|
.value(value)
|
||||||
|
.gas_price(gas_price);
|
||||||
|
|
||||||
|
// send a few transactions
|
||||||
|
let mut txs = Vec::new();
|
||||||
|
for _ in 0..10 {
|
||||||
|
let tx_hash = provider.send_transaction(tx.clone(), None).await.unwrap();
|
||||||
|
txs.push(tx_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we gave a 20s block time, should be plenty for us to get the txpool's content
|
||||||
|
let status = provider.txpool_status().await.unwrap();
|
||||||
|
assert_eq!(status.pending.as_u64(), 10);
|
||||||
|
assert_eq!(status.queued.as_u64(), 0);
|
||||||
|
|
||||||
|
let inspect = provider.txpool_inspect().await.unwrap();
|
||||||
|
assert!(inspect.queued.is_empty());
|
||||||
|
let summary = inspect.pending.get(&account).unwrap();
|
||||||
|
for i in 0..10 {
|
||||||
|
let tx_summary = summary.get(&i.to_string()).unwrap();
|
||||||
|
assert_eq!(tx_summary.gas_price, gas_price);
|
||||||
|
assert_eq!(tx_summary.value, value.into());
|
||||||
|
assert_eq!(tx_summary.gas, 21000.into());
|
||||||
|
assert_eq!(tx_summary.to.unwrap(), account);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = provider.txpool_content().await.unwrap();
|
||||||
|
assert!(content.queued.is_empty());
|
||||||
|
let content = content.pending.get(&account).unwrap();
|
||||||
|
|
||||||
|
// the txs get their gas and nonce auto-set upon mempool entry
|
||||||
|
tx = tx.gas(21000);
|
||||||
|
for i in 0..10 {
|
||||||
|
tx = tx.nonce(i);
|
||||||
|
let req = content.get(&i.to_string()).unwrap();
|
||||||
|
assert_eq!(req, &tx);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue