Skip to main content

foundry_test_utils/
revive.rs

1use alloy_rpc_client::ClientBuilder;
2use eyre::Result;
3use std::{process, thread::sleep, time::Duration};
4use subxt::{OnlineClient, PolkadotConfig};
5use tempfile::TempDir;
6
7const NODE_BINARY: &str = "substrate-node";
8const RPC_PROXY_BINARY: &str = "eth-rpc";
9const MAX_ATTEMPTS: u32 = 15;
10const RPC_URL: &str = "http://127.0.0.1:8545";
11const RETRY_DELAY: Duration = Duration::from_secs(1);
12
13const WALLETS: [(&str, &str); 1] = [(
14    "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac",
15    "0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133",
16)];
17
18/// Spawn and manage an instance of a compatible contracts enabled chain node.
19#[allow(dead_code)]
20struct ContractsNodeProcess {
21    node: process::Child,
22    tmp_dir: tempfile::TempDir,
23}
24
25impl Drop for ContractsNodeProcess {
26    fn drop(&mut self) {
27        self.kill()
28    }
29}
30
31impl ContractsNodeProcess {
32    async fn start() -> Result<Self> {
33        let tmp_dir = TempDir::with_prefix("cargo-contract.cli.test.node")?;
34
35        let mut node = process::Command::new(NODE_BINARY)
36            .env("RUST_LOG", "error")
37            .arg("--dev")
38            .arg(format!("--base-path={}", tmp_dir.path().to_string_lossy()))
39            .arg("--no-prometheus")
40            .spawn()?;
41        // wait for rpc to be initialized
42        let mut attempts = 1;
43        loop {
44            sleep(RETRY_DELAY);
45            tracing::debug!(
46                "Connecting to contracts enabled node, attempt {}/{}",
47                attempts,
48                MAX_ATTEMPTS
49            );
50            match OnlineClient::<PolkadotConfig>::new().await {
51                Result::Ok(_) => return Ok(Self { node, tmp_dir }),
52                Err(err) => {
53                    if attempts < MAX_ATTEMPTS {
54                        attempts += 1;
55                        continue;
56                    }
57                    let err = eyre::eyre!(
58                        "Failed to connect to node rpc after {} attempts: {}",
59                        attempts,
60                        err
61                    );
62                    tracing::error!("{}", err);
63                    node.kill()?;
64                    return Err(err);
65                }
66            }
67        }
68    }
69
70    fn kill(&mut self) {
71        tracing::debug!("Killing contracts node process {}", self.node.id());
72        if let Err(err) = self.node.kill() {
73            tracing::error!("Error killing contracts node process {}: {}", self.node.id(), err)
74        }
75    }
76}
77
78/// Spawn and manage an instance of an ethereum RPC proxy node.
79struct RpcProxyProcess(process::Child);
80
81impl Drop for RpcProxyProcess {
82    fn drop(&mut self) {
83        self.kill()
84    }
85}
86
87impl RpcProxyProcess {
88    async fn start() -> Result<Self> {
89        let mut rpc_proxy = process::Command::new(RPC_PROXY_BINARY)
90            .env("RUST_LOG", "error")
91            .arg("--dev")
92            .arg("--no-prometheus")
93            .spawn()?;
94
95        let client = ClientBuilder::default().connect(RPC_URL).await?;
96
97        let mut attempts = 1;
98        loop {
99            sleep(RETRY_DELAY);
100            match client.request_noparams::<String>("eth_chainId").await {
101                Result::Ok(_) => {
102                    return Ok(Self(rpc_proxy));
103                }
104                Err(err) => {
105                    if attempts < MAX_ATTEMPTS {
106                        attempts += 1;
107                        continue;
108                    }
109
110                    let err = eyre::eyre!(
111                        "Failed to connect to RPC proxy after {} attempts: {}",
112                        MAX_ATTEMPTS,
113                        err
114                    );
115                    tracing::error!("{}", err);
116                    rpc_proxy.kill()?;
117                    return Err(err);
118                }
119            }
120        }
121    }
122
123    fn kill(&mut self) {
124        tracing::debug!("Killing RPC proxy process {}", self.0.id());
125        if let Err(err) = self.0.kill() {
126            tracing::error!("Error killing RPC proxy process {}: {}", self.0.id(), err)
127        }
128    }
129}
130
131/// `PolkadotHubNode` combines a `substrate-node` with an Ethereum RPC proxy to enable
132/// Ethereum-compatible transactions in CI tests.
133///
134/// Before using it, make sure both `substrate-node` and the Ethereum RPC proxy are installed:
135///
136/// ```bash
137/// git clone https://github.com/paritytech/polkadot-sdk
138/// cd polkadot-sdk
139/// cargo build --release --bin substrate-node
140///
141/// cargo install pallet-revive-eth-rpc
142/// ```
143///
144/// Ensure that both binaries are available in your system's PATH and are version-compatible.
145#[allow(dead_code)]
146pub struct PolkadotNode {
147    node: ContractsNodeProcess,
148    rpc_proxy: RpcProxyProcess,
149}
150
151impl PolkadotNode {
152    pub async fn start() -> Result<Self> {
153        let node = ContractsNodeProcess::start().await?;
154        let rpc_proxy = RpcProxyProcess::start().await?;
155        Ok(Self { node, rpc_proxy })
156    }
157
158    pub fn http_endpoint() -> &'static str {
159        RPC_URL
160    }
161
162    pub fn dev_accounts() -> impl Iterator<Item = (&'static str, &'static str)> {
163        WALLETS.iter().copied()
164    }
165}