Skip to content

Commit fa86425

Browse files
authored
Merge branch 'master' into josh/doc-lib-std
2 parents 4bd84bd + 3210f94 commit fa86425

File tree

2 files changed

+260
-14
lines changed

2 files changed

+260
-14
lines changed

forc-plugins/forc-node/src/local/cmd.rs

Lines changed: 259 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
use crate::chain_config::ChainConfig;
21
use crate::consts::DEFAULT_PORT;
2+
use anyhow;
33
use clap::Parser;
44
use fuel_core::{chain_config::default_consensus_dev_key, service::Config};
5-
use fuel_core_chain_config::{SnapshotMetadata, SnapshotReader};
6-
use fuel_core_types::{secrecy::Secret, signer::SignMode};
7-
use std::path::PathBuf;
5+
use fuel_core_chain_config::{
6+
coin_config_helpers::CoinConfigGenerator, ChainConfig, CoinConfig, SnapshotMetadata,
7+
TESTNET_INITIAL_BALANCE,
8+
};
9+
use fuel_core_types::{
10+
fuel_crypto::fuel_types::{Address, AssetId},
11+
secrecy::Secret,
12+
signer::SignMode,
13+
};
14+
use std::{path::PathBuf, str::FromStr};
815

916
#[derive(Parser, Debug, Clone)]
1017
pub struct LocalCmd {
@@ -15,26 +22,111 @@ pub struct LocalCmd {
1522
#[clap(long)]
1623
/// If a db path is provided local node runs in persistent mode.
1724
pub db_path: Option<PathBuf>,
25+
#[clap(long)]
26+
/// Fund accounts with the format: <account-id>:<asset-id>:<amount>
27+
/// Multiple accounts can be provided via comma separation or multiple --account flags
28+
pub account: Vec<String>,
1829
}
1930

20-
impl From<LocalCmd> for Config {
21-
fn from(cmd: LocalCmd) -> Self {
22-
let mut config = Config::local_node();
31+
fn get_coins_per_account(
32+
account_strings: Vec<String>,
33+
base_asset_id: &AssetId,
34+
current_coin_idx: usize,
35+
) -> anyhow::Result<Vec<CoinConfig>> {
36+
let mut coin_generator = CoinConfigGenerator::new();
37+
let mut coins = Vec::new();
2338

24-
config.name = "fuel-core".to_string();
39+
for account_string in account_strings {
40+
let parts: Vec<&str> = account_string.trim().split(':').collect();
41+
let (owner, asset_id, amount) = match parts.as_slice() {
42+
[owner_str] => {
43+
// Only account-id provided, use default asset and amount
44+
let owner = Address::from_str(owner_str)
45+
.map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
46+
(owner, *base_asset_id, TESTNET_INITIAL_BALANCE)
47+
}
48+
[owner_str, asset_str] => {
49+
// account-id:asset-id provided, use default amount
50+
let owner = Address::from_str(owner_str)
51+
.map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
52+
let asset_id = AssetId::from_str(asset_str)
53+
.map_err(|e| anyhow::anyhow!("Invalid asset ID: {}", e))?;
54+
(owner, asset_id, TESTNET_INITIAL_BALANCE)
55+
}
56+
[owner_str, asset_str, amount_str] => {
57+
// Full format: account-id:asset-id:amount
58+
let owner = Address::from_str(owner_str)
59+
.map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
60+
let asset_id = AssetId::from_str(asset_str)
61+
.map_err(|e| anyhow::anyhow!("Invalid asset ID: {}", e))?;
62+
let amount = amount_str
63+
.parse::<u64>()
64+
.map_err(|e| anyhow::anyhow!("Invalid amount: {}", e))?;
65+
(owner, asset_id, amount)
66+
}
67+
_ => {
68+
return Err(anyhow::anyhow!(
69+
"Invalid account format: {}. Expected format: <account-id>[:asset-id[:amount]]",
70+
account_string
71+
));
72+
}
73+
};
74+
let coin = CoinConfig {
75+
amount,
76+
owner,
77+
asset_id,
78+
output_index: (current_coin_idx + coins.len()) as u16,
79+
..coin_generator.generate()
80+
};
81+
coins.push(coin);
82+
}
83+
Ok(coins)
84+
}
2585

26-
// Handle chain config/snapshot
86+
impl From<LocalCmd> for Config {
87+
fn from(cmd: LocalCmd) -> Self {
2788
let snapshot_path = cmd
2889
.chain_config
29-
.unwrap_or_else(|| ChainConfig::Local.into());
30-
if snapshot_path.exists() {
31-
if let Ok(metadata) = SnapshotMetadata::read(&snapshot_path) {
32-
if let Ok(reader) = SnapshotReader::open(metadata) {
33-
config.snapshot_reader = reader;
90+
.unwrap_or_else(|| crate::chain_config::ChainConfig::Local.into());
91+
let chain_config = match SnapshotMetadata::read(&snapshot_path) {
92+
Ok(metadata) => ChainConfig::from_snapshot_metadata(&metadata).unwrap(),
93+
Err(e) => {
94+
tracing::error!("Failed to open snapshot reader: {}", e);
95+
tracing::warn!("Using local testnet snapshot reader");
96+
ChainConfig::local_testnet()
97+
}
98+
};
99+
let base_asset_id = chain_config.consensus_parameters.base_asset_id();
100+
101+
// Parse and validate account funding if provided
102+
let mut state_config = fuel_core_chain_config::StateConfig::local_testnet();
103+
state_config
104+
.coins
105+
.iter_mut()
106+
.for_each(|coin| coin.asset_id = *base_asset_id);
107+
108+
let current_coin_idx = state_config.coins.len();
109+
if !cmd.account.is_empty() {
110+
let coins = get_coins_per_account(cmd.account, base_asset_id, current_coin_idx)
111+
.map_err(|e| anyhow::anyhow!("Error parsing account funding: {}", e))
112+
.unwrap();
113+
if !coins.is_empty() {
114+
tracing::info!("Additional accounts");
115+
for coin in &coins {
116+
tracing::info!(
117+
"Address({:#x}), Asset ID({:#x}), Balance({})",
118+
coin.owner,
119+
coin.asset_id,
120+
coin.amount
121+
);
34122
}
123+
state_config.coins.extend(coins);
35124
}
36125
}
37126

127+
let mut config = Config::local_node_with_configs(chain_config, state_config);
128+
config.name = "fuel-core".to_string();
129+
38130
// Local-specific settings
39131
config.debug = true;
40132
let key = default_consensus_dev_key();
@@ -59,3 +151,156 @@ impl From<LocalCmd> for Config {
59151
config
60152
}
61153
}
154+
155+
#[cfg(test)]
156+
mod tests {
157+
use super::*;
158+
159+
#[test]
160+
fn test_get_coins_per_account_single_account_with_defaults() {
161+
let base_asset_id = AssetId::default();
162+
let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
163+
let accounts = vec![account_id.to_string()];
164+
165+
let result = get_coins_per_account(accounts, &base_asset_id, 0);
166+
assert!(result.is_ok());
167+
168+
let coins = result.unwrap();
169+
assert_eq!(coins.len(), 1);
170+
171+
let coin = &coins[0];
172+
assert_eq!(coin.owner, Address::from_str(account_id).unwrap());
173+
assert_eq!(coin.asset_id, base_asset_id);
174+
assert_eq!(coin.amount, TESTNET_INITIAL_BALANCE);
175+
assert_eq!(coin.output_index, 0);
176+
}
177+
178+
#[test]
179+
fn test_get_coins_per_account_with_custom_asset() {
180+
let base_asset_id = AssetId::default();
181+
let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
182+
let asset_id = "0x0000000000000000000000000000000000000000000000000000000000000002";
183+
let accounts = vec![format!("{}:{}", account_id, asset_id)];
184+
185+
let result = get_coins_per_account(accounts, &base_asset_id, 0);
186+
assert!(result.is_ok());
187+
188+
let coins = result.unwrap();
189+
assert_eq!(coins.len(), 1);
190+
191+
let coin = &coins[0];
192+
assert_eq!(coin.owner, Address::from_str(account_id).unwrap());
193+
assert_eq!(coin.asset_id, AssetId::from_str(asset_id).unwrap());
194+
assert_eq!(coin.amount, TESTNET_INITIAL_BALANCE);
195+
assert_eq!(coin.output_index, 0);
196+
}
197+
198+
#[test]
199+
fn test_get_coins_per_account_with_custom_amount() {
200+
let base_asset_id = AssetId::default();
201+
let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
202+
let asset_id = "0x0000000000000000000000000000000000000000000000000000000000000002";
203+
let amount = 5000000u64;
204+
let accounts = vec![format!("{}:{}:{}", account_id, asset_id, amount)];
205+
206+
let result = get_coins_per_account(accounts, &base_asset_id, 0);
207+
assert!(result.is_ok());
208+
209+
let coins = result.unwrap();
210+
assert_eq!(coins.len(), 1);
211+
212+
let coin = &coins[0];
213+
assert_eq!(coin.owner, Address::from_str(account_id).unwrap());
214+
assert_eq!(coin.asset_id, AssetId::from_str(asset_id).unwrap());
215+
assert_eq!(coin.amount, amount);
216+
assert_eq!(coin.output_index, 0);
217+
}
218+
219+
#[test]
220+
fn test_get_coins_per_account_multiple_accounts() {
221+
let base_asset_id = AssetId::default();
222+
let account1 = "0x0000000000000000000000000000000000000000000000000000000000000001";
223+
let account2 = "0x0000000000000000000000000000000000000000000000000000000000000002";
224+
let accounts = vec![account1.to_string(), account2.to_string()];
225+
226+
let result = get_coins_per_account(accounts, &base_asset_id, 5);
227+
assert!(result.is_ok());
228+
229+
let coins = result.unwrap();
230+
assert_eq!(coins.len(), 2);
231+
232+
let coin1 = &coins[0];
233+
assert_eq!(coin1.owner, Address::from_str(account1).unwrap());
234+
assert_eq!(coin1.output_index, 5);
235+
236+
let coin2 = &coins[1];
237+
assert_eq!(coin2.owner, Address::from_str(account2).unwrap());
238+
assert_eq!(coin2.output_index, 6);
239+
}
240+
241+
#[test]
242+
fn test_get_coins_per_account_edge_cases_and_errors() {
243+
let base_asset_id = AssetId::default();
244+
let valid_account = "0x0000000000000000000000000000000000000000000000000000000000000001";
245+
let valid_asset = "0x0000000000000000000000000000000000000000000000000000000000000002";
246+
247+
// Test empty input
248+
let result = get_coins_per_account(vec![], &base_asset_id, 0);
249+
assert!(result.is_ok());
250+
let coins = result.unwrap();
251+
assert_eq!(coins.len(), 0);
252+
253+
// Test invalid account ID
254+
let result =
255+
get_coins_per_account(vec!["invalid_account_id".to_string()], &base_asset_id, 0);
256+
assert!(result.is_err());
257+
assert_eq!(
258+
result.unwrap_err().to_string(),
259+
"Invalid account ID: Invalid encoded byte in Address"
260+
);
261+
262+
// Test invalid asset ID
263+
let result = get_coins_per_account(
264+
vec![format!("{}:invalid_asset", valid_account)],
265+
&base_asset_id,
266+
0,
267+
);
268+
assert!(result.is_err());
269+
assert_eq!(
270+
result.unwrap_err().to_string(),
271+
"Invalid asset ID: Invalid encoded byte in AssetId"
272+
);
273+
274+
// Test invalid amount
275+
let result = get_coins_per_account(
276+
vec![format!("{}:{}:not_a_number", valid_account, valid_asset)],
277+
&base_asset_id,
278+
0,
279+
);
280+
assert!(result.is_err());
281+
assert_eq!(
282+
result.unwrap_err().to_string(),
283+
"Invalid amount: invalid digit found in string"
284+
);
285+
286+
// Test too many parts
287+
let result = get_coins_per_account(
288+
vec!["part1:part2:part3:part4".to_string()],
289+
&base_asset_id,
290+
0,
291+
);
292+
assert!(result.is_err());
293+
assert_eq!(
294+
result.unwrap_err().to_string(),
295+
"Invalid account format: part1:part2:part3:part4. Expected format: <account-id>[:asset-id[:amount]]"
296+
);
297+
298+
// Test empty account (should fail now)
299+
let result = get_coins_per_account(vec!["".to_string()], &base_asset_id, 0);
300+
assert!(result.is_err());
301+
assert_eq!(
302+
result.unwrap_err().to_string(),
303+
"Invalid account ID: Invalid encoded byte in Address"
304+
);
305+
}
306+
}

forc-plugins/forc-node/tests/local.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ async fn start_local_node_check_health() {
1212
chain_config: None,
1313
port: Some(port),
1414
db_path: None,
15+
account: vec![],
1516
};
1617

1718
let _service = run(local_cmd, false).await.unwrap().unwrap();

0 commit comments

Comments
 (0)