use crate::consts::DEFAULT_PORT;
use anyhow;
use clap::Parser;
use fuel_core::{
chain_config::default_consensus_dev_key,
service::{
config::{DbType, Trigger},
Config,
},
};
use fuel_core_chain_config::{
coin_config_helpers::CoinConfigGenerator, ChainConfig, CoinConfig, Owner, SnapshotMetadata,
TESTNET_INITIAL_BALANCE,
};
use fuel_core_types::{
fuel_crypto::fuel_types::{Address, AssetId},
secrecy::Secret,
signer::SignMode,
};
use std::{path::PathBuf, str::FromStr};
#[derive(Parser, Debug, Clone)]
pub struct LocalCmd {
#[clap(long)]
pub chain_config: Option<PathBuf>,
#[clap(long)]
pub port: Option<u16>,
#[clap(long)]
pub db_path: Option<PathBuf>,
#[clap(long = "db-type", value_enum)]
pub db_type: Option<DbType>,
#[clap(long)]
pub account: Vec<String>,
#[arg(long = "debug", env)]
pub debug: bool,
#[arg(long = "historical-execution", env)]
pub historical_execution: bool,
#[arg(long = "poa-instant", env)]
pub poa_instant: bool,
}
fn get_coins_per_account(
account_strings: Vec<String>,
base_asset_id: &AssetId,
current_coin_idx: usize,
) -> anyhow::Result<Vec<CoinConfig>> {
let mut coin_generator = CoinConfigGenerator::new();
let mut coins = Vec::new();
for account_string in account_strings {
let parts: Vec<&str> = account_string.trim().split(':').collect();
let (owner, asset_id, amount) = match parts.as_slice() {
[owner_str] => {
let owner = Address::from_str(owner_str)
.map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
(owner, *base_asset_id, TESTNET_INITIAL_BALANCE)
}
[owner_str, asset_str] => {
let owner = Address::from_str(owner_str)
.map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
let asset_id = AssetId::from_str(asset_str)
.map_err(|e| anyhow::anyhow!("Invalid asset ID: {}", e))?;
(owner, asset_id, TESTNET_INITIAL_BALANCE)
}
[owner_str, asset_str, amount_str] => {
let owner = Address::from_str(owner_str)
.map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
let asset_id = AssetId::from_str(asset_str)
.map_err(|e| anyhow::anyhow!("Invalid asset ID: {}", e))?;
let amount = amount_str
.parse::<u64>()
.map_err(|e| anyhow::anyhow!("Invalid amount: {}", e))?;
(owner, asset_id, amount)
}
_ => {
return Err(anyhow::anyhow!(
"Invalid account format: {}. Expected format: <account-id>[:asset-id[:amount]]",
account_string
));
}
};
let coin = CoinConfig {
amount,
owner: owner.into(),
asset_id,
output_index: (current_coin_idx + coins.len()) as u16,
..coin_generator.generate()
};
coins.push(coin);
}
Ok(coins)
}
impl From<LocalCmd> for Config {
fn from(cmd: LocalCmd) -> Self {
let LocalCmd {
chain_config,
port,
db_path,
db_type,
account,
debug,
historical_execution,
poa_instant,
..
} = cmd;
let chain_config = match chain_config {
Some(path) => match SnapshotMetadata::read(&path) {
Ok(metadata) => ChainConfig::from_snapshot_metadata(&metadata).unwrap(),
Err(e) => {
tracing::error!("Failed to open snapshot reader: {}", e);
tracing::warn!("Using local testnet snapshot reader");
ChainConfig::local_testnet()
}
},
None => ChainConfig::local_testnet(),
};
let base_asset_id = chain_config.consensus_parameters.base_asset_id();
let mut state_config = fuel_core_chain_config::StateConfig::local_testnet();
state_config
.coins
.iter_mut()
.for_each(|coin| coin.asset_id = *base_asset_id);
let current_coin_idx = state_config.coins.len();
if !account.is_empty() {
let coins = get_coins_per_account(account, base_asset_id, current_coin_idx)
.map_err(|e| anyhow::anyhow!("Error parsing account funding: {}", e))
.unwrap();
if !coins.is_empty() {
tracing::info!("Additional accounts");
for coin in &coins {
let owner_hex = match &coin.owner {
Owner::Address(address) => format!("{address:#x}"),
Owner::SecretKey(secret_key) => format!("{secret_key:#x}"),
};
tracing::info!(
"Address({}), Asset ID({:#x}), Balance({})",
owner_hex,
coin.asset_id,
coin.amount
);
}
state_config.coins.extend(coins);
}
}
let mut config = Config::local_node_with_configs(chain_config, state_config);
config.name = "fuel-core".to_string();
config.debug = debug;
let key = default_consensus_dev_key();
config.consensus_signer = SignMode::Key(Secret::new(key.into()));
match db_type {
Some(DbType::RocksDb) => {
config.combined_db_config.database_type = DbType::RocksDb;
if let Some(db_path) = db_path {
config.combined_db_config.database_path = db_path;
}
config.historical_execution = historical_execution;
}
_ => {
config.combined_db_config.database_type = DbType::InMemory;
config.historical_execution = false;
}
}
config.block_production = match poa_instant {
true => Trigger::Instant,
false => Trigger::Never,
};
let ip = "127.0.0.1".parse().unwrap();
let port = port.unwrap_or(DEFAULT_PORT);
config.graphql_config.addr = std::net::SocketAddr::new(ip, port);
config.utxo_validation = false;
config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_coins_per_account_single_account_with_defaults() {
let base_asset_id = AssetId::default();
let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
let accounts = vec![account_id.to_string()];
let result = get_coins_per_account(accounts, &base_asset_id, 0);
assert!(result.is_ok());
let coins = result.unwrap();
assert_eq!(coins.len(), 1);
let coin = &coins[0];
let expected_owner = Owner::Address(Address::from_str(account_id).unwrap());
assert_eq!(coin.owner, expected_owner);
assert_eq!(coin.asset_id, base_asset_id);
assert_eq!(coin.amount, TESTNET_INITIAL_BALANCE);
assert_eq!(coin.output_index, 0);
}
#[test]
fn test_get_coins_per_account_with_custom_asset() {
let base_asset_id = AssetId::default();
let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
let asset_id = "0x0000000000000000000000000000000000000000000000000000000000000002";
let accounts = vec![format!("{}:{}", account_id, asset_id)];
let result = get_coins_per_account(accounts, &base_asset_id, 0);
assert!(result.is_ok());
let coins = result.unwrap();
assert_eq!(coins.len(), 1);
let coin = &coins[0];
let expected_owner = Owner::Address(Address::from_str(account_id).unwrap());
assert_eq!(coin.owner, expected_owner);
assert_eq!(coin.asset_id, AssetId::from_str(asset_id).unwrap());
assert_eq!(coin.amount, TESTNET_INITIAL_BALANCE);
assert_eq!(coin.output_index, 0);
}
#[test]
fn test_get_coins_per_account_with_custom_amount() {
let base_asset_id = AssetId::default();
let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
let asset_id = "0x0000000000000000000000000000000000000000000000000000000000000002";
let amount = 5000000u64;
let accounts = vec![format!("{}:{}:{}", account_id, asset_id, amount)];
let result = get_coins_per_account(accounts, &base_asset_id, 0);
assert!(result.is_ok());
let coins = result.unwrap();
assert_eq!(coins.len(), 1);
let coin = &coins[0];
let expected_owner = Owner::Address(Address::from_str(account_id).unwrap());
assert_eq!(coin.owner, expected_owner);
assert_eq!(coin.asset_id, AssetId::from_str(asset_id).unwrap());
assert_eq!(coin.amount, amount);
assert_eq!(coin.output_index, 0);
}
#[test]
fn test_get_coins_per_account_multiple_accounts() {
let base_asset_id = AssetId::default();
let account1 = "0x0000000000000000000000000000000000000000000000000000000000000001";
let account2 = "0x0000000000000000000000000000000000000000000000000000000000000002";
let accounts = vec![account1.to_string(), account2.to_string()];
let result = get_coins_per_account(accounts, &base_asset_id, 5);
assert!(result.is_ok());
let coins = result.unwrap();
assert_eq!(coins.len(), 2);
let coin1 = &coins[0];
let expected_owner1 = Owner::Address(Address::from_str(account1).unwrap());
assert_eq!(coin1.owner, expected_owner1);
assert_eq!(coin1.output_index, 5);
let coin2 = &coins[1];
let expected_owner2 = Owner::Address(Address::from_str(account2).unwrap());
assert_eq!(coin2.owner, expected_owner2);
assert_eq!(coin2.output_index, 6);
}
#[test]
fn test_get_coins_per_account_edge_cases_and_errors() {
let base_asset_id = AssetId::default();
let valid_account = "0x0000000000000000000000000000000000000000000000000000000000000001";
let valid_asset = "0x0000000000000000000000000000000000000000000000000000000000000002";
let result = get_coins_per_account(vec![], &base_asset_id, 0);
assert!(result.is_ok());
let coins = result.unwrap();
assert_eq!(coins.len(), 0);
let result =
get_coins_per_account(vec!["invalid_account_id".to_string()], &base_asset_id, 0);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid account ID: Invalid encoded byte in Address"
);
let result = get_coins_per_account(
vec![format!("{}:invalid_asset", valid_account)],
&base_asset_id,
0,
);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid asset ID: Invalid encoded byte in AssetId"
);
let result = get_coins_per_account(
vec![format!("{}:{}:not_a_number", valid_account, valid_asset)],
&base_asset_id,
0,
);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid amount: invalid digit found in string"
);
let result = get_coins_per_account(
vec!["part1:part2:part3:part4".to_string()],
&base_asset_id,
0,
);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid account format: part1:part2:part3:part4. Expected format: <account-id>[:asset-id[:amount]]"
);
let result = get_coins_per_account(vec!["".to_string()], &base_asset_id, 0);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid account ID: Invalid encoded byte in Address"
);
}
}