forc_node/local/
cmd.rs

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    /// If a db path is provided local node runs in persistent mode.
31    #[clap(long)]
32    pub db_path: Option<PathBuf>,
33
34    #[clap(long = "db-type", value_enum)]
35    pub db_type: Option<DbType>,
36
37    /// Fund accounts with the format: <account-id>:<asset-id>:<amount>
38    /// Multiple accounts can be provided via comma separation or multiple --account flags
39    #[clap(long)]
40    pub account: Vec<String>,
41
42    #[arg(long = "debug", env)]
43    pub debug: bool,
44
45    /// Allows execution of transactions based on past block, such as:
46    /// - Dry run in the past
47    #[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                // Only account-id provided, use default asset and amount
67                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                // account-id:asset-id provided, use default amount
73                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                // Full format: account-id:asset-id:amount
81                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        // Parse and validate account funding if provided
137        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        // Local-specific settings
170        config.debug = debug;
171        let key = default_consensus_dev_key();
172        config.consensus_signer = SignMode::Key(Secret::new(key.into()));
173
174        // Database configuration
175        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        // Network configuration
195        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; // local development
200
201        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        // Test empty input
303        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        // Test invalid account ID
309        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        // Test invalid asset ID
318        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        // Test invalid amount
330        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        // Test too many parts
342        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        // Test empty account (should fail now)
354        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}