use self::serialization::{HexNumber, HexType};
use crate::model::fuel_block::BlockHeight;
use fuel_types::{Address, Bytes32, Color, Salt};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, skip_serializing_none};
use std::{io::ErrorKind, path::PathBuf, str::FromStr};
pub mod serialization;
pub const LOCAL_TESTNET: &str = "local_testnet";
pub const TESTNET_INITIAL_BALANCE: u64 = 10_000_000;
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ChainConfig {
pub chain_name: String,
pub block_production: ProductionStrategy,
pub parent_network: BaseChainConfig,
#[serde(default)]
pub initial_state: Option<StateConfig>,
}
impl ChainConfig {
pub fn local_testnet() -> Self {
let initial_coins = (1..10)
.map(|idx| CoinConfig {
tx_id: None,
output_index: None,
block_created: None,
maturity: None,
owner: Address::new([idx; 32]),
amount: TESTNET_INITIAL_BALANCE,
color: Default::default(),
})
.collect_vec();
Self {
chain_name: LOCAL_TESTNET.to_string(),
block_production: ProductionStrategy::Instant,
parent_network: BaseChainConfig::LocalTest,
initial_state: Some(StateConfig {
coins: Some(initial_coins),
..StateConfig::default()
}),
}
}
}
impl FromStr for ChainConfig {
type Err = std::io::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
LOCAL_TESTNET => Ok(Self::local_testnet()),
s => {
let path = PathBuf::from(s.to_string());
let contents = std::fs::read(path)?;
serde_json::from_slice(&contents)
.map_err(|e| std::io::Error::new(ErrorKind::InvalidData, e))
}
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum ProductionStrategy {
Instant,
Manual,
RoundRobin,
ProofOfStake,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type")]
pub enum BaseChainConfig {
LocalTest,
Ethereum,
}
#[serde_as]
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct StateConfig {
pub coins: Option<Vec<CoinConfig>>,
pub contracts: Option<Vec<ContractConfig>>,
#[serde_as(as = "Option<HexNumber>")]
#[serde(default)]
pub height: Option<BlockHeight>,
}
#[skip_serializing_none]
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CoinConfig {
#[serde_as(as = "Option<HexType>")]
#[serde(default)]
pub tx_id: Option<Bytes32>,
#[serde_as(as = "Option<HexNumber>")]
#[serde(default)]
pub output_index: Option<u64>,
#[serde_as(as = "Option<HexNumber>")]
#[serde(default)]
pub block_created: Option<BlockHeight>,
#[serde_as(as = "Option<HexNumber>")]
#[serde(default)]
pub maturity: Option<BlockHeight>,
#[serde_as(as = "HexType")]
pub owner: Address,
#[serde_as(as = "HexNumber")]
pub amount: u64,
#[serde_as(as = "HexType")]
pub color: Color,
}
#[skip_serializing_none]
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ContractConfig {
#[serde_as(as = "HexType")]
pub code: Vec<u8>,
#[serde_as(as = "HexType")]
pub salt: Salt,
#[serde_as(as = "Option<Vec<(HexType, HexType)>>")]
#[serde(default)]
pub state: Option<Vec<(Bytes32, Bytes32)>>,
#[serde_as(as = "Option<Vec<(HexType, HexNumber)>>")]
#[serde(default)]
pub balances: Option<Vec<(Color, u64)>>,
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_asm::Opcode;
use fuel_vm::prelude::Contract;
use rand::prelude::StdRng;
use rand::{Rng, RngCore, SeedableRng};
use std::env::temp_dir;
use std::fs::write;
#[test]
fn from_str_loads_from_file() {
let tmp_file = tmp_path();
let disk_config = ChainConfig::local_testnet();
let json = serde_json::to_string_pretty(&disk_config).unwrap();
write(tmp_file.clone(), json).unwrap();
let load_config: ChainConfig = tmp_file.to_string_lossy().into_owned().parse().unwrap();
assert_eq!(disk_config, load_config);
}
#[test]
fn snapshot_local_testnet_config() {
let config = ChainConfig::local_testnet();
let json = serde_json::to_string_pretty(&config).unwrap();
insta::assert_snapshot!(json);
}
#[test]
fn can_roundtrip_serialize_local_testnet_config() {
let config = ChainConfig::local_testnet();
let json = serde_json::to_string(&config).unwrap();
let deserialized_config: ChainConfig = serde_json::from_str(json.as_str()).unwrap();
assert_eq!(config, deserialized_config);
}
#[test]
fn snapshot_configurable_block_height() {
let mut rng = StdRng::seed_from_u64(2);
let config = ChainConfig {
initial_state: Some(StateConfig {
height: Some(rng.next_u32().into()),
..Default::default()
}),
..ChainConfig::local_testnet()
};
let json = serde_json::to_string_pretty(&config).unwrap();
insta::assert_snapshot!(json);
}
#[test]
fn can_roundtrip_serialize_block_height_config() {
let mut rng = StdRng::seed_from_u64(2);
let config = ChainConfig {
initial_state: Some(StateConfig {
height: Some(rng.next_u32().into()),
..Default::default()
}),
..ChainConfig::local_testnet()
};
let json = serde_json::to_string(&config).unwrap();
let deserialized_config: ChainConfig = serde_json::from_str(json.as_str()).unwrap();
assert_eq!(config, deserialized_config);
}
#[test]
fn snapshot_simple_contract() {
let config = test_config_contract(false, false);
let json = serde_json::to_string_pretty(&config).unwrap();
insta::assert_snapshot!(json);
}
#[test]
fn can_roundtrip_simple_contract() {
let config = test_config_contract(false, false);
let json = serde_json::to_string(&config).unwrap();
let deserialized_config: ChainConfig = serde_json::from_str(json.as_str()).unwrap();
assert_eq!(config, deserialized_config);
}
#[test]
fn snapshot_contract_with_state() {
let config = test_config_contract(true, false);
let json = serde_json::to_string_pretty(&config).unwrap();
insta::assert_snapshot!(json);
}
#[test]
fn can_roundtrip_contract_with_state() {
let config = test_config_contract(true, false);
let json = serde_json::to_string(&config).unwrap();
let deserialized_config: ChainConfig = serde_json::from_str(json.as_str()).unwrap();
assert_eq!(config, deserialized_config);
}
#[test]
fn snapshot_contract_with_balances() {
let config = test_config_contract(false, true);
let json = serde_json::to_string_pretty(&config).unwrap();
insta::assert_snapshot!(json);
}
#[test]
fn can_roundtrip_contract_with_balances() {
let config = test_config_contract(false, true);
let json = serde_json::to_string(&config).unwrap();
let deserialized_config: ChainConfig = serde_json::from_str(json.as_str()).unwrap();
assert_eq!(config, deserialized_config);
}
#[test]
fn snapshot_simple_coin_state() {
let config = test_config_coin_state();
let json = serde_json::to_string_pretty(&config).unwrap();
insta::assert_snapshot!(json);
}
#[test]
fn can_roundtrip_simple_coin_state() {
let config = test_config_coin_state();
let json = serde_json::to_string(&config).unwrap();
let deserialized_config: ChainConfig = serde_json::from_str(json.as_str()).unwrap();
assert_eq!(config, deserialized_config);
}
fn test_config_contract(state: bool, balances: bool) -> ChainConfig {
let mut rng = StdRng::seed_from_u64(1);
let state = if state {
let test_key: Bytes32 = rng.gen();
let test_value: Bytes32 = rng.gen();
Some(vec![(test_key, test_value)])
} else {
None
};
let balances = if balances {
let test_color: Color = rng.gen();
let test_balance: u64 = rng.next_u64();
Some(vec![(test_color, test_balance)])
} else {
None
};
let contract = Contract::from(Opcode::RET(0x10).to_bytes().to_vec());
ChainConfig {
initial_state: Some(StateConfig {
contracts: Some(vec![ContractConfig {
code: contract.into(),
salt: Default::default(),
state,
balances,
}]),
..Default::default()
}),
..ChainConfig::local_testnet()
}
}
fn test_config_coin_state() -> ChainConfig {
let mut rng = StdRng::seed_from_u64(1);
let tx_id: Option<Bytes32> = Some(rng.gen());
let output_index: Option<u64> = Some(rng.gen());
let block_created = Some(rng.next_u32().into());
let maturity = Some(rng.next_u32().into());
let owner = rng.gen();
let amount = rng.gen();
let color = rng.gen();
ChainConfig {
initial_state: Some(StateConfig {
coins: Some(vec![CoinConfig {
tx_id,
output_index,
block_created,
maturity,
owner,
amount,
color,
}]),
..Default::default()
}),
..ChainConfig::local_testnet()
}
}
fn tmp_path() -> PathBuf {
let mut path = temp_dir();
path.push(rand::random::<u16>().to_string());
path
}
}