hopli/
faucet.rs

1//! This module contains arguments and functions to fund some Ethereum wallets
2//! with native tokens and HOPR tokens
3//!
4//! Despite HOPR contracts are mainly deployed on Gnosis chain,
5//! HOPR token contract addresses vary on the network.
6//!
7//! Attention! Do not use this function to distribute large amount of tokens
8//!
9//! Note that to save gas in batch funding, multicall is used to facilitate token distribution, via `transferFrom`
10//! To use this functionality, caller must grant Multicall3 contract the exact allowance equal to the sum of tokens
11//! to be transferred. As it's a separate function, there is a window between granting the allowance and executing
12//! the transactin. Attacker may take advantage of this window and steal tokens from the caller's account.
13//!
14//! A sample command:
15//! ```text
16//! hopli faucet \
17//!     --network anvil-localhost \
18//!     --contracts-root "../ethereum/contracts" \
19//!     --address 0x0aa7420c43b8c1a7b165d216948870c8ecfe1ee1,0xd057604a14982fe8d88c5fc25aac3267ea142a08 \
20//!     --identity-directory "./test" --identity-prefix node_ \
21//!     --password-path "./test/pwd" \
22//!     --private-key ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
23//!     --hopr-amount 10 --native-amount 0.1 \
24//!     --provider-url "http://localhost:8545"
25//! ```
26use std::{ops::Sub, str::FromStr};
27
28use alloy::primitives::{Address, U256, utils::parse_units};
29use clap::Parser;
30use hopr_bindings::hoprtoken::HoprToken;
31use tracing::info;
32
33use crate::{
34    environment_config::NetworkProviderArgs,
35    key_pair::{ArgEnvReader, IdentityFileArgs, PrivateKeyArgs},
36    methods::{get_native_and_token_balances, transfer_native_tokens, transfer_or_mint_tokens},
37    utils::{Cmd, HelperErrors},
38};
39
40/// CLI arguments for `hopli faucet`
41#[derive(Parser, Default, Debug)]
42pub struct FaucetArgs {
43    /// Network name, contracts config file root, and customized provider, if available
44    #[clap(flatten)]
45    pub network_provider: NetworkProviderArgs,
46
47    /// Additional addresses (comma-separated) to receive funds.
48    #[clap(
49        help = "Comma-separated Ethereum addresses of nodes that will receive funds",
50        long,
51        short,
52        default_value = None
53    )]
54    address: Option<String>,
55
56    /// Argument to locate identity file(s)
57    #[clap(flatten)]
58    local_identity: IdentityFileArgs,
59
60    /// The amount of HOPR tokens (in floating number) to be funded per wallet
61    #[clap(
62        help = "Hopr amount in ether, e.g. 10",
63        long,
64        short = 't',
65        value_parser = clap::value_parser!(f64),
66        default_value_t = 2000.0
67    )]
68    hopr_amount: f64,
69
70    /// The amount of native tokens (in floating number) to be funded per wallet
71    #[clap(
72        help = "Native token amount in ether, e.g. 1",
73        long,
74        short = 'g',
75        value_parser = clap::value_parser!(f64),
76        default_value_t = 10.0
77    )]
78    native_amount: f64,
79
80    /// Access to the private key, of which the wallet either contains sufficient assets
81    /// as the source of funds or it can mint necessary tokens
82    #[clap(flatten)]
83    pub private_key: PrivateKeyArgs,
84}
85
86impl FaucetArgs {
87    /// Execute the faucet command, which funds addresses with required amount of tokens
88    async fn execute_faucet(self) -> Result<(), HelperErrors> {
89        let FaucetArgs {
90            network_provider,
91            address,
92            local_identity,
93            hopr_amount,
94            native_amount,
95            private_key,
96        } = self;
97
98        // Include provided address
99        let mut eth_addresses_all: Vec<Address> = Vec::new();
100        if let Some(addresses) = address {
101            eth_addresses_all.extend(addresses.split(',').map(|addr| {
102                Address::from_str(addr).unwrap_or_else(|e| {
103                    panic!(
104                        "{}",
105                        format!("Cannot parse address {:?} for eth_addresses_all, due to {:?}", addr, e)
106                    )
107                })
108            }));
109        }
110        // if local identity dirs/path is provided, read addresses from identity files
111        eth_addresses_all.extend(local_identity.to_addresses()?.into_iter().map(Address::from));
112        info!("All the addresses: {:?}", eth_addresses_all);
113
114        // `PRIVATE_KEY` - Private key is required to send on-chain transactions
115        let signer_private_key = private_key.read_default()?;
116
117        // get RPC provider for the given network and environment
118        let rpc_provider = network_provider.get_provider_with_signer(&signer_private_key).await?;
119        let contract_addresses = network_provider.get_network_details_from_name()?;
120
121        let hopr_token = HoprToken::new(contract_addresses.addresses.token.into(), rpc_provider.clone());
122
123        // complete actions as defined in `transferOrMintHoprAndSendNativeToAmount` in `SingleActions.s.sol`
124        // get token and native balances for addresses
125        let (native_balances, token_balances) =
126            get_native_and_token_balances(hopr_token.clone(), eth_addresses_all.clone())
127                .await
128                .map_err(HelperErrors::MulticallError)?;
129        // Get the amount of HOPR tokens that addresses need to receive to reach the desired amount
130        let hopr_token_amount: U256 = parse_units(&hopr_amount.to_string(), "ether")
131            .map_err(|_| HelperErrors::ParseError("Failed to parse hopr_amount units".into()))?
132            .into();
133
134        let hopr_token_amounts: Vec<U256> = vec![hopr_token_amount; eth_addresses_all.len()]
135            .into_iter()
136            .enumerate()
137            .map(|(i, h)| {
138                if h.gt(&token_balances[i]) {
139                    let diff = h.sub(token_balances[i]);
140                    info!(
141                        "{:?} hopr-token to be transferred to {:?} to top up {:?}",
142                        diff, eth_addresses_all[i], &token_balances[i]
143                    );
144                    diff
145                } else {
146                    U256::ZERO
147                }
148            })
149            .collect();
150
151        // Get the amount of native tokens that addresses need to receive to reach the desired amount
152        let native_token_amount: U256 = parse_units(&native_amount.to_string(), "ether")
153            .map_err(|_| HelperErrors::ParseError("Failed to parse native_amount units".into()))?
154            .into();
155
156        let native_token_amounts: Vec<U256> = vec![native_token_amount; eth_addresses_all.len()]
157            .into_iter()
158            .enumerate()
159            .map(|(i, n)| {
160                if n.gt(&native_balances[i]) {
161                    let diff = n.sub(native_balances[i]);
162                    info!(
163                        "{:?} native-token to be transferred to {:?}, to top up {:?}",
164                        diff, eth_addresses_all[i], &native_balances[i]
165                    );
166                    diff
167                } else {
168                    U256::ZERO
169                }
170            })
171            .collect();
172
173        // transfer of mint HOPR tokens
174        let total_transferred_hopr_token =
175            transfer_or_mint_tokens(hopr_token, eth_addresses_all.clone(), hopr_token_amounts).await?;
176        info!("total transferred hopr-token is {:?}", total_transferred_hopr_token);
177        // send native tokens
178        let total_transferred_native_token =
179            transfer_native_tokens(rpc_provider, eth_addresses_all, native_token_amounts).await?;
180        info!("total transferred native-token is {:?}", total_transferred_native_token);
181        Ok(())
182    }
183}
184
185impl Cmd for FaucetArgs {
186    /// Run the execute_faucet function
187    fn run(self) -> Result<(), HelperErrors> {
188        Ok(())
189    }
190
191    async fn async_run(self) -> Result<(), HelperErrors> {
192        self.execute_faucet().await
193    }
194}