Skip to content

Commit

Permalink
Add support for "fauceting" test/example transaction via local active…
Browse files Browse the repository at this point in the history
… address (#1473)

* add support for tests against devnet

- add `API_ENDPOINT` env variable for tests and examples
- add `IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS` env variable to toggle funding via active address

* update inline function documentation

* replace local api endpoint constants

- now using `IOTA_LOCAL_NETWORK_URL` from `iota` create
  • Loading branch information
wulfraem authored Dec 3, 2024
1 parent 077e96b commit 2092b7c
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 27 deletions.
7 changes: 3 additions & 4 deletions examples/utils/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use identity_storage::KeyType;
use identity_storage::StorageSigner;
use identity_stronghold::StrongholdStorage;
use iota_sdk::IotaClientBuilder;
use iota_sdk::IOTA_LOCAL_NETWORK_URL;
use iota_sdk_legacy::client::secret::stronghold::StrongholdSecretManager;
use iota_sdk_legacy::client::Password;
use rand::distributions::DistString;
Expand Down Expand Up @@ -81,11 +82,9 @@ where
K: JwkStorage,
I: KeyIdStorage,
{
// The API endpoint of an IOTA node
let api_endpoint: &str = "http://127.0.0.1:9000";

let api_endpoint = std::env::var("API_ENDPOINT").unwrap_or_else(|_| IOTA_LOCAL_NETWORK_URL.to_string());
let iota_client = IotaClientBuilder::default()
.build(api_endpoint)
.build(&api_endpoint)
.await
.map_err(|err| anyhow::anyhow!(format!("failed to connect to network; {}", err)))?;

Expand Down
32 changes: 31 additions & 1 deletion identity_iota_core/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
IOTA Identity
===

This crate provides the core data structures for the [IOTA DID Method Specification](https://wiki.iota.org/shimmer/identity.rs/specs/did/iota_did_method_spec). It provides interfaces for publishing and resolving DID Documents to and from the Tangle according to the IOTA DID Method Specification.
## About
This crate provides the core data structures for the [IOTA DID Method Specification](https://wiki.iota.org/identity.rs/references/specifications/iota-did-method-spec/). It provides interfaces for publishing and resolving DID Documents according to the IOTA DID Method Specification.

## Running the tests
You can run the tests as usual with:

```sh
cargo test
```

The e2e should be run against against a [local network](https://docs.iota.org/developer/getting-started/local-network), as this makes funding way more easy, as the local faucet can be used deliberately.

### Running the tests with active-address-funding
When you're not running the tests locally, you might notice some restrictions in regards of interactions with the faucet. The current e2e test setup creates new test accounts for every test to avoid test pollution, but those accounts requests funds from a faucet. That faucet might have restrictions on how much funds an IP can request in a certain time range. For example, this might happen when trying to run the tests against `devnet`.

As we want to verify that our API works as expected on this environment as well, a toggle has been added to change the behavior in the tests to not request the faucet for funds, but use the active account of the IOTA CLI to send funds to new test users. This is not the default test behavior and should only be used in edge cases, as it comes with a few caveats, that might not be desired to have in the tests:

- The active address must be well funded, the current active-address-funding transfers 500_000_000 NANOS to new test accounts. So make sure, this account has enough funds to support a few 2e2 tests with one or more accounts.
- The tests will take longer, as they have to be run sequentially to avoid collisions between the fund sending transactions.

You can run a tests with active-address-funding with:

```sh
IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS=true cargo test -- --test-threads=1
```

To check your active account's funds, you can use

```sh
iota client gas
```
2 changes: 1 addition & 1 deletion identity_iota_core/src/rebased/client/full_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ impl<S> IdentityClient<S> {
self.sender_address(),
vec![gas_coin.object_ref()],
tx.clone(),
50_000_000_000,
50_000_000,
gas_price,
);
let dry_run_gas_result = self.read_api().dry_run_transaction_block(tx_data).await?.effects;
Expand Down
97 changes: 77 additions & 20 deletions identity_iota_core/src/rebased/utils.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use std::process::Output;

use anyhow::Context as _;
use iota_sdk::types::base_types::ObjectID;
use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder;
use iota_sdk::types::transaction::Argument;
use iota_sdk::types::TypeTag;
use serde::Deserialize;
use serde::Serialize;
use tokio::process::Command;

Expand All @@ -15,8 +18,15 @@ use iota_sdk::IotaClientBuilder;

use crate::rebased::Error;

/// The local `IOTA` network.
pub const LOCAL_NETWORK: &str = "http://127.0.0.1:9000";
const FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET: u64 = 5_000_000;
const FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE: u64 = 500_000_000;

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct CoinOutput {
gas_coin_id: ObjectID,
nanos_balance: u64,
}

/// Builds an `IOTA` client for the given network.
pub async fn get_client(network: &str) -> Result<IotaClient, Error> {
Expand All @@ -28,25 +38,72 @@ pub async fn get_client(network: &str) -> Result<IotaClient, Error> {
Ok(client)
}

/// Requests funds from the local `IOTA` faucet.
pub async fn request_funds(address: &IotaAddress) -> anyhow::Result<()> {
let output = Command::new("iota")
.arg("client")
.arg("faucet")
.arg("--address")
.arg(address.to_string())
.arg("--url")
.arg("http://127.0.0.1:9123/gas")
.arg("--json")
.output()
.await
.context("Failed to execute command")?;

fn unpack_command_output(output: &Output, task: &str) -> anyhow::Result<String> {
let stdout = std::str::from_utf8(&output.stdout)?;
if !output.status.success() {
anyhow::bail!(
"Failed to request funds from faucet: {}",
std::str::from_utf8(&output.stderr).unwrap()
);
let stderr = std::str::from_utf8(&output.stderr)?;
anyhow::bail!("Failed to {task}: {stdout}, {stderr}");
}

Ok(stdout.to_string())
}

/// Requests funds from the local IOTA client's configured faucet.
///
/// This behavior can be changed to send funds with local IOTA client's active address to the given address.
/// For that the env variable `IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS` must be set to `true`.
/// Notice, that this is a setting mostly intended for internal test use and must be used with care.
/// For details refer to to `identity_iota_core`'s README.md.
pub async fn request_funds(address: &IotaAddress) -> anyhow::Result<()> {
let fund_with_active_address = std::env::var("IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS")
.map(|v| !v.is_empty() && v.to_lowercase() == "true")
.unwrap_or(false);

if !fund_with_active_address {
let output = Command::new("iota")
.arg("client")
.arg("faucet")
.arg("--address")
.arg(address.to_string())
.arg("--json")
.output()
.await
.context("Failed to execute command")?;
unpack_command_output(&output, "request funds from faucet")?;
} else {
let output = Command::new("iota")
.arg("client")
.arg("gas")
.arg("--json")
.output()
.await
.context("Failed to execute command")?;
let output_str = unpack_command_output(&output, "fetch active account's gas coins")?;

let parsed: Vec<CoinOutput> = serde_json::from_str(&output_str)?;
let min_balance = FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE + FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET;
let matching = parsed.into_iter().find(|coin| coin.nanos_balance >= min_balance);
let Some(coin_to_use) = matching else {
anyhow::bail!("Failed to find coin object with enough funds to transfer to test account");
};

let address_string = address.to_string();
let output = Command::new("iota")
.arg("client")
.arg("pay-iota")
.arg("--recipients")
.arg(&address_string)
.arg("--input-coins")
.arg(coin_to_use.gas_coin_id.to_string())
.arg("--amounts")
.arg(FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE.to_string())
.arg("--gas-budget")
.arg(FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET.to_string())
.arg("--json")
.output()
.await
.context("Failed to execute command")?;
unpack_command_output(&output, &format!("send funds from active account to {address_string}"))?;
}

Ok(())
Expand Down
4 changes: 3 additions & 1 deletion identity_iota_core/tests/e2e/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use iota_sdk::types::TypeTag;
use iota_sdk::types::IOTA_FRAMEWORK_PACKAGE_ID;
use iota_sdk::IotaClient;
use iota_sdk::IotaClientBuilder;
use iota_sdk::IOTA_LOCAL_NETWORK_URL;
use jsonpath_rust::JsonPathQuery;
use lazy_static::lazy_static;
use move_core_types::ident_str;
Expand Down Expand Up @@ -75,7 +76,8 @@ lazy_static! {
}

pub async fn get_client() -> anyhow::Result<TestClient> {
let client = IotaClientBuilder::default().build_localnet().await?;
let api_endpoint = std::env::var("API_ENDPOINT").unwrap_or_else(|_| IOTA_LOCAL_NETWORK_URL.to_string());
let client = IotaClientBuilder::default().build(&api_endpoint).await?;
let package_id = PACKAGE_ID.get_or_try_init(|| init(&client)).await.copied()?;
let address = get_active_address().await?;

Expand Down

0 comments on commit 2092b7c

Please sign in to comment.