sn_testnet_deploy/
funding.rs

1// Copyright (c) 2023, MaidSafe.
2// All rights reserved.
3//
4// This SAFE Network Software is licensed under the BSD-3-Clause license.
5// Please see the LICENSE file for more details.
6
7use crate::error::Result;
8use crate::{
9    ansible::{inventory::AnsibleInventoryType, provisioning::AnsibleProvisioner},
10    error::Error,
11    inventory::VirtualMachine,
12    EnvironmentDetails, EvmNetwork,
13};
14use alloy::primitives::Address;
15use alloy::{network::EthereumWallet, signers::local::PrivateKeySigner};
16use evmlib::{common::U256, wallet::Wallet, Network};
17use log::{debug, error, warn};
18use std::collections::HashMap;
19use std::str::FromStr;
20
21/// 100 token (1e20)
22const DEFAULT_TOKEN_AMOUNT: &str = "100_000_000_000_000_000_000";
23/// 0.1 ETH (1e17)
24const DEFAULT_GAS_AMOUNT: &str = "100_000_000_000_000_000";
25
26pub struct FundingOptions {
27    pub evm_network: EvmNetwork,
28    /// For custom network
29    pub evm_data_payments_address: Option<String>,
30    /// For custom network
31    pub evm_merkle_payments_address: Option<String>,
32    /// For custom network
33    pub evm_payment_token_address: Option<String>,
34    /// For custom network
35    pub evm_rpc_url: Option<String>,
36    pub funding_wallet_secret_key: Option<String>,
37    /// The amount of gas tokens to transfer to each ant instance
38    /// Defaults to 0.1 ETH i.e, 100_000_000_000_000_000
39    pub gas_amount: Option<U256>,
40    /// The amount of tokens to transfer to each ant instance.
41    /// Defaults to 100 token, i.e., 100_000_000_000_000_000_000
42    pub token_amount: Option<U256>,
43    /// Have to specify during upscale and deploy
44    pub uploaders_count: Option<u16>,
45}
46
47impl AnsibleProvisioner {
48    /// Retrieve the Ant secret keys from all the Client VMs.
49    pub fn get_client_secret_keys(&self) -> Result<HashMap<VirtualMachine, Vec<PrivateKeySigner>>> {
50        let ant_instance_count = self.get_current_ant_instance_count()?;
51
52        debug!("Fetching ANT secret keys");
53        let mut ant_secret_keys = HashMap::new();
54
55        if ant_instance_count.is_empty() {
56            debug!("No Client VMs found");
57            return Err(Error::EmptyInventory(AnsibleInventoryType::Clients));
58        }
59
60        for (vm, count) in ant_instance_count {
61            if count == 0 {
62                warn!("No ANT instances found for {:?}, ", vm.name);
63                ant_secret_keys.insert(vm.clone(), Vec::new());
64            } else {
65                let sks = self.get_ant_secret_key_per_vm(&vm, count)?;
66                ant_secret_keys.insert(vm.clone(), sks);
67            }
68        }
69
70        Ok(ant_secret_keys)
71    }
72
73    /// Deposit funds from the funding_wallet_secret_key to the ant wallets
74    /// If FundingOptions::ant_uploader_count is provided, it will generate the missing secret keys.
75    /// If not provided, we'll just fund the existing ant wallets
76    pub async fn deposit_funds_to_clients(
77        &self,
78        options: &FundingOptions,
79    ) -> Result<HashMap<VirtualMachine, Vec<PrivateKeySigner>>> {
80        debug!(
81            "Funding secret key: {:?}",
82            options.funding_wallet_secret_key
83        );
84        debug!("Funding all the ant wallets");
85        let mut ant_secret_keys = self.get_client_secret_keys()?;
86
87        for (vm, keys) in ant_secret_keys.iter_mut() {
88            if let Some(provided_count) = options.uploaders_count {
89                if provided_count < keys.len() as u16 {
90                    error!("Provided {provided_count} is less than the existing {} ant uploader count for {}", keys.len(), vm.name);
91                    return Err(Error::InvalidUpscaleDesiredClientCount);
92                }
93                let missing_keys_count = provided_count - keys.len() as u16;
94                debug!(
95                    "Found {} secret keys for {}, missing {missing_keys_count} keys",
96                    keys.len(),
97                    vm.name
98                );
99                if missing_keys_count > 0 {
100                    debug!(
101                        "Generating {missing_keys_count} secret keys for {}",
102                        vm.name
103                    );
104                    for _ in 0..missing_keys_count {
105                        let sk = PrivateKeySigner::random();
106                        debug!("Generated key with address: {}", sk.address());
107                        keys.push(sk);
108                    }
109                }
110            }
111        }
112
113        self.deposit_funds(&ant_secret_keys, options).await?;
114
115        Ok(ant_secret_keys)
116    }
117
118    pub async fn prepare_pre_funded_wallets(
119        &self,
120        wallet_keys: &[String],
121    ) -> Result<HashMap<VirtualMachine, Vec<PrivateKeySigner>>> {
122        debug!("Using pre-funded wallets");
123
124        let client_vms = self
125            .ansible_runner
126            .get_inventory(AnsibleInventoryType::Clients, true)?;
127        if client_vms.is_empty() {
128            return Err(Error::EmptyInventory(AnsibleInventoryType::Clients));
129        }
130
131        let total_keys = wallet_keys.len();
132        let vm_count = client_vms.len();
133        if !total_keys.is_multiple_of(vm_count) {
134            return Err(Error::InvalidWalletCount(total_keys, vm_count));
135        }
136
137        let uploaders_per_vm = total_keys / vm_count;
138        let mut vm_to_keys = HashMap::new();
139        let mut key_index = 0;
140
141        for vm in client_vms {
142            let mut keys = Vec::new();
143            for _ in 0..uploaders_per_vm {
144                let sk_str = &wallet_keys[key_index];
145                let sk = sk_str.parse().map_err(|_| Error::FailedToParseKey)?;
146                keys.push(sk);
147                key_index += 1;
148            }
149            vm_to_keys.insert(vm, keys);
150        }
151
152        Ok(vm_to_keys)
153    }
154
155    /// Drain all the funds from the local ANT wallets to the provided wallet
156    pub async fn drain_funds_from_ant_instances(
157        &self,
158        to_address: Address,
159        evm_network: Network,
160    ) -> Result<()> {
161        debug!("Draining all the local ANT wallets to {to_address:?}");
162        println!("Draining all the local ANT wallets to {to_address:?}");
163        let ant_secret_keys = self.get_client_secret_keys()?;
164
165        for (vm, keys) in ant_secret_keys.iter() {
166            debug!(
167                "Draining funds for Client vm: {} to {to_address:?}",
168                vm.name
169            );
170            for ant_sk in keys.iter() {
171                debug!(
172                    "Draining funds for Client vm: {} with key: {ant_sk:?}",
173                    vm.name,
174                );
175
176                let from_wallet =
177                    Wallet::new(evm_network.clone(), EthereumWallet::new(ant_sk.clone()));
178
179                let token_balance = from_wallet.balance_of_tokens().await.inspect_err(|err| {
180                    debug!(
181                        "Failed to get token balance for {} with err: {err:?}",
182                        from_wallet.address()
183                    )
184                })?;
185
186                println!(
187                    "Draining {token_balance} tokens from {} to {to_address:?}",
188                    from_wallet.address()
189                );
190                debug!(
191                    "Draining {token_balance} tokens from {} to {to_address:?}",
192                    from_wallet.address()
193                );
194
195                if token_balance.is_zero() {
196                    debug!(
197                        "No tokens to drain from wallet: {} with token balance",
198                        from_wallet.address()
199                    );
200                } else {
201                    from_wallet
202                        .transfer_tokens(to_address, token_balance)
203                        .await
204                        .inspect_err(|err| {
205                            debug!(
206                                "Failed to transfer {token_balance} tokens from {to_address} with err: {err:?}",
207                            )
208                        })?;
209                    println!(
210                        "Drained {token_balance} tokens from {} to {to_address:?}",
211                        from_wallet.address()
212                    );
213                    debug!(
214                        "Drained {token_balance} tokens from {} to {to_address:?}",
215                        from_wallet.address()
216                    );
217                }
218
219                let gas_balance = from_wallet
220                    .balance_of_gas_tokens()
221                    .await
222                    .inspect_err(|err| {
223                        debug!(
224                            "Failed to get gas token balance for {} with err: {err:?}",
225                            from_wallet.address()
226                        )
227                    })?;
228
229                println!(
230                    "Draining {gas_balance} gas from {} to {to_address:?}",
231                    from_wallet.address()
232                );
233                debug!(
234                    "Draining {gas_balance} gas from {} to {to_address:?}",
235                    from_wallet.address()
236                );
237
238                if gas_balance.is_zero() {
239                    debug!("No gas tokens to drain from wallet: {to_address}");
240                } else {
241                    from_wallet
242                    // 0.001 gas
243                        .transfer_gas_tokens(to_address, gas_balance - U256::from_str("10_000_000_000_000").unwrap()).await
244                        .inspect_err(|err| {
245                            debug!(
246                                "Failed to transfer {gas_balance} gas from {to_address} with err: {err:?}",
247                            )
248                        })?;
249                    println!(
250                        "Drained {gas_balance} gas from {} to {to_address:?}",
251                        from_wallet.address()
252                    );
253                    debug!(
254                        "Drained {gas_balance} gas from {} to {to_address:?}",
255                        from_wallet.address()
256                    );
257                }
258            }
259        }
260        println!("All funds drained to {to_address:?} successfully");
261        debug!("All funds drained to {to_address:?} successfully");
262
263        Ok(())
264    }
265
266    /// Return the (vm name, ant_instance count) for all Client VMs
267    pub fn get_current_ant_instance_count(&self) -> Result<HashMap<VirtualMachine, usize>> {
268        let client_inventories = self
269            .ansible_runner
270            .get_inventory(AnsibleInventoryType::Clients, true)?;
271        if client_inventories.is_empty() {
272            debug!("No Client VMs found");
273            return Err(Error::EmptyInventory(AnsibleInventoryType::Clients));
274        }
275
276        let mut ant_instnace_count = HashMap::new();
277
278        for vm in client_inventories {
279            debug!(
280                "Fetching ant instance count for {} @ {}",
281                vm.name, vm.public_ip_addr
282            );
283            let cmd =
284                "systemctl list-units --type=service --all | grep ant_random_uploader_ | wc -l";
285            let result = self
286                .ssh_client
287                .run_command(&vm.public_ip_addr, "root", cmd, true);
288            match result {
289                Ok(count) => {
290                    debug!("Count found to be {count:?}, parsing");
291                    let count = count
292                        .first()
293                        .ok_or_else(|| {
294                            error!("No count found for {}", vm.name);
295                            Error::SecretKeyNotFound
296                        })?
297                        .trim()
298                        .parse()
299                        .map_err(|_| Error::FailedToParseKey)?;
300                    ant_instnace_count.insert(vm.clone(), count);
301                }
302                Err(Error::ExternalCommandRunFailed {
303                    binary,
304                    exit_status,
305                }) => {
306                    if let Some(1) = exit_status.code() {
307                        debug!("No ant instance found for {:?}", vm.public_ip_addr);
308                        ant_instnace_count.insert(vm.clone(), 0);
309                    } else {
310                        debug!("Error while fetching ant instance count with different exit code {exit_status:?}",);
311                        return Err(Error::ExternalCommandRunFailed {
312                            binary,
313                            exit_status,
314                        });
315                    }
316                }
317                Err(err) => {
318                    debug!("Error while fetching ant instance count: {err:?}",);
319                    return Err(err);
320                }
321            }
322        }
323
324        Ok(ant_instnace_count)
325    }
326
327    fn get_ant_secret_key_per_vm(
328        &self,
329        vm: &VirtualMachine,
330        instance_count: usize,
331    ) -> Result<Vec<PrivateKeySigner>> {
332        let mut sks_per_vm = Vec::new();
333
334        debug!(
335            "Fetching ANT secret key for {} @ {}",
336            vm.name, vm.public_ip_addr
337        );
338        // Note: if this is gonna be parallelized, we need to make sure the secret keys are in order.
339        // the playbook expects them in order
340        for count in 1..=instance_count {
341            let cmd = format!(
342                "systemctl show ant_random_uploader_{count}.service --property=Environment | grep SECRET_KEY | cut -d= -f3 | awk '{{print $1}}'"
343            );
344            debug!("Fetching secret key for {} instance {count}", vm.name);
345            let result = self
346                .ssh_client
347                .run_command(&vm.public_ip_addr, "root", &cmd, true);
348            match result {
349                Ok(secret_keys) => {
350                    let sk_str = secret_keys
351                        .iter()
352                        .map(|sk| sk.trim().to_string())
353                        .collect::<Vec<String>>();
354                    let sk_str = sk_str.first().ok_or({
355                        debug!("No secret key found for {}", vm.name);
356                        Error::SecretKeyNotFound
357                    })?;
358                    let sk = sk_str.parse().map_err(|_| Error::FailedToParseKey)?;
359
360                    debug!("Secret keys found for {} instance {count}: {sk:?}", vm.name,);
361
362                    sks_per_vm.push(sk);
363                }
364                Err(err) => {
365                    debug!("Error while fetching secret key: {err}");
366                    return Err(err);
367                }
368            }
369        }
370
371        Ok(sks_per_vm)
372    }
373
374    async fn deposit_funds(
375        &self,
376        all_secret_keys: &HashMap<VirtualMachine, Vec<PrivateKeySigner>>,
377        options: &FundingOptions,
378    ) -> Result<()> {
379        if all_secret_keys.is_empty() {
380            error!("No ANT secret keys found");
381            return Err(Error::SecretKeyNotFound);
382        }
383
384        let funding_wallet_sk: PrivateKeySigner =
385            if let Some(sk) = &options.funding_wallet_secret_key {
386                sk.parse().map_err(|_| Error::FailedToParseKey)?
387            } else {
388                warn!("Funding wallet secret key not provided. Skipping funding.");
389                return Ok(());
390            };
391
392        let _sk_count = all_secret_keys.values().map(|v| v.len()).sum::<usize>();
393
394        let from_wallet = match &options.evm_network {
395            EvmNetwork::Anvil | EvmNetwork::Custom => {
396                let network = if let (
397                    Some(evm_data_payments_address),
398                    Some(evm_payment_token_address),
399                    Some(evm_rpc_url),
400                ) = (
401                    options.evm_data_payments_address.as_ref(),
402                    options.evm_payment_token_address.as_ref(),
403                    options.evm_rpc_url.as_ref(),
404                ) {
405                    Network::new_custom(
406                        evm_rpc_url,
407                        evm_payment_token_address,
408                        evm_data_payments_address,
409                        options.evm_merkle_payments_address.as_deref(),
410                    )
411                } else {
412                    error!("Custom evm network data not provided");
413                    return Err(Error::EvmTestnetDataNotFound);
414                };
415
416                Wallet::new(network.clone(), EthereumWallet::new(funding_wallet_sk))
417            }
418            EvmNetwork::ArbitrumOne => {
419                let network = Network::ArbitrumOne;
420                Wallet::new(network.clone(), EthereumWallet::new(funding_wallet_sk))
421            }
422            EvmNetwork::ArbitrumSepoliaTest => {
423                let network = Network::ArbitrumSepoliaTest;
424                Wallet::new(network.clone(), EthereumWallet::new(funding_wallet_sk))
425            }
426        };
427        debug!("Using EVM network: {:?}", options.evm_network);
428
429        let token_balance = from_wallet.balance_of_tokens().await?;
430        let gas_balance = from_wallet.balance_of_gas_tokens().await?;
431        println!("Funding wallet token balance: {token_balance}");
432        println!("Funding wallet gas balance: {gas_balance}");
433        debug!("Funding wallet token balance: {token_balance:?} and gas balance {gas_balance}");
434
435        let default_token_amount = U256::from_str(DEFAULT_TOKEN_AMOUNT).unwrap();
436        let default_gas_amount = U256::from_str(DEFAULT_GAS_AMOUNT).unwrap();
437
438        let token_amount = options.token_amount.unwrap_or(default_token_amount);
439        let gas_amount = options.gas_amount.unwrap_or(default_gas_amount);
440
441        println!(
442            "Transferring {token_amount} tokens and {gas_amount} gas tokens to each ANT instance"
443        );
444        debug!(
445            "Transferring {token_amount} tokens and {gas_amount} gas tokens to each ANT instance"
446        );
447
448        for (vm, vm_secret_keys) in all_secret_keys.iter() {
449            println!("Transferring funds for Client vm: {}", vm.name);
450            for sk in vm_secret_keys.iter() {
451                sk.address();
452
453                if !token_amount.is_zero() {
454                    print!("Transferring {token_amount} tokens to {}...", sk.address());
455                    from_wallet
456                        .transfer_tokens(sk.address(), token_amount)
457                        .await
458                        .inspect_err(|err| {
459                            debug!(
460                                "Failed to transfer {token_amount} tokens to {}: {err:?}",
461                                sk.address()
462                            )
463                        })?;
464                    println!("Transfer complete");
465                }
466                if !gas_amount.is_zero() {
467                    print!("Transferring {gas_amount} gas to {}...", sk.address());
468                    from_wallet
469                        .transfer_gas_tokens(sk.address(), gas_amount)
470                        .await
471                        .inspect_err(|err| {
472                            debug!(
473                                "Failed to transfer {gas_amount} gas to {}: {err:?}",
474                                sk.address()
475                            )
476                        })?;
477                    println!("Transfer complete");
478                }
479            }
480        }
481        println!("All funds transferred successfully");
482        debug!("All funds transferred successfully");
483
484        Ok(())
485    }
486}
487
488/// Get the Address of the funding wallet from the secret key string
489pub fn get_address_from_sk(secret_key: &str) -> Result<Address> {
490    let sk: PrivateKeySigner = secret_key.parse().map_err(|_| Error::FailedToParseKey)?;
491    Ok(sk.address())
492}
493
494pub async fn drain_funds(
495    ansible_provisioner: &AnsibleProvisioner,
496    environment_details: &EnvironmentDetails,
497) -> Result<()> {
498    let evm_network = match environment_details.evm_details.network {
499        EvmNetwork::Anvil => None,
500        EvmNetwork::Custom => Some(Network::new_custom(
501            environment_details.evm_details.rpc_url.as_ref().unwrap(),
502            environment_details
503                .evm_details
504                .payment_token_address
505                .as_ref()
506                .unwrap(),
507            environment_details
508                .evm_details
509                .data_payments_address
510                .as_ref()
511                .unwrap(),
512            environment_details
513                .evm_details
514                .merkle_payments_address
515                .as_deref(),
516        )),
517        EvmNetwork::ArbitrumOne => Some(Network::ArbitrumOne),
518        EvmNetwork::ArbitrumSepoliaTest => Some(Network::ArbitrumSepoliaTest),
519    };
520
521    if let (Some(network), Some(address)) =
522        (evm_network, &environment_details.funding_wallet_address)
523    {
524        // Check if wallets exist before attempting to drain funds
525        match ansible_provisioner.get_current_ant_instance_count() {
526            Ok(ant_instances) if !ant_instances.is_empty() => {
527                let has_wallets = ant_instances.values().any(|&count| count > 0);
528                if has_wallets {
529                    ansible_provisioner
530                        .drain_funds_from_ant_instances(
531                            Address::from_str(address).map_err(|err| {
532                                log::error!("Invalid funding wallet public key: {err:?}");
533                                Error::FailedToParseKey
534                            })?,
535                            network,
536                        )
537                        .await?;
538                } else {
539                    println!("No wallets found to drain funds from. Skipping wallet removal.");
540                    log::info!("No wallets found to drain funds from. Skipping wallet removal.");
541                }
542            }
543            Ok(_) | Err(_) => {
544                println!("No client VMs or wallets found. Skipping wallet removal.");
545                log::info!("No client VMs or wallets found. Skipping wallet removal.");
546            }
547        }
548        Ok(())
549    } else {
550        println!("Custom network provided. Not draining funds.");
551        log::info!("Custom network provided. Not draining funds.");
552        Ok(())
553    }
554}