hoprd_gen_test/
hoprd-gen-test.rs

1use anyhow::Context;
2use clap::Parser;
3use hopr_chain_connector::{
4    BlockchainConnectorConfig,
5    api::*,
6    blokli_client::{BlokliClient, BlokliClientConfig, BlokliQueryClient},
7    create_trustful_safeless_hopr_blokli_connector,
8    reexports::hopr_chain_types::exports::alloy::hex,
9};
10use hopr_lib::{ChainKeypair, HoprKeys, Keypair, SafeModule, XDaiBalance, crypto_traits::Randomizable};
11use hoprd::config::{Db, HoprdConfig, Identity, SessionIpForwardingConfig, UserHoprLibConfig};
12use hoprd_api::config::{Api, Auth};
13
14/// Tool used to generate test node Safes and hoprd configuration files.
15///
16/// This tool generates nodes identities, deploys and funds its Safes, and generates node
17/// configuration files to be used with `hoprd`.
18///
19/// This is mostly useful for testing purposes.
20#[derive(Parser, Debug)]
21#[command(name = "hoprd-gen-test", author, version, about = "Tool used to generate test node Safes and hoprd configuration files", long_about = None)]
22struct Args {
23    /// Blokli URL
24    #[arg(long, short, default_value = "http://localhost:8080")]
25    blokli_url: String,
26
27    /// Private key of the Smart Contract deployer
28    #[arg(
29        long,
30        short,
31        default_value = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
32    )]
33    private_key: String,
34
35    /// Number of nodes to generate.
36    #[arg(long, short, default_value = "3")]
37    num_nodes: usize,
38
39    /// Home path where all the node data (config, identity, db) will be stored.
40    #[arg(long, short, default_value = "/tmp/hopr-nodes")]
41    config_home: String,
42
43    #[arg(long, default_value = "password")]
44    identity_password: String,
45
46    /// Whether to generate random IDs or fixed deterministic ones.
47    #[arg(long, short, default_value = "false")]
48    random_identities: bool,
49}
50
51lazy_static::lazy_static! {
52    static ref NODE_KEYS: [HoprKeys; 5] = [
53        (
54            hex!("76a4edbc3f595d4d07671779a0055e30b2b8477ecfd5d23c37afd7b5aa83781d"),
55            hex!("71bf1f42ebbfcd89c3e197a3fd7cda79b92499e509b6fefa0fe44d02821d146a")
56        ).try_into().unwrap(),
57        (
58            hex!("c90f09e849aa512be3dd007452977e32c7cfdc1e3de1a62bd92ba6592bcc9e90"),
59            hex!("c3659450e994f3ad086373440e4e7070629a1bfbd555387237ccb28d17acbfc8")
60        ).try_into().unwrap(),
61        (
62            hex!("40d4749a620d1a4278d030a3153b5b94d6fcd4f9677f6ce8e37e6ebb1987ad53"),
63            hex!("4a14c5aeb53629a2dd45058a8d233f24dd90192189e8200a1e5f10069868f963")
64        ).try_into().unwrap(),
65        (
66            hex!("e539f1ac48270be4e84b6acfe35252df5e141a29b50ddb07b50670271bb574ee"),
67            hex!("8c1edcdebfe508031e4124168bb4a133180e8ee68207a7946fcdc4ad0068ef0d")
68        ).try_into().unwrap(),
69        (
70            hex!("9ab557eb14d8b081c7e1750eb87407d8c421aa79bdeb420f38980829e7dbf936"),
71            hex!("6075c595103667537c33cdb954e3e5189921cab942e5fc0ba9ec27fe6d7787d1")
72        ).try_into().unwrap()
73    ];
74}
75
76#[tokio::main]
77async fn main() -> anyhow::Result<()> {
78    let args = Args::parse();
79
80    std::fs::create_dir_all(&args.config_home)?;
81    let home_path = std::path::Path::new(&args.config_home);
82    let private_key = hex::decode(&args.private_key).context("invalid private key")?;
83
84    let blokli_client = BlokliClient::new(args.blokli_url.parse()?, BlokliClientConfig::default());
85    let status = blokli_client.query_health().await?;
86    if !status.eq_ignore_ascii_case("ok") {
87        return Err(anyhow::anyhow!("Blokli is not usable: {status}"));
88    }
89
90    // Create connector for the deployer account
91    let mut anvil_connector = create_trustful_safeless_hopr_blokli_connector(
92        &ChainKeypair::from_secret(&private_key)?,
93        Default::default(),
94        blokli_client.clone(),
95    )
96    .await?;
97    anvil_connector.connect().await?;
98
99    let initial_token_balance: HoprBalance = "1000 wxHOPR".parse()?;
100    let initial_native_balance: XDaiBalance = "1 xDai".parse()?;
101
102    for id in 0..args.num_nodes.clamp(1, NODE_KEYS.len()) {
103        let kp = if args.random_identities {
104            HoprKeys::random()
105        } else {
106            NODE_KEYS[id].clone()
107        };
108        let node_address = kp.chain_key.public().to_address();
109        eprintln!("Node {id}: Address {node_address}");
110
111        let node_connector = std::sync::Arc::new(
112            create_trustful_safeless_hopr_blokli_connector(
113                &kp.chain_key,
114                BlockchainConnectorConfig::default(),
115                blokli_client.clone(),
116            )
117            .await?,
118        );
119
120        eprint!("Node {id}: Checking balances...");
121
122        // Send 1 xDai to the new node address from Anvil 0 account
123        let node_native_balance: XDaiBalance = node_connector.balance(node_address).await?;
124        if node_native_balance < initial_native_balance {
125            let top_up = initial_native_balance - node_native_balance;
126            if anvil_connector.balance(*anvil_connector.me()).await? < top_up {
127                return Err(anyhow::anyhow!(
128                    "Account {} must have at least {top_up}.",
129                    anvil_connector.me()
130                ));
131            }
132
133            anvil_connector.withdraw(top_up, &node_address).await?.await?;
134            eprint!("\x1b[2K\rNode {id}: {top_up} transferred to {node_address}");
135        } else {
136            eprint!("\x1b[2K\rNode {id}: {node_address} already has {node_native_balance} xDai tokens");
137        }
138
139        eprint!("\x1b[2K\rNode {id}: Checking Safe deployment...");
140        let safe = if let Some(safe) = node_connector.safe_info(SafeSelector::Owner(node_address)).await? {
141            safe
142        } else {
143            // Send 1000 wxHOPR tokens to the new node address from Anvil 0 account
144            eprint!("\x1b[2K\rNode {id}: Topping up to {initial_token_balance}...");
145            let node_token_balance: HoprBalance = node_connector.balance(node_address).await?;
146            if node_token_balance < initial_token_balance {
147                let top_up = initial_token_balance - node_token_balance;
148                if anvil_connector.balance(*anvil_connector.me()).await? < top_up {
149                    return Err(anyhow::anyhow!(
150                        "Account {} must have at least {top_up}.",
151                        anvil_connector.me()
152                    ));
153                }
154
155                anvil_connector.withdraw(top_up, &node_address).await?.await?;
156                eprint!("\x1b[2K\rNode {id}: {top_up} transferred to {node_address}");
157            } else {
158                eprint!("\x1b[2K\rNode {id}: {node_address} already has {node_token_balance} wxHOPR tokens");
159            }
160
161            eprint!("\x1b[2K\rNode {id}: Deploying Safe...");
162            let node_connector_clone = node_connector.clone();
163            let jh = tokio::task::spawn(async move {
164                node_connector_clone
165                    .await_safe_deployment(SafeSelector::Owner(node_address), std::time::Duration::from_secs(10))
166                    .await
167            });
168            node_connector.deploy_safe(initial_token_balance).await?.await?;
169            jh.await??
170        };
171
172        let id_file = home_path
173            .join(format!("node_id_{id}.id"))
174            .to_str()
175            .ok_or(anyhow::anyhow!("Invalid path"))?
176            .to_owned();
177
178        let node_cfg = HoprdConfig {
179            hopr: UserHoprLibConfig {
180                announce: true,
181                safe_module: SafeModule {
182                    safe_address: safe.address,
183                    module_address: safe.module,
184                },
185                ..Default::default()
186            },
187            identity: Identity {
188                file: id_file.clone(),
189                password: args.identity_password.clone(),
190                private_key: None,
191            },
192            db: Db {
193                data: home_path
194                    .join(format!("db_{id}"))
195                    .to_str()
196                    .ok_or(anyhow::anyhow!("Invalid path"))?
197                    .to_owned(),
198                initialize: true,
199                force_initialize: true,
200            },
201            api: Api {
202                enable: true,
203                auth: Auth::None,
204                ..Default::default()
205            },
206            blokli_url: Some(args.blokli_url.clone()),
207            session_ip_forwarding: SessionIpForwardingConfig {
208                use_target_allow_list: false,
209                ..Default::default()
210            },
211            ..Default::default()
212        };
213
214        let cfg_file = home_path
215            .join(format!("hoprd_cfg_{id}.yaml"))
216            .to_str()
217            .ok_or(anyhow::anyhow!("Invalid path"))?
218            .to_owned();
219        std::fs::write(&cfg_file, serde_yaml::to_string(&node_cfg)?)?;
220        kp.write_eth_keystore(&id_file, &args.identity_password)?;
221
222        eprintln!("\x1b[2K\rNode {id}: Node config written to {cfg_file}");
223    }
224
225    Ok(())
226}