identity_iota_core/rebased/
utils.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::process::Output;
5
6use anyhow::Context as _;
7use iota_interaction::types::base_types::ObjectID;
8use iota_sdk::IotaClient;
9use iota_sdk::IotaClientBuilder;
10use serde::Deserialize;
11#[cfg(not(target_arch = "wasm32"))]
12use tokio::process::Command;
13
14use crate::rebased::Error;
15use iota_interaction::types::base_types::IotaAddress;
16
17const FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET: u64 = 5_000_000;
18const FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE: u64 = 500_000_000;
19
20#[derive(Deserialize, Debug)]
21#[serde(rename_all = "camelCase")]
22struct CoinOutput {
23  gas_coin_id: ObjectID,
24  nanos_balance: u64,
25}
26
27/// Builds an `IOTA` client for the given network.
28pub async fn get_client(network: &str) -> Result<IotaClient, Error> {
29  let client = IotaClientBuilder::default()
30    .build(network)
31    .await
32    .map_err(|err| Error::Network(format!("failed to connect to {network}"), err))?;
33
34  Ok(client)
35}
36
37fn unpack_command_output(output: &Output, task: &str) -> anyhow::Result<String> {
38  let stdout = std::str::from_utf8(&output.stdout)?;
39  if !output.status.success() {
40    let stderr = std::str::from_utf8(&output.stderr)?;
41    anyhow::bail!("Failed to {task}: {stdout}, {stderr}");
42  }
43
44  Ok(stdout.to_string())
45}
46
47/// Requests funds from the local IOTA client's configured faucet.
48///
49/// This behavior can be changed to send funds with local IOTA client's active address to the given address.
50/// For that the env variable `IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS` must be set to `true`.
51/// Notice, that this is a setting mostly intended for internal test use and must be used with care.
52/// For details refer to to `identity_iota_core`'s README.md.
53#[cfg(not(target_arch = "wasm32"))]
54pub async fn request_funds(address: &IotaAddress) -> anyhow::Result<()> {
55  let fund_with_active_address = std::env::var("IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS")
56    .map(|v| !v.is_empty() && v.to_lowercase() == "true")
57    .unwrap_or(false);
58
59  if !fund_with_active_address {
60    let output = Command::new("iota")
61      .arg("client")
62      .arg("faucet")
63      .arg("--address")
64      .arg(address.to_string())
65      .arg("--json")
66      .output()
67      .await
68      .context("Failed to execute command")?;
69    unpack_command_output(&output, "request funds from faucet")?;
70  } else {
71    let output = Command::new("iota")
72      .arg("client")
73      .arg("gas")
74      .arg("--json")
75      .output()
76      .await
77      .context("Failed to execute command")?;
78    let output_str = unpack_command_output(&output, "fetch active account's gas coins")?;
79
80    let parsed: Vec<CoinOutput> = serde_json::from_str(&output_str)?;
81    let min_balance = FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE + FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET;
82    let matching = parsed.into_iter().find(|coin| coin.nanos_balance >= min_balance);
83    let Some(coin_to_use) = matching else {
84      anyhow::bail!("Failed to find coin object with enough funds to transfer to test account");
85    };
86
87    let address_string = address.to_string();
88    let output = Command::new("iota")
89      .arg("client")
90      .arg("pay-iota")
91      .arg("--recipients")
92      .arg(&address_string)
93      .arg("--input-coins")
94      .arg(coin_to_use.gas_coin_id.to_string())
95      .arg("--amounts")
96      .arg(FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE.to_string())
97      .arg("--gas-budget")
98      .arg(FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET.to_string())
99      .arg("--json")
100      .output()
101      .await
102      .context("Failed to execute command")?;
103    unpack_command_output(&output, &format!("send funds from active account to {address_string}"))?;
104  }
105
106  Ok(())
107}