1use crate::consts::DEFAULT_PORT;
2use anyhow;
3use clap::Parser;
4use fuel_core::{
5 chain_config::default_consensus_dev_key,
6 service::{
7 config::{DbType, Trigger},
8 Config,
9 },
10};
11use fuel_core_chain_config::{
12 coin_config_helpers::CoinConfigGenerator, ChainConfig, CoinConfig, Owner, SnapshotMetadata,
13 TESTNET_INITIAL_BALANCE,
14};
15use fuel_core_types::{
16 fuel_crypto::fuel_types::{Address, AssetId},
17 secrecy::Secret,
18 signer::SignMode,
19};
20use std::{path::PathBuf, str::FromStr};
21
22#[derive(Parser, Debug, Clone)]
23pub struct LocalCmd {
24 #[clap(long)]
25 pub chain_config: Option<PathBuf>,
26
27 #[clap(long)]
28 pub port: Option<u16>,
29
30 #[clap(long)]
32 pub db_path: Option<PathBuf>,
33
34 #[clap(long = "db-type", value_enum)]
35 pub db_type: Option<DbType>,
36
37 #[clap(long)]
40 pub account: Vec<String>,
41
42 #[arg(long = "debug", env)]
43 pub debug: bool,
44
45 #[arg(long = "historical-execution", env)]
48 pub historical_execution: bool,
49
50 #[arg(long = "poa-instant", env)]
51 pub poa_instant: bool,
52}
53
54fn get_coins_per_account(
55 account_strings: Vec<String>,
56 base_asset_id: &AssetId,
57 current_coin_idx: usize,
58) -> anyhow::Result<Vec<CoinConfig>> {
59 let mut coin_generator = CoinConfigGenerator::new();
60 let mut coins = Vec::new();
61
62 for account_string in account_strings {
63 let parts: Vec<&str> = account_string.trim().split(':').collect();
64 let (owner, asset_id, amount) = match parts.as_slice() {
65 [owner_str] => {
66 let owner = Address::from_str(owner_str)
68 .map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
69 (owner, *base_asset_id, TESTNET_INITIAL_BALANCE)
70 }
71 [owner_str, asset_str] => {
72 let owner = Address::from_str(owner_str)
74 .map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
75 let asset_id = AssetId::from_str(asset_str)
76 .map_err(|e| anyhow::anyhow!("Invalid asset ID: {}", e))?;
77 (owner, asset_id, TESTNET_INITIAL_BALANCE)
78 }
79 [owner_str, asset_str, amount_str] => {
80 let owner = Address::from_str(owner_str)
82 .map_err(|e| anyhow::anyhow!("Invalid account ID: {}", e))?;
83 let asset_id = AssetId::from_str(asset_str)
84 .map_err(|e| anyhow::anyhow!("Invalid asset ID: {}", e))?;
85 let amount = amount_str
86 .parse::<u64>()
87 .map_err(|e| anyhow::anyhow!("Invalid amount: {}", e))?;
88 (owner, asset_id, amount)
89 }
90 _ => {
91 return Err(anyhow::anyhow!(
92 "Invalid account format: {}. Expected format: <account-id>[:asset-id[:amount]]",
93 account_string
94 ));
95 }
96 };
97 let coin = CoinConfig {
98 amount,
99 owner: owner.into(),
100 asset_id,
101 output_index: (current_coin_idx + coins.len()) as u16,
102 ..coin_generator.generate()
103 };
104 coins.push(coin);
105 }
106 Ok(coins)
107}
108
109impl From<LocalCmd> for Config {
110 fn from(cmd: LocalCmd) -> Self {
111 let LocalCmd {
112 chain_config,
113 port,
114 db_path,
115 db_type,
116 account,
117 debug,
118 historical_execution,
119 poa_instant,
120 ..
121 } = cmd;
122
123 let chain_config = match chain_config {
124 Some(path) => match SnapshotMetadata::read(&path) {
125 Ok(metadata) => ChainConfig::from_snapshot_metadata(&metadata).unwrap(),
126 Err(e) => {
127 tracing::error!("Failed to open snapshot reader: {}", e);
128 tracing::warn!("Using local testnet snapshot reader");
129 ChainConfig::local_testnet()
130 }
131 },
132 None => ChainConfig::local_testnet(),
133 };
134 let base_asset_id = chain_config.consensus_parameters.base_asset_id();
135
136 let mut state_config = fuel_core_chain_config::StateConfig::local_testnet();
138 state_config
139 .coins
140 .iter_mut()
141 .for_each(|coin| coin.asset_id = *base_asset_id);
142
143 let current_coin_idx = state_config.coins.len();
144 if !account.is_empty() {
145 let coins = get_coins_per_account(account, base_asset_id, current_coin_idx)
146 .map_err(|e| anyhow::anyhow!("Error parsing account funding: {}", e))
147 .unwrap();
148 if !coins.is_empty() {
149 tracing::info!("Additional accounts");
150 for coin in &coins {
151 let owner_hex = match &coin.owner {
152 Owner::Address(address) => format!("{address:#x}"),
153 Owner::SecretKey(secret_key) => format!("{secret_key:#x}"),
154 };
155 tracing::info!(
156 "Address({}), Asset ID({:#x}), Balance({})",
157 owner_hex,
158 coin.asset_id,
159 coin.amount
160 );
161 }
162 state_config.coins.extend(coins);
163 }
164 }
165
166 let mut config = Config::local_node_with_configs(chain_config, state_config);
167 config.name = "fuel-core".to_string();
168
169 config.debug = debug;
171 let key = default_consensus_dev_key();
172 config.consensus_signer = SignMode::Key(Secret::new(key.into()));
173
174 match db_type {
176 Some(DbType::RocksDb) => {
177 config.combined_db_config.database_type = DbType::RocksDb;
178 if let Some(db_path) = db_path {
179 config.combined_db_config.database_path = db_path;
180 }
181 config.historical_execution = historical_execution;
182 }
183 _ => {
184 config.combined_db_config.database_type = DbType::InMemory;
185 config.historical_execution = false;
186 }
187 }
188
189 config.block_production = match poa_instant {
190 true => Trigger::Instant,
191 false => Trigger::Never,
192 };
193
194 let ip = "127.0.0.1".parse().unwrap();
196 let port = port.unwrap_or(DEFAULT_PORT);
197 config.graphql_config.addr = std::net::SocketAddr::new(ip, port);
198
199 config.utxo_validation = false; config
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_get_coins_per_account_single_account_with_defaults() {
211 let base_asset_id = AssetId::default();
212 let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
213 let accounts = vec![account_id.to_string()];
214
215 let result = get_coins_per_account(accounts, &base_asset_id, 0);
216 assert!(result.is_ok());
217
218 let coins = result.unwrap();
219 assert_eq!(coins.len(), 1);
220
221 let coin = &coins[0];
222 let expected_owner = Owner::Address(Address::from_str(account_id).unwrap());
223 assert_eq!(coin.owner, expected_owner);
224 assert_eq!(coin.asset_id, base_asset_id);
225 assert_eq!(coin.amount, TESTNET_INITIAL_BALANCE);
226 assert_eq!(coin.output_index, 0);
227 }
228
229 #[test]
230 fn test_get_coins_per_account_with_custom_asset() {
231 let base_asset_id = AssetId::default();
232 let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
233 let asset_id = "0x0000000000000000000000000000000000000000000000000000000000000002";
234 let accounts = vec![format!("{}:{}", account_id, asset_id)];
235
236 let result = get_coins_per_account(accounts, &base_asset_id, 0);
237 assert!(result.is_ok());
238
239 let coins = result.unwrap();
240 assert_eq!(coins.len(), 1);
241
242 let coin = &coins[0];
243 let expected_owner = Owner::Address(Address::from_str(account_id).unwrap());
244 assert_eq!(coin.owner, expected_owner);
245 assert_eq!(coin.asset_id, AssetId::from_str(asset_id).unwrap());
246 assert_eq!(coin.amount, TESTNET_INITIAL_BALANCE);
247 assert_eq!(coin.output_index, 0);
248 }
249
250 #[test]
251 fn test_get_coins_per_account_with_custom_amount() {
252 let base_asset_id = AssetId::default();
253 let account_id = "0x0000000000000000000000000000000000000000000000000000000000000001";
254 let asset_id = "0x0000000000000000000000000000000000000000000000000000000000000002";
255 let amount = 5000000u64;
256 let accounts = vec![format!("{}:{}:{}", account_id, asset_id, amount)];
257
258 let result = get_coins_per_account(accounts, &base_asset_id, 0);
259 assert!(result.is_ok());
260
261 let coins = result.unwrap();
262 assert_eq!(coins.len(), 1);
263
264 let coin = &coins[0];
265 let expected_owner = Owner::Address(Address::from_str(account_id).unwrap());
266 assert_eq!(coin.owner, expected_owner);
267 assert_eq!(coin.asset_id, AssetId::from_str(asset_id).unwrap());
268 assert_eq!(coin.amount, amount);
269 assert_eq!(coin.output_index, 0);
270 }
271
272 #[test]
273 fn test_get_coins_per_account_multiple_accounts() {
274 let base_asset_id = AssetId::default();
275 let account1 = "0x0000000000000000000000000000000000000000000000000000000000000001";
276 let account2 = "0x0000000000000000000000000000000000000000000000000000000000000002";
277 let accounts = vec![account1.to_string(), account2.to_string()];
278
279 let result = get_coins_per_account(accounts, &base_asset_id, 5);
280 assert!(result.is_ok());
281
282 let coins = result.unwrap();
283 assert_eq!(coins.len(), 2);
284
285 let coin1 = &coins[0];
286 let expected_owner1 = Owner::Address(Address::from_str(account1).unwrap());
287 assert_eq!(coin1.owner, expected_owner1);
288 assert_eq!(coin1.output_index, 5);
289
290 let coin2 = &coins[1];
291 let expected_owner2 = Owner::Address(Address::from_str(account2).unwrap());
292 assert_eq!(coin2.owner, expected_owner2);
293 assert_eq!(coin2.output_index, 6);
294 }
295
296 #[test]
297 fn test_get_coins_per_account_edge_cases_and_errors() {
298 let base_asset_id = AssetId::default();
299 let valid_account = "0x0000000000000000000000000000000000000000000000000000000000000001";
300 let valid_asset = "0x0000000000000000000000000000000000000000000000000000000000000002";
301
302 let result = get_coins_per_account(vec![], &base_asset_id, 0);
304 assert!(result.is_ok());
305 let coins = result.unwrap();
306 assert_eq!(coins.len(), 0);
307
308 let result =
310 get_coins_per_account(vec!["invalid_account_id".to_string()], &base_asset_id, 0);
311 assert!(result.is_err());
312 assert_eq!(
313 result.unwrap_err().to_string(),
314 "Invalid account ID: Invalid encoded byte in Address"
315 );
316
317 let result = get_coins_per_account(
319 vec![format!("{}:invalid_asset", valid_account)],
320 &base_asset_id,
321 0,
322 );
323 assert!(result.is_err());
324 assert_eq!(
325 result.unwrap_err().to_string(),
326 "Invalid asset ID: Invalid encoded byte in AssetId"
327 );
328
329 let result = get_coins_per_account(
331 vec![format!("{}:{}:not_a_number", valid_account, valid_asset)],
332 &base_asset_id,
333 0,
334 );
335 assert!(result.is_err());
336 assert_eq!(
337 result.unwrap_err().to_string(),
338 "Invalid amount: invalid digit found in string"
339 );
340
341 let result = get_coins_per_account(
343 vec!["part1:part2:part3:part4".to_string()],
344 &base_asset_id,
345 0,
346 );
347 assert!(result.is_err());
348 assert_eq!(
349 result.unwrap_err().to_string(),
350 "Invalid account format: part1:part2:part3:part4. Expected format: <account-id>[:asset-id[:amount]]"
351 );
352
353 let result = get_coins_per_account(vec!["".to_string()], &base_asset_id, 0);
355 assert!(result.is_err());
356 assert_eq!(
357 result.unwrap_err().to_string(),
358 "Invalid account ID: Invalid encoded byte in Address"
359 );
360 }
361}