ethers_hardhat_rs/
utils.rs

1use std::env;
2use std::fs::canonicalize;
3use std::io::ErrorKind;
4use std::marker::PhantomData;
5use std::path::PathBuf;
6
7use async_process::Child;
8use async_process::Command;
9use async_process::ExitStatus;
10
11use ethers_providers_rs::providers::http;
12use ethers_providers_rs::Provider;
13use ethers_signer_rs::signer::Signer;
14use ethers_signer_rs::wallet::WalletSigner;
15use ethers_wallet_rs::{hd_wallet::bip32::DriveKey, wallet::Wallet};
16use futures::executor::block_on;
17use futures::executor::ThreadPool;
18use once_cell::sync::OnceCell;
19
20use crate::error::HardhatError;
21
22/// Kill process and all children processess by process id
23#[cfg(target_family = "unix")]
24pub async fn kill_process_recursive(process_id: u32) -> anyhow::Result<ExitStatus> {
25    let mut child = Command::new("kill")
26        .arg(format!("{}", process_id))
27        .spawn()?;
28
29    Ok(child.status().await?)
30}
31
32/// Returns hardhat base command instance
33pub fn hardhat_command<P>(hardhat_root: P) -> anyhow::Result<Command>
34where
35    P: Into<PathBuf>,
36{
37    let mut command = Command::new("npx");
38
39    command.arg("hardhat");
40
41    command.current_dir(hardhat_root.into());
42
43    Ok(command)
44}
45
46/// Get global thread pool for async task to executing.
47pub fn thread_pool() -> &'static ThreadPool {
48    static POOLS: OnceCell<ThreadPool> = OnceCell::new();
49
50    POOLS.get_or_init(|| ThreadPool::new().unwrap())
51}
52/// Find the nearest cargo manifest dir.
53pub fn find_manifest_dir() -> anyhow::Result<PathBuf> {
54    let start_dir = env::current_dir()?;
55
56    fn search(start_dir: PathBuf) -> anyhow::Result<PathBuf> {
57        log::trace!(target:"HARDHAT","Search manifest file in {}",start_dir.to_string_lossy());
58
59        for item in start_dir.read_dir()? {
60            if let Ok(item) = item {
61                if item.path().is_dir() {
62                    continue;
63                }
64
65                if item.file_name() == "Cargo.toml" {
66                    let path = canonicalize(start_dir)?;
67
68                    log::trace!(target:"HARDHAT","found cargo manifest dir, {}",path.to_string_lossy());
69
70                    return Ok(path);
71                }
72            }
73        }
74
75        if let Some(parent) = start_dir.parent() {
76            return search(parent.to_path_buf());
77        }
78
79        Err(HardhatError::CargoManifestDirNotFound.into())
80    }
81
82    search(start_dir)
83}
84
85/// Returns the hardhat project default path `$CARGO_MANIFEST_DIR/hardhat`
86pub fn hardhat_default_path() -> anyhow::Result<PathBuf> {
87    find_manifest_dir().map(|p| p.join("sol"))
88}
89
90#[async_trait::async_trait]
91pub trait HardhatCommandContext {
92    /// Execute hardhat command in current_path `hardhat_root`
93    ///
94    #[allow(unused)]
95    fn init_command(hardhat_root: PathBuf, c: &mut Command) -> anyhow::Result<()> {
96        Ok(())
97    }
98
99    #[allow(unused)]
100    async fn start_command(c: &mut Child) -> anyhow::Result<()> {
101        Ok(())
102    }
103
104    #[allow(unused)]
105    async fn drop_command(hardhat_root: PathBuf) -> anyhow::Result<()> {
106        Ok(())
107    }
108}
109
110#[derive(Debug)]
111pub struct HardhatCommand<C: HardhatCommandContext> {
112    hardhat_root: PathBuf,
113    /// hardhat network startup command
114    command: Command,
115    /// Started hardhat network process
116    child_process: Option<Child>,
117
118    _marked: PhantomData<C>,
119}
120
121impl<C> HardhatCommand<C>
122where
123    C: HardhatCommandContext,
124{
125    pub fn new() -> anyhow::Result<Self> {
126        Self::new_with(hardhat_default_path()?)
127    }
128    /// Create new hardhat network instance with hardhat project root path.
129    pub fn new_with<P>(hardhat_root: P) -> anyhow::Result<Self>
130    where
131        P: Into<PathBuf>,
132    {
133        let hardhat_root: PathBuf = hardhat_root.into();
134
135        let mut command = hardhat_command(hardhat_root.clone())?;
136
137        C::init_command(hardhat_root.clone(), &mut command)?;
138
139        Ok(Self {
140            hardhat_root,
141            child_process: None,
142            command,
143            _marked: Default::default(),
144        })
145    }
146
147    /// Returns if network started.
148    pub fn is_started(&self) -> bool {
149        self.child_process.is_some()
150    }
151
152    /// Start hardhat network child process.
153    ///
154    /// If already started, returns false.
155    pub async fn start(&mut self) -> anyhow::Result<bool> {
156        if self.is_started() {
157            return Ok(false);
158        }
159
160        let mut child = match self.command.spawn() {
161            Ok(child) => child,
162            Err(err) => {
163                if err.kind() == ErrorKind::NotFound {
164                    return Err(HardhatError::NodejsRequired.into());
165                } else {
166                    return Err(err.into());
167                }
168            }
169        };
170
171        C::start_command(&mut child).await?;
172
173        self.child_process = Some(child);
174
175        return Ok(true);
176    }
177
178    /// Stop hardhat network.
179    ///
180    /// If hardhat network already stopped, returns [`HardhatError::HardhatNetworkStopped`]
181    pub async fn stop(&mut self) -> anyhow::Result<()> {
182        if let Some(child_process) = self.child_process.take() {
183            kill_process_recursive(child_process.id()).await?;
184            Ok(())
185        } else {
186            Err(HardhatError::HardhatNetworkStopped.into())
187        }
188    }
189
190    /// Wait until child process stopped and returns status code.
191    pub async fn status(&mut self) -> anyhow::Result<ExitStatus> {
192        if let Some(mut child_process) = self.child_process.take() {
193            Ok(child_process.status().await?)
194        } else {
195            Err(HardhatError::HardhatNetworkStopped.into())
196        }
197    }
198}
199
200impl<C> Drop for HardhatCommand<C>
201where
202    C: HardhatCommandContext,
203{
204    fn drop(&mut self) {
205        if self.is_started() {
206            let child_process = self.child_process.take().unwrap();
207            let hardhat_root = self.hardhat_root.clone();
208
209            block_on(async move {
210                let drop_result = C::drop_command(hardhat_root).await;
211
212                log::debug!("drop command result, {:?}", drop_result);
213
214                _ = kill_process_recursive(child_process.id()).await;
215            });
216        }
217    }
218}
219
220/// Get hardhat builtin accounts
221pub fn get_hardhat_network_account(i: usize) -> Signer {
222    let drive_key = DriveKey::new(
223        "test test test test test test test test test test test junk",
224        "",
225    );
226    let key = drive_key
227        .drive(format!("m/44'/60'/0'/0/{}", i))
228        .expect("Bip32 drive key");
229
230    Wallet::new(key.private_key)
231        .expect("Create wallet")
232        .try_into_signer()
233        .expect("Create signer error")
234}
235
236/// Get hardhat default provider
237pub fn get_hardhat_network_provider() -> Provider {
238    http::connect_to("http://localhost:8545")
239}
240
241#[cfg(test)]
242mod tests {
243
244    use super::find_manifest_dir;
245
246    #[test]
247    fn test_manifest_dir() {
248        _ = pretty_env_logger::try_init();
249
250        log::debug!("{:?}", find_manifest_dir().expect("find manifest dir"));
251    }
252}