1use 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
21const DEFAULT_TOKEN_AMOUNT: &str = "100_000_000_000_000_000_000";
23const DEFAULT_GAS_AMOUNT: &str = "100_000_000_000_000_000";
25
26pub struct FundingOptions {
27 pub evm_network: EvmNetwork,
28 pub evm_data_payments_address: Option<String>,
30 pub evm_merkle_payments_address: Option<String>,
32 pub evm_payment_token_address: Option<String>,
34 pub evm_rpc_url: Option<String>,
36 pub funding_wallet_secret_key: Option<String>,
37 pub gas_amount: Option<U256>,
40 pub token_amount: Option<U256>,
43 pub uploaders_count: Option<u16>,
45}
46
47impl AnsibleProvisioner {
48 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 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 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 .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 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 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
488pub 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 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}