use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use crate::account::PublicId;
pub struct ChainOptions<'a> {
pub home: &'a str,
pub block_time: &'a str,
pub faucet: &'a PublicId,
pub faucet_balance: u64,
pub max_txs_per_block: u32,
pub genesis_accounts: &'a [(PublicId, u64)],
}
pub const MAX_TXS_PER_BLOCK: std::ops::RangeInclusive<u32> = 1..=1000;
pub fn check_max_txs_per_block(max: u32) -> Result<()> {
if !MAX_TXS_PER_BLOCK.contains(&max) {
bail!(
"max_txs_per_block must be {} to {}",
MAX_TXS_PER_BLOCK.start(),
MAX_TXS_PER_BLOCK.end()
);
}
Ok(())
}
pub fn patch(mut debug: Value, opts: &ChainOptions) -> Result<Value> {
let config = debug
.as_object_mut()
.context("the upstream sequencer config is not a JSON object")?;
config.insert("home".into(), json!(opts.home));
config.insert("block_create_timeout".into(), json!(opts.block_time));
check_max_txs_per_block(opts.max_txs_per_block)?;
config.insert("max_num_tx_in_block".into(), json!(opts.max_txs_per_block));
config.insert("metrics_address".into(), Value::Null);
config.insert("gossip".into(), Value::Null);
let genesis = config
.entry("genesis")
.or_insert_with(|| json!([]))
.as_array_mut()
.context("the upstream config's `genesis` is not a list")?;
genesis.retain(|action| action.get("supply_account").is_none());
genesis.push(json!({
"supply_account": { "account_id": opts.faucet.as_str(), "balance": opts.faucet_balance }
}));
for (account, balance) in opts.genesis_accounts {
genesis.push(json!({
"supply_account": { "account_id": account.as_str(), "balance": balance }
}));
}
let block_secs = block_time_secs(opts.block_time)?;
if let Some(timeout) = config
.get("bedrock_config")
.and_then(|b| b["channel_params"]["posting_timeout"].as_u64())
&& block_secs >= timeout as f64
{
bail!(
"block time {} must be under the channel's posting_timeout of {timeout}s",
opts.block_time
);
}
Ok(debug)
}
pub fn block_time_secs(text: &str) -> Result<f64> {
let (number, unit) = text
.find(|c: char| !c.is_ascii_digit())
.map(|i| text.split_at(i))
.context("a block time needs a unit, such as 1s or 500ms")?;
let number: f64 = number
.parse()
.with_context(|| format!("not a block time: {text}"))?;
let secs = match unit {
"ms" => number / 1000.0,
"s" => number,
"m" => number * 60.0,
_ => bail!("not a block time: {text} (use ms, s or m)"),
};
if secs == 0.0 {
bail!("the block time can't be zero");
}
Ok(secs)
}
#[cfg(test)]
mod tests {
use super::*;
fn upstream() -> Value {
json!({
"home": ".",
"block_create_timeout": "15s",
"bedrock_config": { "channel_params": { "posting_timeout": 25 } },
"genesis": [
{ "supply_account": { "account_id": "Stock", "balance": 10 } },
{ "stake_sequencer": { "sequencer_key": "k" } }
],
"future_field": 7
})
}
const FAUCET: &str = "HpavNGFYxjyhLsy171aXsvv146yUeXmRWiho22uvT43d";
const USER: &str = "6YvesSEDWW25kpi8SSki2c8o5m3uACsigvGnNQ6wMruh";
fn patched_with(block_time: &str, max_txs_per_block: u32) -> Result<Value> {
let faucet = PublicId::parse(FAUCET).unwrap();
let users = [(PublicId::parse(USER).unwrap(), 7)];
patch(
upstream(),
&ChainOptions {
home: "/devnet/chain",
block_time,
faucet: &faucet,
faucet_balance: 1_000,
max_txs_per_block,
genesis_accounts: &users,
},
)
}
fn patched(block_time: &str) -> Result<Value> {
patched_with(block_time, 20)
}
#[test]
fn sets_home_and_block_time_and_turns_listeners_off() {
let out = patched("1s").unwrap();
assert_eq!(out["home"], "/devnet/chain");
assert_eq!(out["block_create_timeout"], "1s");
assert!(out["metrics_address"].is_null());
assert!(out["gossip"].is_null());
assert_eq!(out["future_field"], 7, "fields we don't know about stay");
}
#[test]
fn funds_only_the_faucet_and_the_named_accounts() {
let out = patched("1s").unwrap();
let genesis = out["genesis"].as_array().unwrap();
assert_eq!(
genesis,
&vec![
json!({ "stake_sequencer": { "sequencer_key": "k" } }),
json!({ "supply_account": { "account_id": FAUCET, "balance": 1000 } }),
json!({ "supply_account": { "account_id": USER, "balance": 7 } }),
]
);
}
#[test]
fn sets_and_bounds_the_transactions_per_block() {
assert_eq!(patched_with("1s", 200).unwrap()["max_num_tx_in_block"], 200);
assert!(patched_with("1s", 0).is_err());
assert!(patched_with("1s", 1001).is_err());
}
#[test]
fn refuses_a_block_time_at_or_over_the_posting_timeout() {
assert!(patched("25s").is_err());
assert!(patched("500ms").is_ok());
}
#[test]
fn refuses_malformed_block_times() {
for bad in ["1", "s", "1h", "0s", ""] {
assert!(block_time_secs(bad).is_err(), "{bad:?}");
}
assert_eq!(block_time_secs("2m").unwrap(), 120.0);
}
}