1- use crate :: chain_config:: ChainConfig ;
21use crate :: consts:: DEFAULT_PORT ;
2+ use anyhow;
33use clap:: Parser ;
44use 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 ) ]
1017pub 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+ }
0 commit comments