From f6067ed72fc52cf94bdb1d592d3c33d8f28c09f2 Mon Sep 17 00:00:00 2001 From: stringhandler Date: Tue, 4 Aug 2026 16:45:13 +0200 Subject: [PATCH] chore: add ci --- .github/workflows/ci.yml | 82 + txmanifest_lib/examples/covaddr.rs | 23 +- .../examples/factory_opreturn_recon.rs | 17 +- txmanifest_lib/examples/factory_recon.rs | 12 +- txmanifest_lib/examples/gen_schema.rs | 4 +- .../examples/lending_active_recon.rs | 165 +- txmanifest_lib/examples/lending_recon.rs | 159 +- txmanifest_lib/examples/opreturn_recon.rs | 17 +- txmanifest_lib/examples/prelock_recon.rs | 168 +- txmanifest_lib/src/canonical.rs | 20 +- txmanifest_lib/src/config.rs | 3 +- txmanifest_lib/src/context.rs | 7 +- txmanifest_lib/src/covenant.rs | 93 +- txmanifest_lib/src/describe.rs | 120 +- txmanifest_lib/src/eval.rs | 148 +- txmanifest_lib/src/instance.rs | 18 +- txmanifest_lib/src/lib.rs | 4 +- txmanifest_lib/src/lifecycle.rs | 2217 ++++++++++++----- txmanifest_lib/src/manifest.rs | 21 +- txmanifest_lib/src/params.rs | 14 +- txmanifest_lib/src/prepare.rs | 95 +- txmanifest_lib/src/preview.rs | 209 +- txmanifest_lib/src/prompt.rs | 20 +- txmanifest_lib/src/pset_builder.rs | 278 ++- txmanifest_lib/src/schema.rs | 5 +- txmanifest_lib/src/state.rs | 28 +- txmanifest_lib/src/validate.rs | 153 +- txmanifest_lib/src/wallet.rs | 83 +- txmanifest_lib/tests/examples_parse.rs | 5 +- txmanifest_lib/tests/schema.rs | 14 +- txmanifest_lib/tests/ui_coverage.rs | 10 +- txmanifest_wallet/src/main.rs | 170 +- 32 files changed, 3276 insertions(+), 1106 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..89dad5a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,82 @@ +name: CI + +# Build and test on every push to main and every pull request. +# +# The `simplicityhl` git dependency dominates build time, so both jobs lean on +# `Swatinem/rust-cache` and run with `--locked`: the lock file pins that dependency to +# an exact rev, and a build that silently updated it would no longer be testing what a +# release builds. +on: + push: + branches: [main] + pull_request: + +# A new push to a PR makes the in-flight run obsolete; don't pay to finish it. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + # Linux is the reference platform; Windows is what the project is developed on + # and what `tests/cli_parses_examples.rs` exercises path handling against. + os: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + # Plain `cargo test` (not `--all-targets`) so doc-tests run too. This also + # compiles `examples/`, which is what keeps `gen_schema` from bit-rotting. + # + # Deliberately NOT `--all-features`: the `simplicity_eval` feature needs the + # `compile_function` fork of SimplicityHL, and does not compile against the + # upstream master rev this workspace pins. See txmanifest_lib/Cargo.toml. + - name: Test + run: cargo test --workspace --locked + + lint: + name: lint + runs-on: ubuntu-latest + timeout-minutes: 30 + # NON-BLOCKING for now: the tree is not yet rustfmt-clean and clippy reports 22 + # warnings (all warnings, no errors). This job exists to make both visible on a PR + # without gating merges on a cleanup nobody has done yet. + # + # To turn it into a real gate: run `cargo fmt --all`, clear the clippy warnings + # (`cargo clippy --fix` handles most), then delete this line and add + # `-- -D warnings` to the clippy step. + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + - name: Formatting + run: cargo fmt --all --check + + - name: Clippy + # `--all-targets` so tests and examples are linted too — that is where the + # shadowed-binding and unused-variable classes of bug actually show up. + run: cargo clippy --workspace --all-targets --locked diff --git a/txmanifest_lib/examples/covaddr.rs b/txmanifest_lib/examples/covaddr.rs index 9d22415..e36281a 100644 --- a/txmanifest_lib/examples/covaddr.rs +++ b/txmanifest_lib/examples/covaddr.rs @@ -23,12 +23,23 @@ fn main() { hints.insert("SCRIPT_HASH".to_string(), "bytes32".to_string()); let tapleaf = covenant::compute_tapleaf_hash(&simf, ¶ms, &hints, true).unwrap(); - let spk_hash = - covenant::compute_covenant_script_hash(&simf, ¶ms, &hints, ElementsNetwork::LiquidTestnet, true) - .unwrap(); - let addr = - covenant::compute_covenant_address(&simf, ¶ms, &hints, &[], ElementsNetwork::LiquidTestnet, true) - .unwrap(); + let spk_hash = covenant::compute_covenant_script_hash( + &simf, + ¶ms, + &hints, + ElementsNetwork::LiquidTestnet, + true, + ) + .unwrap(); + let addr = covenant::compute_covenant_address( + &simf, + ¶ms, + &hints, + &[], + ElementsNetwork::LiquidTestnet, + true, + ) + .unwrap(); eprintln!("---- result ----"); println!("simf = {}", simf.display()); diff --git a/txmanifest_lib/examples/factory_opreturn_recon.rs b/txmanifest_lib/examples/factory_opreturn_recon.rs index 20e5e21..08606c1 100644 --- a/txmanifest_lib/examples/factory_opreturn_recon.rs +++ b/txmanifest_lib/examples/factory_opreturn_recon.rs @@ -5,12 +5,14 @@ // reissuance_flags (8, u64 LE) // The program-id derivation is done HERE (authoring time); the tx-encoder now sees only a // plain `bytes` constant (the manifest's FACTORY_PROGRAM_ID param default). -use std::collections::HashMap; use lwk_wollet::elements::hashes::{sha256, Hash}; +use std::collections::HashMap; use tx_manifest_lib::{context::ExecutionContext, eval}; fn program_id(simf_path: &std::path::Path) -> String { - let src = std::fs::read_to_string(simf_path).unwrap().replace("\r\n", "\n"); + let src = std::fs::read_to_string(simf_path) + .unwrap() + .replace("\r\n", "\n"); let h = sha256::Hash::hash(src.as_bytes()).to_byte_array(); h[..4].iter().map(|b| format!("{b:02x}")).collect() } @@ -37,9 +39,16 @@ fn main() { println!("creation op_return = {hex} ({} bytes)", bytes.len()); println!(" program_id = {pid}"); println!(" issuing_utxos_count= {}", bytes[4]); - println!(" reissuance_flags = {}", u64::from_le_bytes(bytes[5..13].try_into().unwrap())); + println!( + " reissuance_flags = {}", + u64::from_le_bytes(bytes[5..13].try_into().unwrap()) + ); assert_eq!(bytes.len(), 13, "creation metadata must be 13 bytes"); assert_eq!(bytes[4], 2, "issuing_utxos_count must be 2"); - assert_eq!(&bytes[5..13], &0u64.to_le_bytes(), "reissuance_flags must be 0"); + assert_eq!( + &bytes[5..13], + &0u64.to_le_bytes(), + "reissuance_flags must be 0" + ); println!("OK: layout matches IssuanceFactoryCreationMetadata::encode"); } diff --git a/txmanifest_lib/examples/factory_recon.rs b/txmanifest_lib/examples/factory_recon.rs index 197fc88..9048f12 100644 --- a/txmanifest_lib/examples/factory_recon.rs +++ b/txmanifest_lib/examples/factory_recon.rs @@ -1,6 +1,6 @@ // Reproduce the issuance_factory covenant (out[1]) and compare to on-chain. -use std::collections::HashMap; use lwk_wollet::ElementsNetwork; +use std::collections::HashMap; use tx_manifest_lib::covenant; fn main() { @@ -11,7 +11,15 @@ fn main() { h.insert("ISSUING_UTXOS_COUNT".to_string(), "u8".to_string()); p.insert("REISSUANCE_FLAGS".to_string(), "0".to_string()); h.insert("REISSUANCE_FLAGS".to_string(), "u64".to_string()); - let addr = covenant::compute_covenant_address(&d.join("issuance_factory.simf"), &p, &h, &[], ElementsNetwork::LiquidTestnet, true).unwrap(); + let addr = covenant::compute_covenant_address( + &d.join("issuance_factory.simf"), + &p, + &h, + &[], + ElementsNetwork::LiquidTestnet, + true, + ) + .unwrap(); eprintln!("---- result ----"); println!("factory out[1] spk (repro) = {:x}", addr.script_pubkey()); println!("factory out[1] spk (chain) = 5120456881785cc7d561caaa059e02f1a2823066bd860423996bea3e92c621bb064b"); diff --git a/txmanifest_lib/examples/gen_schema.rs b/txmanifest_lib/examples/gen_schema.rs index b7eeed8..a3ba469 100644 --- a/txmanifest_lib/examples/gen_schema.rs +++ b/txmanifest_lib/examples/gen_schema.rs @@ -13,7 +13,9 @@ use tx_manifest_lib::schema::{json_schema_string, SCHEMA_PATH}; fn main() -> anyhow::Result<()> { // Examples run with the crate root as CWD; the schema lives at the workspace root. - let out = Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join(SCHEMA_PATH); + let out = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join(SCHEMA_PATH); if let Some(dir) = out.parent() { std::fs::create_dir_all(dir)?; } diff --git a/txmanifest_lib/examples/lending_active_recon.rs b/txmanifest_lib/examples/lending_active_recon.rs index 874ab9a..1933bf2 100644 --- a/txmanifest_lib/examples/lending_active_recon.rs +++ b/txmanifest_lib/examples/lending_active_recon.rs @@ -2,15 +2,23 @@ // address changes between the pending and active states because slot0 (is_active) flips. // Pending (is_active=0) is anchored on-chain (out[5] of live offer 43ab4efe); the active // address is the same params with slot0 byte[31]=1 — computed by the same verified machinery. -use std::collections::HashMap; use lwk_wollet::ElementsNetwork; +use std::collections::HashMap; use tx_manifest_lib::covenant; -fn add(p: &mut HashMap, h: &mut HashMap, k: &str, v: &str, t: &str) { +fn add( + p: &mut HashMap, + h: &mut HashMap, + k: &str, + v: &str, + t: &str, +) { p.insert(k.to_string(), v.to_string()); h.insert(k.to_string(), t.to_string()); } -fn hx(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() } +fn hx(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} fn main() { let net = ElementsNetwork::LiquidTestnet; @@ -21,22 +29,63 @@ fn main() { // Live offer 43ab4efe params (same as lending_recon.rs). let collateral = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; - let principal = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; + let principal = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; let borrower_nft = "78d61185c79f855fac51a87c191b00266f02d28752f50b3d9092ccf6b978181e"; let lender_nft = "213462821a5cdb96f435f5ea6597e8937359d6fd5a64b6ac8ef4262bc279fcfb"; let protocol_fee = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; let z32 = "00".repeat(32); - let vault = |is_active: &str, finalized_hash: &str, keeper: &str, keeper_burn: &str, supplier_burn: &str| -> (HashMap, HashMap) { + let vault = |is_active: &str, + finalized_hash: &str, + keeper: &str, + keeper_burn: &str, + supplier_burn: &str| + -> (HashMap, HashMap) { let (mut p, mut h) = (HashMap::new(), HashMap::new()); - add(&mut p, &mut h, "VAULT_ASSET_ID", principal, "liquid.asset_id"); - add(&mut p, &mut h, "KEEPER_AUTH_ASSET_ID", keeper, "liquid.asset_id"); - add(&mut p, &mut h, "SUPPLIER_AUTH_ASSET_ID", borrower_nft, "liquid.asset_id"); + add( + &mut p, + &mut h, + "VAULT_ASSET_ID", + principal, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "KEEPER_AUTH_ASSET_ID", + keeper, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "SUPPLIER_AUTH_ASSET_ID", + borrower_nft, + "liquid.asset_id", + ); add(&mut p, &mut h, "KEEPER_AUTH_ASSET_AMOUNT", "1", "u64"); - add(&mut p, &mut h, "FINALIZED_VAULT_COV_HASH", finalized_hash, "bytes32"); + add( + &mut p, + &mut h, + "FINALIZED_VAULT_COV_HASH", + finalized_hash, + "bytes32", + ); add(&mut p, &mut h, "IS_ACTIVE", is_active, "bool"); - add(&mut p, &mut h, "WITH_KEEPER_ASSET_BURN", keeper_burn, "bool"); - add(&mut p, &mut h, "WITH_SUPPLIER_ASSET_BURN", supplier_burn, "bool"); + add( + &mut p, + &mut h, + "WITH_KEEPER_ASSET_BURN", + keeper_burn, + "bool", + ); + add( + &mut p, + &mut h, + "WITH_SUPPLIER_ASSET_BURN", + supplier_burn, + "bool", + ); (p, h) }; let (fp, fh) = vault("false", &z32, lender_nft, "true", "true"); @@ -48,25 +97,85 @@ fn main() { let (ap2, ah2) = vault("true", &f_proto, protocol_fee, "false", "true"); let a_proto = sh("asset_auth_vault.simf", &ap2, &ah2); let (mut pp, mut ph) = (HashMap::new(), HashMap::new()); - add(&mut pp, &mut ph, "ASSET_ID", borrower_nft, "liquid.asset_id"); + add( + &mut pp, + &mut ph, + "ASSET_ID", + borrower_nft, + "liquid.asset_id", + ); add(&mut pp, &mut ph, "ASSET_AMOUNT", "1", "u64"); add(&mut pp, &mut ph, "WITH_ASSET_BURN", "false", "bool"); let principal_out = sh("asset_auth.simf", &pp, &ph); let (mut p, mut h) = (HashMap::new(), HashMap::new()); - add(&mut p, &mut h, "COLLATERAL_ASSET_ID", collateral, "liquid.asset_id"); - add(&mut p, &mut h, "PRINCIPAL_ASSET_ID", principal, "liquid.asset_id"); - add(&mut p, &mut h, "BORROWER_NFT_ASSET_ID", borrower_nft, "liquid.asset_id"); - add(&mut p, &mut h, "LENDER_NFT_ASSET_ID", lender_nft, "liquid.asset_id"); + add( + &mut p, + &mut h, + "COLLATERAL_ASSET_ID", + collateral, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "PRINCIPAL_ASSET_ID", + principal, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "BORROWER_NFT_ASSET_ID", + borrower_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "LENDER_NFT_ASSET_ID", + lender_nft, + "liquid.asset_id", + ); add(&mut p, &mut h, "COLLATERAL_AMOUNT", "21000", "u64"); add(&mut p, &mut h, "PRINCIPAL_AMOUNT", "1000", "u64"); add(&mut p, &mut h, "PRINCIPAL_INTEREST_RATE", "10000", "u64"); add(&mut p, &mut h, "LOAN_EXPIRATION_TIME", "2536857", "u32"); - add(&mut p, &mut h, "LENDER_VAULT_COV_HASH", &a_lender, "bytes32"); - add(&mut p, &mut h, "FINALIZED_LENDER_VAULT_COV_HASH", &f_lender, "bytes32"); - add(&mut p, &mut h, "PROTOCOL_FEE_VAULT_COV_HASH", &a_proto, "bytes32"); - add(&mut p, &mut h, "FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH", &f_proto, "bytes32"); - add(&mut p, &mut h, "PRINCIPAL_OUTPUT_SCRIPT_HASH", &principal_out, "bytes32"); + add( + &mut p, + &mut h, + "LENDER_VAULT_COV_HASH", + &a_lender, + "bytes32", + ); + add( + &mut p, + &mut h, + "FINALIZED_LENDER_VAULT_COV_HASH", + &f_lender, + "bytes32", + ); + add( + &mut p, + &mut h, + "PROTOCOL_FEE_VAULT_COV_HASH", + &a_proto, + "bytes32", + ); + add( + &mut p, + &mut h, + "FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH", + &f_proto, + "bytes32", + ); + add( + &mut p, + &mut h, + "PRINCIPAL_OUTPUT_SCRIPT_HASH", + &principal_out, + "bytes32", + ); // current_debt = principal + principal*rate/10000 = 2000 (unchanged across accept). let mut debt = vec![0u8; 32]; @@ -74,18 +183,26 @@ fn main() { // Pending: slot0 = is_active(false) = all zeros; slot1 = current_debt. let pending = [vec![0u8; 32], debt.clone()]; - let addr_p = covenant::compute_covenant_address(&d.join("lending.simf"), &p, &h, &pending, net, true).unwrap(); + let addr_p = + covenant::compute_covenant_address(&d.join("lending.simf"), &p, &h, &pending, net, true) + .unwrap(); // Active: slot0 = is_active(true) = value 1 (byte[31]=0x01); slot1 = current_debt. let mut slot0_active = vec![0u8; 32]; slot0_active[31] = 1; let active = [slot0_active, debt.clone()]; - let addr_a = covenant::compute_covenant_address(&d.join("lending.simf"), &p, &h, &active, net, true).unwrap(); + let addr_a = + covenant::compute_covenant_address(&d.join("lending.simf"), &p, &h, &active, net, true) + .unwrap(); eprintln!("---- result ----"); println!("pending out[5] spk (repro) = {:x}", addr_p.script_pubkey()); println!("pending out[5] spk (chain) = 51201ae9d30d7a31f1393a289196a4dacc01fac95459540895db448aeca47fbd84e1"); println!("active lending spk (repro) = {:x}", addr_a.script_pubkey()); - assert_ne!(addr_p.script_pubkey(), addr_a.script_pubkey(), "active address must differ from pending"); + assert_ne!( + addr_p.script_pubkey(), + addr_a.script_pubkey(), + "active address must differ from pending" + ); println!("OK: storage transition flips the covenant address (accept: is_active 0 -> 1)"); } diff --git a/txmanifest_lib/examples/lending_recon.rs b/txmanifest_lib/examples/lending_recon.rs index 12f84da..1dfc901 100644 --- a/txmanifest_lib/examples/lending_recon.rs +++ b/txmanifest_lib/examples/lending_recon.rs @@ -6,11 +6,19 @@ use std::collections::HashMap; use lwk_wollet::ElementsNetwork; use tx_manifest_lib::covenant; -fn add(p: &mut HashMap, h: &mut HashMap, k: &str, v: &str, t: &str) { +fn add( + p: &mut HashMap, + h: &mut HashMap, + k: &str, + v: &str, + t: &str, +) { p.insert(k.to_string(), v.to_string()); h.insert(k.to_string(), t.to_string()); } -fn hx(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() } +fn hx(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} fn main() { let net = ElementsNetwork::LiquidTestnet; @@ -21,23 +29,64 @@ fn main() { // Live offer 43ab4efe… params. let collateral_asset = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; - let principal_asset = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; - let borrower_nft = "78d61185c79f855fac51a87c191b00266f02d28752f50b3d9092ccf6b978181e"; - let lender_nft = "213462821a5cdb96f435f5ea6597e8937359d6fd5a64b6ac8ef4262bc279fcfb"; - let protocol_fee = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; + let principal_asset = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; + let borrower_nft = "78d61185c79f855fac51a87c191b00266f02d28752f50b3d9092ccf6b978181e"; + let lender_nft = "213462821a5cdb96f435f5ea6597e8937359d6fd5a64b6ac8ef4262bc279fcfb"; + let protocol_fee = "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"; let out5 = "51201ae9d30d7a31f1393a289196a4dacc01fac95459540895db448aeca47fbd84e1"; // helper to build an asset_auth_vault arg set - let vault = |is_active: &str, finalized_hash: &str, keeper: &str, keeper_burn: &str, supplier_burn: &str| -> (HashMap, HashMap) { + let vault = |is_active: &str, + finalized_hash: &str, + keeper: &str, + keeper_burn: &str, + supplier_burn: &str| + -> (HashMap, HashMap) { let (mut p, mut h) = (HashMap::new(), HashMap::new()); - add(&mut p, &mut h, "VAULT_ASSET_ID", principal_asset, "liquid.asset_id"); - add(&mut p, &mut h, "KEEPER_AUTH_ASSET_ID", keeper, "liquid.asset_id"); - add(&mut p, &mut h, "SUPPLIER_AUTH_ASSET_ID", borrower_nft, "liquid.asset_id"); + add( + &mut p, + &mut h, + "VAULT_ASSET_ID", + principal_asset, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "KEEPER_AUTH_ASSET_ID", + keeper, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "SUPPLIER_AUTH_ASSET_ID", + borrower_nft, + "liquid.asset_id", + ); add(&mut p, &mut h, "KEEPER_AUTH_ASSET_AMOUNT", "1", "u64"); - add(&mut p, &mut h, "FINALIZED_VAULT_COV_HASH", finalized_hash, "bytes32"); + add( + &mut p, + &mut h, + "FINALIZED_VAULT_COV_HASH", + finalized_hash, + "bytes32", + ); add(&mut p, &mut h, "IS_ACTIVE", is_active, "bool"); - add(&mut p, &mut h, "WITH_KEEPER_ASSET_BURN", keeper_burn, "bool"); - add(&mut p, &mut h, "WITH_SUPPLIER_ASSET_BURN", supplier_burn, "bool"); + add( + &mut p, + &mut h, + "WITH_KEEPER_ASSET_BURN", + keeper_burn, + "bool", + ); + add( + &mut p, + &mut h, + "WITH_SUPPLIER_ASSET_BURN", + supplier_burn, + "bool", + ); (p, h) }; let z32 = "00".repeat(32); @@ -56,26 +105,86 @@ fn main() { // principal output = AssetAuth(borrower_nft, 1, false) let (mut pp, mut ph) = (HashMap::new(), HashMap::new()); - add(&mut pp, &mut ph, "ASSET_ID", borrower_nft, "liquid.asset_id"); + add( + &mut pp, + &mut ph, + "ASSET_ID", + borrower_nft, + "liquid.asset_id", + ); add(&mut pp, &mut ph, "ASSET_AMOUNT", "1", "u64"); add(&mut pp, &mut ph, "WITH_ASSET_BURN", "false", "bool"); let principal_out = sh("asset_auth.simf", &pp, &ph); // lending covenant args let (mut p, mut h) = (HashMap::new(), HashMap::new()); - add(&mut p, &mut h, "COLLATERAL_ASSET_ID", collateral_asset, "liquid.asset_id"); - add(&mut p, &mut h, "PRINCIPAL_ASSET_ID", principal_asset, "liquid.asset_id"); - add(&mut p, &mut h, "BORROWER_NFT_ASSET_ID", borrower_nft, "liquid.asset_id"); - add(&mut p, &mut h, "LENDER_NFT_ASSET_ID", lender_nft, "liquid.asset_id"); + add( + &mut p, + &mut h, + "COLLATERAL_ASSET_ID", + collateral_asset, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "PRINCIPAL_ASSET_ID", + principal_asset, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "BORROWER_NFT_ASSET_ID", + borrower_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "LENDER_NFT_ASSET_ID", + lender_nft, + "liquid.asset_id", + ); add(&mut p, &mut h, "COLLATERAL_AMOUNT", "21000", "u64"); add(&mut p, &mut h, "PRINCIPAL_AMOUNT", "1000", "u64"); add(&mut p, &mut h, "PRINCIPAL_INTEREST_RATE", "10000", "u64"); add(&mut p, &mut h, "LOAN_EXPIRATION_TIME", "2536857", "u32"); - add(&mut p, &mut h, "LENDER_VAULT_COV_HASH", &a_lender, "bytes32"); - add(&mut p, &mut h, "FINALIZED_LENDER_VAULT_COV_HASH", &f_lender, "bytes32"); - add(&mut p, &mut h, "PROTOCOL_FEE_VAULT_COV_HASH", &a_proto, "bytes32"); - add(&mut p, &mut h, "FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH", &f_proto, "bytes32"); - add(&mut p, &mut h, "PRINCIPAL_OUTPUT_SCRIPT_HASH", &principal_out, "bytes32"); + add( + &mut p, + &mut h, + "LENDER_VAULT_COV_HASH", + &a_lender, + "bytes32", + ); + add( + &mut p, + &mut h, + "FINALIZED_LENDER_VAULT_COV_HASH", + &f_lender, + "bytes32", + ); + add( + &mut p, + &mut h, + "PROTOCOL_FEE_VAULT_COV_HASH", + &a_proto, + "bytes32", + ); + add( + &mut p, + &mut h, + "FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH", + &f_proto, + "bytes32", + ); + add( + &mut p, + &mut h, + "PRINCIPAL_OUTPUT_SCRIPT_HASH", + &principal_out, + "bytes32", + ); // storage leaves: slot0 = is_active (0), slot1 = current_debt (2000) in last 8 bytes BE let slot0 = vec![0u8; 32]; @@ -83,7 +192,9 @@ fn main() { slot1[24..32].copy_from_slice(&2000u64.to_be_bytes()); let extra = [slot0, slot1]; - let addr = covenant::compute_covenant_address(&d.join("lending.simf"), &p, &h, &extra, net, true).unwrap(); + let addr = + covenant::compute_covenant_address(&d.join("lending.simf"), &p, &h, &extra, net, true) + .unwrap(); eprintln!("---- result ----"); println!("F_lender = {f_lender}"); diff --git a/txmanifest_lib/examples/opreturn_recon.rs b/txmanifest_lib/examples/opreturn_recon.rs index b813dd0..ca681ab 100644 --- a/txmanifest_lib/examples/opreturn_recon.rs +++ b/txmanifest_lib/examples/opreturn_recon.rs @@ -2,13 +2,15 @@ // Also proves the baked-in LENDING_PROGRAM_ID constant (f80c6162) really is // sha256(LF-normalized lending.simf source)[..4] — the derivation is done HERE (authoring // time), not by the tx-encoder, which now sees only a plain `bytes` constant. -use std::collections::HashMap; use lwk_wollet::elements::hashes::{sha256, Hash}; +use std::collections::HashMap; use tx_manifest_lib::{context::ExecutionContext, eval}; /// Protocol message-type tag = first 4 bytes of SHA-256 of the LF-normalized source text. fn program_id(simf_path: &std::path::Path) -> String { - let src = std::fs::read_to_string(simf_path).unwrap().replace("\r\n", "\n"); + let src = std::fs::read_to_string(simf_path) + .unwrap() + .replace("\r\n", "\n"); let h = sha256::Hash::hash(src.as_bytes()).to_byte_array(); h[..4].iter().map(|b| format!("{b:02x}")).collect() } @@ -16,12 +18,17 @@ fn program_id(simf_path: &std::path::Path) -> String { fn main() { let d = std::path::Path::new("examples/lending_v3"); let lending_program_id = program_id(&d.join("lending.simf")); - assert_eq!(lending_program_id, "f80c6162", - "LENDING_PROGRAM_ID constant in the manifest must equal sha256(lending.simf source)[..4]"); + assert_eq!( + lending_program_id, "f80c6162", + "LENDING_PROGRAM_ID constant in the manifest must equal sha256(lending.simf source)[..4]" + ); let mut ctx = ExecutionContext::new(); ctx.set_compile_param("LENDING_PROGRAM_ID", &lending_program_id); - ctx.set_compile_param("PRINCIPAL_ASSET_ID", "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"); + ctx.set_compile_param( + "PRINCIPAL_ASSET_ID", + "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5", + ); ctx.set_compile_param("PRINCIPAL_AMOUNT", "1000"); ctx.set_compile_param("LOAN_EXPIRATION_TIME", "2536857"); ctx.set_compile_param("PRINCIPAL_INTEREST_RATE", "10000"); diff --git a/txmanifest_lib/examples/prelock_recon.rs b/txmanifest_lib/examples/prelock_recon.rs index 01fbc63..d4e43fd 100644 --- a/txmanifest_lib/examples/prelock_recon.rs +++ b/txmanifest_lib/examples/prelock_recon.rs @@ -6,7 +6,13 @@ use std::collections::HashMap; use lwk_wollet::ElementsNetwork; use tx_manifest_lib::covenant; -fn add(p: &mut HashMap, h: &mut HashMap, k: &str, v: &str, t: &str) { +fn add( + p: &mut HashMap, + h: &mut HashMap, + k: &str, + v: &str, + t: &str, +) { p.insert(k.to_string(), v.to_string()); h.insert(k.to_string(), t.to_string()); } @@ -34,7 +40,10 @@ fn main() { add(&mut p, &mut h, "ASSET_ID", lender_nft, "liquid.asset_id"); add(&mut p, &mut h, "ASSET_AMOUNT", "1", "u64"); add(&mut p, &mut h, "WITH_ASSET_BURN", "true", "bool"); - let lender_principal_cov = hexs(&covenant::compute_covenant_script_hash(&dir.join("asset_auth.simf"), &p, &h, net, true).unwrap()); + let lender_principal_cov = hexs( + &covenant::compute_covenant_script_hash(&dir.join("asset_auth.simf"), &p, &h, net, true) + .unwrap(), + ); // 2. LENDING_COV_HASH = script_hash(lending, ...) let (mut p, mut h) = (HashMap::new(), HashMap::new()); @@ -42,19 +51,67 @@ fn main() { add(&mut p, &mut h, "PRINCIPAL_AMOUNT", "1000", "u64"); add(&mut p, &mut h, "LOAN_EXPIRATION_TIME", "5000000", "u32"); add(&mut p, &mut h, "PRINCIPAL_INTEREST_RATE", "100", "u16"); - add(&mut p, &mut h, "COLLATERAL_ASSET_ID", collateral_asset, "liquid.asset_id"); - add(&mut p, &mut h, "FIRST_PARAMETERS_NFT_ASSET_ID", first_nft, "liquid.asset_id"); - add(&mut p, &mut h, "SECOND_PARAMETERS_NFT_ASSET_ID", second_nft, "liquid.asset_id"); - add(&mut p, &mut h, "BORROWER_NFT_ASSET_ID", borrower_nft, "liquid.asset_id"); - add(&mut p, &mut h, "PRINCIPAL_ASSET_ID", principal_asset, "liquid.asset_id"); - add(&mut p, &mut h, "LENDER_PRINCIPAL_COV_HASH", &lender_principal_cov, "bytes32"); - add(&mut p, &mut h, "LENDER_NFT_ASSET_ID", lender_nft, "liquid.asset_id"); - let lending_cov = hexs(&covenant::compute_covenant_script_hash(&dir.join("lending.simf"), &p, &h, net, true).unwrap()); + add( + &mut p, + &mut h, + "COLLATERAL_ASSET_ID", + collateral_asset, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "FIRST_PARAMETERS_NFT_ASSET_ID", + first_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "SECOND_PARAMETERS_NFT_ASSET_ID", + second_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "BORROWER_NFT_ASSET_ID", + borrower_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "PRINCIPAL_ASSET_ID", + principal_asset, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "LENDER_PRINCIPAL_COV_HASH", + &lender_principal_cov, + "bytes32", + ); + add( + &mut p, + &mut h, + "LENDER_NFT_ASSET_ID", + lender_nft, + "liquid.asset_id", + ); + let lending_cov = hexs( + &covenant::compute_covenant_script_hash(&dir.join("lending.simf"), &p, &h, net, true) + .unwrap(), + ); // 3. PARAMETERS_NFT_OUTPUT_SCRIPT_HASH = script_hash(script_auth, SCRIPT_HASH=lending_cov) let (mut p, mut h) = (HashMap::new(), HashMap::new()); add(&mut p, &mut h, "SCRIPT_HASH", &lending_cov, "bytes32"); - let params_nft_out = hexs(&covenant::compute_covenant_script_hash(&dir.join("script_auth.simf"), &p, &h, net, true).unwrap()); + let params_nft_out = hexs( + &covenant::compute_covenant_script_hash(&dir.join("script_auth.simf"), &p, &h, net, true) + .unwrap(), + ); // 4. pre_lock address let (mut p, mut h) = (HashMap::new(), HashMap::new()); @@ -62,23 +119,88 @@ fn main() { add(&mut p, &mut h, "PRINCIPAL_AMOUNT", "1000", "u64"); add(&mut p, &mut h, "LOAN_EXPIRATION_TIME", "5000000", "u32"); add(&mut p, &mut h, "PRINCIPAL_INTEREST_RATE", "100", "u16"); - add(&mut p, &mut h, "COLLATERAL_ASSET_ID", collateral_asset, "liquid.asset_id"); - add(&mut p, &mut h, "FIRST_PARAMETERS_NFT_ASSET_ID", first_nft, "liquid.asset_id"); - add(&mut p, &mut h, "SECOND_PARAMETERS_NFT_ASSET_ID", second_nft, "liquid.asset_id"); - add(&mut p, &mut h, "BORROWER_NFT_ASSET_ID", borrower_nft, "liquid.asset_id"); - add(&mut p, &mut h, "LENDER_NFT_ASSET_ID", lender_nft, "liquid.asset_id"); - add(&mut p, &mut h, "PRINCIPAL_ASSET_ID", principal_asset, "liquid.asset_id"); + add( + &mut p, + &mut h, + "COLLATERAL_ASSET_ID", + collateral_asset, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "FIRST_PARAMETERS_NFT_ASSET_ID", + first_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "SECOND_PARAMETERS_NFT_ASSET_ID", + second_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "BORROWER_NFT_ASSET_ID", + borrower_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "LENDER_NFT_ASSET_ID", + lender_nft, + "liquid.asset_id", + ); + add( + &mut p, + &mut h, + "PRINCIPAL_ASSET_ID", + principal_asset, + "liquid.asset_id", + ); add(&mut p, &mut h, "LENDING_COV_HASH", &lending_cov, "bytes32"); - add(&mut p, &mut h, "PRINCIPAL_OUTPUT_SCRIPT_HASH", borrower_out_hash, "bytes32"); - add(&mut p, &mut h, "PARAMETERS_NFT_OUTPUT_SCRIPT_HASH", ¶ms_nft_out, "bytes32"); - add(&mut p, &mut h, "BORROWER_NFT_OUTPUT_SCRIPT_HASH", borrower_out_hash, "bytes32"); - add(&mut p, &mut h, "BORROWER_PUB_KEY", borrower_pubkey, "pubkey"); - let addr = covenant::compute_covenant_address(&dir.join("pre_lock.simf"), &p, &h, &[], net, true).unwrap(); + add( + &mut p, + &mut h, + "PRINCIPAL_OUTPUT_SCRIPT_HASH", + borrower_out_hash, + "bytes32", + ); + add( + &mut p, + &mut h, + "PARAMETERS_NFT_OUTPUT_SCRIPT_HASH", + ¶ms_nft_out, + "bytes32", + ); + add( + &mut p, + &mut h, + "BORROWER_NFT_OUTPUT_SCRIPT_HASH", + borrower_out_hash, + "bytes32", + ); + add( + &mut p, + &mut h, + "BORROWER_PUB_KEY", + borrower_pubkey, + "pubkey", + ); + let addr = + covenant::compute_covenant_address(&dir.join("pre_lock.simf"), &p, &h, &[], net, true) + .unwrap(); eprintln!("---- result ----"); println!("LENDER_PRINCIPAL_COV_HASH = {lender_principal_cov}"); println!("LENDING_COV_HASH = {lending_cov}"); println!("PARAMETERS_NFT_OUT_HASH = {params_nft_out}"); - println!("pre_lock spk (this wallet, debug=true) = {:x}", addr.script_pubkey()); + println!( + "pre_lock spk (this wallet, debug=true) = {:x}", + addr.script_pubkey() + ); println!("indexer reconstruction (simplicity-lending) = 512050... (old) / f2b6fe... (correct)"); } diff --git a/txmanifest_lib/src/canonical.rs b/txmanifest_lib/src/canonical.rs index 8ca9d40..f5fa2a7 100644 --- a/txmanifest_lib/src/canonical.rs +++ b/txmanifest_lib/src/canonical.rs @@ -100,7 +100,10 @@ pub fn manifest_id(raw: &str) -> Result<[u8; 32]> { /// [`manifest_id`] as lowercase hex — the form a registry key would take. pub fn manifest_id_hex(raw: &str) -> Result { - Ok(manifest_id(raw)?.iter().map(|b| format!("{b:02x}")).collect()) + Ok(manifest_id(raw)? + .iter() + .map(|b| format!("{b:02x}")) + .collect()) } #[cfg(test)] @@ -125,7 +128,10 @@ mod tests { fn editing_a_description_does_not_change_the_id() { // The whole point: prose churn must not mint a new registry entry. let edited = BASE - .replace("the original prose", "completely rewritten, much longer prose") + .replace( + "the original prose", + "completely rewritten, much longer prose", + ) .replace("developer note", "a different note entirely"); assert_eq!(manifest_id(BASE).unwrap(), manifest_id(&edited).unwrap()); } @@ -136,7 +142,10 @@ mod tests { let value: Value = serde_json::from_str(BASE).unwrap(); let reformatted = serde_json::to_string_pretty(&value).unwrap(); let compact = serde_json::to_string(&value).unwrap(); - assert_eq!(manifest_id(BASE).unwrap(), manifest_id(&reformatted).unwrap()); + assert_eq!( + manifest_id(BASE).unwrap(), + manifest_id(&reformatted).unwrap() + ); assert_eq!(manifest_id(BASE).unwrap(), manifest_id(&compact).unwrap()); } @@ -171,7 +180,10 @@ mod tests { for key in UNHASHED_KEYS { assert!(!text.contains(key), "canonical form still contains '{key}'"); } - assert!(text.contains("change back to you"), "ui.label must be hashed"); + assert!( + text.contains("change back to you"), + "ui.label must be hashed" + ); } #[test] diff --git a/txmanifest_lib/src/config.rs b/txmanifest_lib/src/config.rs index 9e19c1a..43002a8 100644 --- a/txmanifest_lib/src/config.rs +++ b/txmanifest_lib/src/config.rs @@ -100,6 +100,5 @@ pub fn save(config: &Config) -> Result<()> { .with_context(|| format!("Cannot create config dir: {}", parent.display()))?; } let raw = serde_json::to_string_pretty(config)?; - std::fs::write(&path, raw) - .with_context(|| format!("Cannot write config: {}", path.display())) + std::fs::write(&path, raw).with_context(|| format!("Cannot write config: {}", path.display())) } diff --git a/txmanifest_lib/src/context.rs b/txmanifest_lib/src/context.rs index 3550234..653f152 100644 --- a/txmanifest_lib/src/context.rs +++ b/txmanifest_lib/src/context.rs @@ -104,11 +104,14 @@ impl ExecutionContext { attr: impl Into, value: impl Into, ) { - self.input_attrs.insert((input_id.into(), attr.into()), value.into()); + self.input_attrs + .insert((input_id.into(), attr.into()), value.into()); } pub fn get_input_attr(&self, input_id: &str, attr: &str) -> Option<&str> { - self.input_attrs.get(&(input_id.to_string(), attr.to_string())).map(String::as_str) + self.input_attrs + .get(&(input_id.to_string(), attr.to_string())) + .map(String::as_str) } pub fn set_input_entropy(&mut self, id: &str, entropy_hex: String) { diff --git a/txmanifest_lib/src/covenant.rs b/txmanifest_lib/src/covenant.rs index 90fd508..e359c2c 100644 --- a/txmanifest_lib/src/covenant.rs +++ b/txmanifest_lib/src/covenant.rs @@ -46,9 +46,13 @@ pub fn compute_tapleaf_hash( let args_json = build_args_json(compile_params, type_hints)?; let arguments: Arguments = serde_json::from_str(&args_json) .with_context(|| format!("Failed to parse Arguments from JSON:\n{args_json}"))?; - let compiled = - CompiledProgram::new(source, arguments, include_debug_symbols, Box::new(ElementsJetHinter::new())) - .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; + let compiled = CompiledProgram::new( + source, + arguments, + include_debug_symbols, + Box::new(ElementsJetHinter::new()), + ) + .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; let commit = compiled.commit(); let cmr = commit.cmr(); let leaf_ver = simplicity_leaf_version(); @@ -69,7 +73,14 @@ pub fn compute_covenant_script_hash( network: lwk_wollet::ElementsNetwork, include_debug_symbols: bool, ) -> Result<[u8; 32]> { - compute_covenant_script_hash_with_leaves(simf_path, compile_params, type_hints, &[], network, include_debug_symbols) + compute_covenant_script_hash_with_leaves( + simf_path, + compile_params, + type_hints, + &[], + network, + include_debug_symbols, + ) } /// Like [`compute_covenant_script_hash`] but folds `extra_leaf_payloads` (taproot storage @@ -84,7 +95,14 @@ pub fn compute_covenant_script_hash_with_leaves( network: lwk_wollet::ElementsNetwork, include_debug_symbols: bool, ) -> Result<[u8; 32]> { - let addr = compute_covenant_address(simf_path, compile_params, type_hints, extra_leaf_payloads, network, include_debug_symbols)?; + let addr = compute_covenant_address( + simf_path, + compile_params, + type_hints, + extra_leaf_payloads, + network, + include_debug_symbols, + )?; let spk = addr.script_pubkey(); Ok(sha256::Hash::hash(spk.as_bytes()).to_byte_array()) } @@ -103,8 +121,13 @@ pub fn check_compile( let args_json = build_args_json(compile_params, type_hints)?; let arguments: Arguments = serde_json::from_str(&args_json) .with_context(|| format!("Failed to parse Arguments from JSON:\n{args_json}"))?; - CompiledProgram::new(source, arguments, include_debug_symbols, Box::new(ElementsJetHinter::new())) - .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; + CompiledProgram::new( + source, + arguments, + include_debug_symbols, + Box::new(ElementsJetHinter::new()), + ) + .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; Ok(()) } @@ -344,9 +367,13 @@ pub fn dry_run_covenant( let args_json = build_args_json(compile_params, type_hints)?; let arguments: Arguments = serde_json::from_str(&args_json) .with_context(|| format!("Failed to parse Arguments from JSON:\n{args_json}"))?; - let compiled = - CompiledProgram::new(source, arguments, include_debug_symbols, Box::new(ElementsJetHinter::new())) - .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; + let compiled = CompiledProgram::new( + source, + arguments, + include_debug_symbols, + Box::new(ElementsJetHinter::new()), + ) + .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; let abi_meta = compiled .generate_abi_meta() .map_err(|e| anyhow::anyhow!("Cannot get ABI metadata: {e}"))?; @@ -588,9 +615,13 @@ pub fn finalize_covenant_input( let args_json = build_args_json(compile_params, type_hints)?; let arguments: Arguments = serde_json::from_str(&args_json) .with_context(|| format!("Failed to parse Arguments from JSON:\n{args_json}"))?; - let compiled = - CompiledProgram::new(source, arguments, include_debug_symbols, Box::new(ElementsJetHinter::new())) - .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; + let compiled = CompiledProgram::new( + source, + arguments, + include_debug_symbols, + Box::new(ElementsJetHinter::new()), + ) + .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; let abi_meta = compiled .generate_abi_meta() .map_err(|e| anyhow::anyhow!("Cannot get ABI metadata: {e}"))?; @@ -721,9 +752,13 @@ pub fn compute_covenant_address( .with_context(|| format!("Cannot read simf file: {}", simf_path.display()))?; eprintln!("[covenant] simf source loaded ({} bytes)", source.len()); - let compiled = - CompiledProgram::new(source, arguments, include_debug_symbols, Box::new(ElementsJetHinter::new())) - .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; + let compiled = CompiledProgram::new( + source, + arguments, + include_debug_symbols, + Box::new(ElementsJetHinter::new()), + ) + .map_err(|e| anyhow::anyhow!("SimplicityHL compilation failed: {e}"))?; eprintln!("[covenant] SimplicityHL compilation OK"); // Get CMR; tapscript leaf = CMR (32 bytes) as required by Elements Simplicity validator @@ -1124,7 +1159,11 @@ mod tests { #[test] fn untyped_asset_named_param_is_not_inferred_as_asset() { let asset = "857e17708b6ec9ad0e2cc50a8faa8140b7ad253029443513850f14e4a95589b4"; - assert_eq!(infer_simf_type("SOME_ASSET_ID"), None, "asset names are no longer type-inferred"); + assert_eq!( + infer_simf_type("SOME_ASSET_ID"), + None, + "asset names are no longer type-inferred" + ); let mut params = HashMap::new(); params.insert("SOME_ASSET_ID".to_string(), asset.to_string()); @@ -1174,8 +1213,8 @@ mod tests { hints.insert("SCRIPT_HASH".to_string(), "bytes32".to_string()); // Path A — the function under test. (Debug-symbol setting must match Path B.) - let hash_a = compute_tapleaf_hash(&simf_path, ¶ms, &hints, true) - .expect("compute_tapleaf_hash"); + let hash_a = + compute_tapleaf_hash(&simf_path, ¶ms, &hints, true).expect("compute_tapleaf_hash"); // Path B — compile directly, get CMR, use TapLeafHash::from_script. let source = std::fs::read_to_string(&simf_path).expect("read simf"); @@ -1360,9 +1399,14 @@ mod tests { ); // Hash A: explicit params only (mirrors IssueUtilityNFTs PRE_LOCK_COV_HASH computation) - let hash_a = - compute_covenant_script_hash(&simf_path, &explicit_params, &explicit_hints, network, false) - .expect("hash with explicit params"); + let hash_a = compute_covenant_script_hash( + &simf_path, + &explicit_params, + &explicit_hints, + network, + false, + ) + .expect("hash with explicit params"); // Add the extra params that LockCollateral includes via compile_params_map // (all instance fields, including ones pre_lock.simf does NOT reference). @@ -1426,8 +1470,9 @@ mod tests { ); // Hash B: all params (mirrors how LockCollateral creates the pre_lock output) - let hash_b = compute_covenant_script_hash(&simf_path, &all_params, &all_hints, network, false) - .expect("hash with all params"); + let hash_b = + compute_covenant_script_hash(&simf_path, &all_params, &all_hints, network, false) + .expect("hash with all params"); let hex_a: String = hash_a.iter().map(|b| format!("{b:02x}")).collect(); let hex_b: String = hash_b.iter().map(|b| format!("{b:02x}")).collect(); diff --git a/txmanifest_lib/src/describe.rs b/txmanifest_lib/src/describe.rs index 16ac68b..1ed5317 100644 --- a/txmanifest_lib/src/describe.rs +++ b/txmanifest_lib/src/describe.rs @@ -12,7 +12,7 @@ use serde_json::Value; use std::collections::BTreeMap; use crate::manifest::{ - Action, ContractTemplate, Manifest, InstanceCreate, Input, Output, ParamDef, + Action, ContractTemplate, Input, InstanceCreate, Manifest, Output, ParamDef, }; /// Entry point: explore the contract interactively, or dump it if non-interactive. @@ -70,7 +70,10 @@ fn main_menu(manifest: &Manifest) -> Result<()> { if let Some(contract_templates) = &manifest.contract_templates { for (cname, cdef) in contract_templates { - labels.push(format!("template {cname} ({} actions)", cdef.actions.len())); + labels.push(format!( + "template {cname} ({} actions)", + cdef.actions.len() + )); targets.push(Target::Template(cname.clone())); } } @@ -104,7 +107,11 @@ fn main_menu(manifest: &Manifest) -> Result<()> { } fn template_menu(manifest: &Manifest, template_name: &str) -> Result<()> { - let template = match manifest.contract_templates.as_ref().and_then(|c| c.get(template_name)) { + let template = match manifest + .contract_templates + .as_ref() + .and_then(|c| c.get(template_name)) + { Some(c) => c, None => return Ok(()), }; @@ -164,14 +171,21 @@ fn print_overview(manifest: &Manifest) { if let Some(d) = &manifest.description { println!(" {}", style(d).italic()); } - println!(" chain : {}", manifest.chain.as_deref().unwrap_or("elements (default)")); + println!( + " chain : {}", + manifest.chain.as_deref().unwrap_or("elements (default)") + ); println!(" version : {}", manifest.manifest_version); if let Some(utxo_types) = &manifest.utxo_types { if !utxo_types.is_empty() { println!(" {}", style("UTXO types").bold()); for (name, t) in utxo_types { - println!(" {} — {}", style(name).green(), style(&t.description).dim()); + println!( + " {} — {}", + style(name).green(), + style(&t.description).dim() + ); } } } @@ -179,12 +193,20 @@ fn print_overview(manifest: &Manifest) { if let Some(contract_templates) = &manifest.contract_templates { if !contract_templates.is_empty() { let names: Vec<&str> = contract_templates.keys().map(String::as_str).collect(); - println!(" {}: {}", style("Contract templates").bold(), names.join(", ")); + println!( + " {}: {}", + style("Contract templates").bold(), + names.join(", ") + ); } } if !manifest.actions.is_empty() { let names: Vec<&str> = manifest.actions.keys().map(String::as_str).collect(); - println!(" {}: {}", style("Standalone actions").bold(), names.join(", ")); + println!( + " {}: {}", + style("Standalone actions").bold(), + names.join(", ") + ); } } @@ -197,12 +219,35 @@ fn print_template_header(name: &str, template: &ContractTemplate) { if !template.fields.is_empty() { println!(" {}", style("Fields").bold()); for (fname, def) in &template.fields { - let desc = def.description.as_deref().map(|d| format!(" — {d}")).unwrap_or_default(); - let default = def.default.as_deref().map(|d| format!(" [default: {d}]")).unwrap_or_default(); - println!(" {} : {}{}{}", style(fname).green(), def.type_, style(desc).dim(), style(default).yellow()); + let desc = def + .description + .as_deref() + .map(|d| format!(" — {d}")) + .unwrap_or_default(); + let default = def + .default + .as_deref() + .map(|d| format!(" [default: {d}]")) + .unwrap_or_default(); + println!( + " {} : {}{}{}", + style(fname).green(), + def.type_, + style(desc).dim(), + style(default).yellow() + ); } } - println!(" {}: {}", style("Actions").bold(), template.actions.keys().cloned().collect::>().join(", ")); + println!( + " {}: {}", + style("Actions").bold(), + template + .actions + .keys() + .cloned() + .collect::>() + .join(", ") + ); } fn print_action(title: &str, action: &Action) { @@ -237,8 +282,18 @@ fn print_param_map(label: &str, params: &Option>) { if def.compute.is_some() { extra.push_str(" (computed)"); } - let desc = def.description.as_deref().map(|d| format!(" — {d}")).unwrap_or_default(); - println!(" {} : {}{}{}", style(name).green(), def.type_, style(extra).yellow(), style(desc).dim()); + let desc = def + .description + .as_deref() + .map(|d| format!(" — {d}")) + .unwrap_or_default(); + println!( + " {} : {}{}{}", + style(name).green(), + def.type_, + style(extra).yellow(), + style(desc).dim() + ); } } @@ -256,16 +311,35 @@ fn print_inputs(inputs: &Option>) { } else { val_str(&inp.utxo_source) }; - let asset = inp.asset.as_ref().map(|a| format!(" asset={}", val_str(a))).unwrap_or_default(); - let amount = inp.amount_sat.as_ref().map(|a| format!(" amount={}", val_str(a))).unwrap_or_default(); - println!(" {}{} ← {}{}{}", style(&inp.id).green(), role_tag(inp.ui_role()), src, style(asset).dim(), style(amount).dim()); + let asset = inp + .asset + .as_ref() + .map(|a| format!(" asset={}", val_str(a))) + .unwrap_or_default(); + let amount = inp + .amount_sat + .as_ref() + .map(|a| format!(" amount={}", val_str(a))) + .unwrap_or_default(); + println!( + " {}{} ← {}{}{}", + style(&inp.id).green(), + role_tag(inp.ui_role()), + src, + style(asset).dim(), + style(amount).dim() + ); if let Some(label) = inp.ui_label() { println!(" {}", style(label).dim()); } if let Some(Value::Object(m)) = &inp.witnesses { if !m.is_empty() { let keys: Vec<&str> = m.keys().map(String::as_str).collect(); - println!(" {} {}", style("witnesses:").dim(), style(keys.join(", ")).dim()); + println!( + " {} {}", + style("witnesses:").dim(), + style(keys.join(", ")).dim() + ); } } if inp.issuance.is_some() { @@ -286,8 +360,16 @@ fn print_outputs(outputs: &Option>) { .as_ref() .map(|a| format!(" amount={}", val_str(a))) .unwrap_or_else(|| " amount=(auto)".to_string()); - let asset = o.asset.as_ref().map(|a| format!(" asset={}", val_str(a))).unwrap_or_default(); - let opt = if o.optional.unwrap_or(false) { " (optional)" } else { "" }; + let asset = o + .asset + .as_ref() + .map(|a| format!(" asset={}", val_str(a))) + .unwrap_or_default(); + let opt = if o.optional.unwrap_or(false) { + " (optional)" + } else { + "" + }; println!( " {}{} → {}{}{}{}", style(&o.id).green(), diff --git a/txmanifest_lib/src/eval.rs b/txmanifest_lib/src/eval.rs index 2750953..36bcf68 100644 --- a/txmanifest_lib/src/eval.rs +++ b/txmanifest_lib/src/eval.rs @@ -22,15 +22,17 @@ pub fn eval_expr_str(expr: &str, ctx: &ExecutionContext) -> Result { pub fn eval_amount(value: &serde_json::Value, ctx: &ExecutionContext) -> Result { match value { serde_json::Value::Null => Ok(0), - serde_json::Value::Number(n) => { - n.as_u64() - .ok_or_else(|| anyhow::anyhow!("amount_sat number is not a valid u64: {n}")) - } + serde_json::Value::Number(n) => n + .as_u64() + .ok_or_else(|| anyhow::anyhow!("amount_sat number is not a valid u64: {n}")), serde_json::Value::String(s) => eval_expr(s.trim(), ctx), // { "value": "", "description": "..." } — documented amount field serde_json::Value::Object(m) => match m.get("value") { Some(v) => eval_amount(v, ctx), - None => bail!("Unsupported amount_sat object (no 'value' field): {}", serde_json::Value::Object(m.clone())), + None => bail!( + "Unsupported amount_sat object (no 'value' field): {}", + serde_json::Value::Object(m.clone()) + ), }, other => bail!("Unsupported amount_sat value: {other}"), } @@ -86,10 +88,9 @@ pub fn eval_op_return_data( match data { serde_json::Value::String(expr) => eval_op_return_concat(expr, ctx, type_hints), serde_json::Value::Object(m) => { - let parts = m - .get("parts") - .and_then(|v| v.as_array()) - .ok_or_else(|| anyhow::anyhow!("OP_RETURN data object must have a 'parts' array"))?; + let parts = m.get("parts").and_then(|v| v.as_array()).ok_or_else(|| { + anyhow::anyhow!("OP_RETURN data object must have a 'parts' array") + })?; let mut out = Vec::new(); for part in parts { out.extend_from_slice(&eval_op_return_part(part, ctx)?); @@ -101,25 +102,34 @@ pub fn eval_op_return_data( } /// Encode a single typed OP_RETURN `parts` entry (see [`eval_op_return_data`]). -fn eval_op_return_part( - part: &serde_json::Value, - ctx: &ExecutionContext, -) -> Result> { - let ty = part.get("type").and_then(|v| v.as_str()) +fn eval_op_return_part(part: &serde_json::Value, ctx: &ExecutionContext) -> Result> { + let ty = part + .get("type") + .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("OP_RETURN part missing 'type': {part}"))?; - let value_ref = part.get("value").and_then(|v| v.as_str()) + let value_ref = part + .get("value") + .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("OP_RETURN part needs 'value': {part}"))?; let resolved = resolve_ref(value_ref, ctx) .unwrap_or_else(|| value_ref.trim_matches(['"', '\'']).to_string()); match ty { "u8" | "u16" | "u32" | "u64" => { - let n: u64 = resolved.trim().parse() - .map_err(|_| anyhow::anyhow!("OP_RETURN '{value_ref}' = '{resolved}' is not an integer"))?; - let width = match ty { "u8" => 1, "u16" => 2, "u32" => 4, _ => 8 }; + let n: u64 = resolved.trim().parse().map_err(|_| { + anyhow::anyhow!("OP_RETURN '{value_ref}' = '{resolved}' is not an integer") + })?; + let width = match ty { + "u8" => 1, + "u16" => 2, + "u32" => 4, + _ => 8, + }; let le = part.get("endian").and_then(|v| v.as_str()) != Some("be"); let full = n.to_le_bytes(); let mut bytes = full[..width].to_vec(); - if !le { bytes.reverse(); } + if !le { + bytes.reverse(); + } Ok(bytes) } "liquid.asset_id" => { @@ -145,11 +155,10 @@ fn eval_op_return_part( /// of a 32-byte slot) or on the right when `align: "left"`. /// /// Used for dynamic storage slots such as the lending covenant's `current_debt` leaf. -pub fn encode_leaf_value( - item: &serde_json::Value, - ctx: &ExecutionContext, -) -> Result> { - let value_ref = item.get("value").and_then(|v| v.as_str()) +pub fn encode_leaf_value(item: &serde_json::Value, ctx: &ExecutionContext) -> Result> { + let value_ref = item + .get("value") + .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("taproot leaf value item needs 'value': {item}"))?; let resolved = resolve_ref(value_ref, ctx) .unwrap_or_else(|| value_ref.trim_matches(['"', '\'']).to_string()); @@ -160,27 +169,44 @@ pub fn encode_leaf_value( /// Split from [`encode_leaf_value`] so callers that resolve `value` themselves (e.g. /// against an in-progress `create_instance` field map) can reuse the typed encoding. pub fn encode_leaf_bytes(item: &serde_json::Value, resolved: &str) -> Result> { - let ty = item.get("type").and_then(|v| v.as_str()) + let ty = item + .get("type") + .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("taproot leaf value item needs 'type': {item}"))?; let mut bytes = match ty { "u8" | "u16" | "u32" | "u64" => { - let n: u64 = resolved.trim().parse() - .map_err(|_| anyhow::anyhow!("taproot leaf value '{resolved}' is not an integer"))?; - let width = match ty { "u8" => 1, "u16" => 2, "u32" => 4, _ => 8 }; + let n: u64 = resolved.trim().parse().map_err(|_| { + anyhow::anyhow!("taproot leaf value '{resolved}' is not an integer") + })?; + let width = match ty { + "u8" => 1, + "u16" => 2, + "u32" => 4, + _ => 8, + }; let le = item.get("endian").and_then(|v| v.as_str()) != Some("be"); let full = n.to_le_bytes(); let mut b = full[..width].to_vec(); - if !le { b.reverse(); } + if !le { + b.reverse(); + } b } "bytes32" | "bytes" | "pubkey" => hex_to_bytes(&resolved)?, other => bail!("Unsupported taproot leaf value type '{other}'"), }; - if let Some(pad_to) = item.get("pad_to").and_then(|v| v.as_u64()).map(|n| n as usize) { + if let Some(pad_to) = item + .get("pad_to") + .and_then(|v| v.as_u64()) + .map(|n| n as usize) + { if bytes.len() > pad_to { - bail!("taproot leaf value '{resolved}' encodes to {} bytes, exceeds pad_to {pad_to}", bytes.len()); + bail!( + "taproot leaf value '{resolved}' encodes to {} bytes, exceeds pad_to {pad_to}", + bytes.len() + ); } let pad = pad_to - bytes.len(); // Default align for a padded value is "right" (value occupies the trailing bytes). @@ -229,9 +255,16 @@ fn eval_op_return_concat( // Byte-reversal is driven ONLY by the declared `liquid.asset_id` type of the // referenced key — never by its name. An asset ref must be a typed compile param. let key = arg.rsplit('.').next().unwrap_or(arg); - let is_asset = type_hints.get(key).map(|t| t == "liquid.asset_id").unwrap_or(false); - let resolved = resolve_ref(arg, ctx).unwrap_or_else(|| arg.trim_matches(['"', '\'']).to_string()); - let hex = resolved.trim().trim_start_matches("0x").trim_start_matches("0X"); + let is_asset = type_hints + .get(key) + .map(|t| t == "liquid.asset_id") + .unwrap_or(false); + let resolved = + resolve_ref(arg, ctx).unwrap_or_else(|| arg.trim_matches(['"', '\'']).to_string()); + let hex = resolved + .trim() + .trim_start_matches("0x") + .trim_start_matches("0X"); if hex.len() % 2 != 0 { bail!("OP_RETURN data part '{arg}' resolved to odd-length hex '{hex}'"); } @@ -239,7 +272,9 @@ fn eval_op_return_concat( .step_by(2) .map(|i| u8::from_str_radix(&hex[i..i + 2], 16)) .collect::, _>>() - .map_err(|_| anyhow::anyhow!("OP_RETURN data part '{arg}' is not valid hex: '{hex}'"))?; + .map_err(|_| { + anyhow::anyhow!("OP_RETURN data part '{arg}' is not valid hex: '{hex}'") + })?; if is_asset { bytes.reverse(); } @@ -451,9 +486,9 @@ pub fn eval_simplicityhl_hook( hook_input_id: &str, ctx: &ExecutionContext, ) -> Result { - let resolved = ctx.get_input(hook_input_id).ok_or_else(|| { - anyhow::anyhow!("Input '{}' not found in context", hook_input_id) - })?; + let resolved = ctx + .get_input(hook_input_id) + .ok_or_else(|| anyhow::anyhow!("Input '{}' not found in context", hook_input_id))?; let txid = Txid::from_str(&resolved.txid) .map_err(|e| anyhow::anyhow!("Cannot parse txid '{}': {e}", resolved.txid))?; @@ -493,7 +528,10 @@ pub fn eval_simplicityhl_hook( Ok(reversed) } Err(simplicityhl::EvalError::RequiresTransactionContext(jets)) => { - bail!("Expression requires transaction context: {}", jets.join(", ")) + bail!( + "Expression requires transaction context: {}", + jets.join(", ") + ) } Err(e) => bail!("SimplicityHL eval failed: {e}"), } @@ -529,7 +567,9 @@ pub fn eval_param_compute_expr(expr: &str, ctx: &ExecutionContext) -> Result String { let mut s = expr.to_string(); while let Some(pos) = s.find("pow(") { let inner_start = pos + 4; - let Some(rel_close) = s[inner_start..].find(')') else { break }; + let Some(rel_close) = s[inner_start..].find(')') else { + break; + }; let inner = s[inner_start..inner_start + rel_close].to_string(); let Some(comma) = inner.find(',') else { break }; let base_s = inner[..comma].trim(); @@ -639,9 +681,7 @@ fn substitute_vars(expr: &str, ctx: &ExecutionContext) -> String { { i += 1; // consume the dot let key_start = i; - while i < bytes.len() - && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') - { + while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; } let key = &expr[key_start..i]; @@ -699,10 +739,16 @@ mod fee_keyword_tests { let mut ctx = ExecutionContext::new(); ctx.set_param("amount", "100000"); // Default fee is 0. - assert_eq!(eval_amount(&serde_json::json!("amount - fee"), &ctx).unwrap(), 100000); + assert_eq!( + eval_amount(&serde_json::json!("amount - fee"), &ctx).unwrap(), + 100000 + ); // After estimation, `fee` reflects the set value. ctx.set_fee(250); - assert_eq!(eval_amount(&serde_json::json!("amount - fee"), &ctx).unwrap(), 99750); + assert_eq!( + eval_amount(&serde_json::json!("amount - fee"), &ctx).unwrap(), + 99750 + ); // Bare `fee` resolves directly. assert_eq!(eval_amount(&serde_json::json!("fee"), &ctx).unwrap(), 250); } @@ -780,7 +826,10 @@ mod op_return_data_tests { ctx.set_compile_param("PRINCIPAL_ASSET_ID", asset_display); let mut hints = HashMap::new(); - hints.insert("PRINCIPAL_ASSET_ID".to_string(), "liquid.asset_id".to_string()); + hints.insert( + "PRINCIPAL_ASSET_ID".to_string(), + "liquid.asset_id".to_string(), + ); hints.insert("BORROWER_PUB_KEY".to_string(), "pubkey".to_string()); let bytes = eval_op_return_data( @@ -804,7 +853,10 @@ mod op_return_data_tests { #[test] fn parts_form_encodes_typed_50_byte_metadata() { let mut ctx = ExecutionContext::new(); - ctx.set_compile_param("PRINCIPAL_ASSET_ID", "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"); + ctx.set_compile_param( + "PRINCIPAL_ASSET_ID", + "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5", + ); ctx.set_compile_param("PRINCIPAL_AMOUNT", "1000"); ctx.set_compile_param("LOAN_EXPIRATION_TIME", "2536857"); ctx.set_compile_param("PRINCIPAL_INTEREST_RATE", "10000"); diff --git a/txmanifest_lib/src/instance.rs b/txmanifest_lib/src/instance.rs index f915139..53d19e8 100644 --- a/txmanifest_lib/src/instance.rs +++ b/txmanifest_lib/src/instance.rs @@ -81,19 +81,29 @@ impl InstanceFile { .map(str::to_string); Some(( id.clone(), - ResolvedInput { id: id.clone(), txid, vout, amount_sat, asset, issuance_entropy }, + ResolvedInput { + id: id.clone(), + txid, + vout, + amount_sat, + asset, + issuance_entropy, + }, )) }) .collect() }) .unwrap_or_default(); - Ok(Self { instance, instance_params, provided_inputs }) + Ok(Self { + instance, + instance_params, + provided_inputs, + }) } pub fn write(&self, path: &Path) -> Result<()> { - let json = serde_json::to_string_pretty(self) - .context("Cannot serialise instance file")?; + let json = serde_json::to_string_pretty(self).context("Cannot serialise instance file")?; std::fs::write(path, json) .with_context(|| format!("Cannot write instance file: {}", path.display())) } diff --git a/txmanifest_lib/src/lib.rs b/txmanifest_lib/src/lib.rs index 9e1dbd7..2035736 100644 --- a/txmanifest_lib/src/lib.rs +++ b/txmanifest_lib/src/lib.rs @@ -1,13 +1,13 @@ -pub mod manifest; pub mod backend; pub mod canonical; pub mod config; -pub mod describe; pub mod context; pub mod covenant; +pub mod describe; pub mod eval; pub mod instance; pub mod lifecycle; +pub mod manifest; pub mod params; pub mod prepare; pub mod preview; diff --git a/txmanifest_lib/src/lifecycle.rs b/txmanifest_lib/src/lifecycle.rs index b3e56cf..ff2f917 100644 --- a/txmanifest_lib/src/lifecycle.rs +++ b/txmanifest_lib/src/lifecycle.rs @@ -7,13 +7,13 @@ use console::style; use lwk_common::Signer; use lwk_wollet::{ElementsNetwork, FsPersister, Wollet}; -use crate::manifest::{Manifest, Input}; use crate::context::{ExecutionContext, ResolvedInput}; use crate::instance::InstanceFile; -use crate::state::{history_path, ContractState, HistoryEntry, StateHistory, StateUtxo}; +use crate::manifest::{Input, Manifest}; use crate::params::ParamOverrides; use crate::preview; use crate::prompt; +use crate::state::{history_path, ContractState, HistoryEntry, StateHistory, StateUtxo}; use crate::wallet::{self, WalletFile}; use crate::{config, covenant, eval, pset_builder}; @@ -32,21 +32,28 @@ fn encode_sequence(spec: &serde_json::Value, ctx: &ExecutionContext) -> Result { if let Some(v) = map.get("relative_blocks") { - let blocks = eval::eval_amount(v, ctx).context("evaluating sequence.relative_blocks")?; + let blocks = + eval::eval_amount(v, ctx).context("evaluating sequence.relative_blocks")?; if blocks > SEQUENCE_LOCKTIME_MASK as u64 { - anyhow::bail!("relative_blocks {blocks} exceeds the 16-bit BIP68 maximum ({})", SEQUENCE_LOCKTIME_MASK); + anyhow::bail!( + "relative_blocks {blocks} exceeds the 16-bit BIP68 maximum ({})", + SEQUENCE_LOCKTIME_MASK + ); } // Type flag clear = block-based; disable flag clear = enabled. Ok(blocks as u32) } else if let Some(v) = map.get("relative_seconds") { - let secs = eval::eval_amount(v, ctx).context("evaluating sequence.relative_seconds")?; + let secs = + eval::eval_amount(v, ctx).context("evaluating sequence.relative_seconds")?; let intervals = secs.div_ceil(512); if intervals > SEQUENCE_LOCKTIME_MASK as u64 { anyhow::bail!("relative_seconds {secs} ({intervals} × 512s units) exceeds the 16-bit BIP68 maximum"); } Ok(SEQUENCE_LOCKTIME_TYPE_FLAG | intervals as u32) } else { - anyhow::bail!("sequence object must have a 'relative_blocks' or 'relative_seconds' key"); + anyhow::bail!( + "sequence object must have a 'relative_blocks' or 'relative_seconds' key" + ); } } // Bare integer or expression string → raw nSequence. @@ -64,7 +71,9 @@ fn encode_sequence(spec: &serde_json::Value, ctx: &ExecutionContext) -> Result Result> { - let Some(spec) = &inp.sequence else { return Ok(None) }; + let Some(spec) = &inp.sequence else { + return Ok(None); + }; let seq = encode_sequence(spec, ctx)?; if seq & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 { println!( @@ -137,7 +146,12 @@ impl OutpointOverride { if txid.len() != 64 || !txid.bytes().all(|b| b.is_ascii_hexdigit()) { anyhow::bail!("txid in '{s}' must be 64 hex chars"); } - Ok(Self { txid: txid.to_string(), vout, amount_sat: None, asset: None }) + Ok(Self { + txid: txid.to_string(), + vout, + amount_sat: None, + asset: None, + }) } } @@ -174,13 +188,11 @@ pub fn run( // ------------------------------------------------------------------ // Step 0 — load and parse // ------------------------------------------------------------------ - let raw = std::fs::read_to_string(manifest_file).with_context(|| { - format!("Failed to read manifest file: {}", manifest_file.display()) - })?; + let raw = std::fs::read_to_string(manifest_file) + .with_context(|| format!("Failed to read manifest file: {}", manifest_file.display()))?; - let manifest: Manifest = Manifest::from_json_str(&raw).with_context(|| { - format!("Failed to parse manifest file: {}", manifest_file.display()) - })?; + let manifest: Manifest = Manifest::from_json_str(&raw) + .with_context(|| format!("Failed to parse manifest file: {}", manifest_file.display()))?; // Whether covenants compile with SimplicityHL debug symbols (affects every CMR/address). // Sourced from the manifest so interop targets (e.g. simplicity-lending) can be matched // without hardcoding; defaults to false. @@ -196,12 +208,14 @@ pub fn run( // bare canonical. Nothing is ever clobbered, and the input state is preserved. let manifest_dir = manifest_file.parent().unwrap_or(Path::new(".")); let manifest_stem = manifest_file - .file_name().and_then(|n| n.to_str()).unwrap_or("contract") + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("contract") .trim_end_matches(".json"); // Stable, unversioned bases — used to seed numbered outputs and to derive the // single append-only history log (which must not itself be versioned). let instance_base = manifest_dir.join(format!("{manifest_stem}.instance.json")); - let state_base = manifest_dir.join(format!("{manifest_stem}.state.json")); + let state_base = manifest_dir.join(format!("{manifest_stem}.state.json")); let effective_instance_out: std::path::PathBuf = match instance_out_path { Some(p) => p.to_path_buf(), None => crate::state::next_version_path(&instance_base), @@ -221,7 +235,10 @@ pub fn run( Some(p) if p.exists() => match ContractState::load(p) { Ok(s) => Some(s), Err(e) => { - eprintln!(" {} Could not load state file: {e}", style("[warn]").yellow()); + eprintln!( + " {} Could not load state file: {e}", + style("[warn]").yellow() + ); None } }, @@ -243,9 +260,7 @@ pub fn run( // Load UTXOs from persisted wallet state for auto-selection. let available_utxos: Vec = match &loaded_wallet { - Some(w) if data_dir.exists() => { - wallet::utxos(w, data_dir).unwrap_or_else(|_| vec![]) - } + Some(w) if data_dir.exists() => wallet::utxos(w, data_dir).unwrap_or_else(|_| vec![]), _ => vec![], }; let available_explicit: Vec = match &loaded_wallet { @@ -259,7 +274,9 @@ pub fn run( let mut enclosing_template: Option<&str> = None; let action = if let Some(a) = manifest.actions.get(action_name) { a - } else if let Some((template_id, _template_def, template_action)) = manifest.find_template_action(action_name) { + } else if let Some((template_id, _template_def, template_action)) = + manifest.find_template_action(action_name) + { enclosing_template = Some(template_id); template_action } else { @@ -283,12 +300,20 @@ pub fn run( // Protocol / action header // ------------------------------------------------------------------ println!(); - println!("{}", style(format!("Protocol: {}", manifest.protocol)).bold().cyan()); + println!( + "{}", + style(format!("Protocol: {}", manifest.protocol)) + .bold() + .cyan() + ); if let Some(desc) = &manifest.description { println!(" {}", style(desc).dim()); } println!(); - println!("{}", style(format!("Action: {}", action_name)).bold().cyan()); + println!( + "{}", + style(format!("Action: {}", action_name)).bold().cyan() + ); if let Some(desc) = &action.description { println!(" {}", style(desc).dim()); } @@ -342,7 +367,8 @@ pub fn run( if loaded_fields > 0 { println!( " {} {} template field(s) loaded from instance.", - style("✓").green(), loaded_fields + style("✓").green(), + loaded_fields ); } } @@ -396,7 +422,9 @@ pub fn run( WV::Address => "wallet.address", }; let w = loaded_wallet.as_ref().ok_or_else(|| { - anyhow::anyhow!("Param '{name}' computes from {label} but no wallet is loaded") + anyhow::anyhow!( + "Param '{name}' computes from {label} but no wallet is loaded" + ) })?; let (v, detail) = match kind { WV::Key => { @@ -410,7 +438,11 @@ pub fn run( // receives at the address, so the two must agree. WV::ScriptHash | WV::Address => { let (addr, hash) = wallet::committed_output(w)?; - let v = if matches!(kind, WV::ScriptHash) { hash } else { addr }; + let v = if matches!(kind, WV::ScriptHash) { + hash + } else { + addr + }; (v, format!("[{label}]")) } }; @@ -472,18 +504,30 @@ pub fn run( // outpoint), then the manifest input spec — so `txid:vout` alone works // for covenant inputs whose amount/asset are fixed in the manifest. let state_utxo = contract_state.as_ref().and_then(|s| { - s.utxos.iter().find(|u| u.txid == ov.txid && u.vout == ov.vout) + s.utxos + .iter() + .find(|u| u.txid == ov.txid && u.vout == ov.vout) }); let asset = ov .asset .clone() .or_else(|| state_utxo.map(|u| u.asset.clone())) - .or_else(|| input.asset.as_ref().and_then(|v| eval::eval_asset_label(v, &ctx).ok())) + .or_else(|| { + input + .asset + .as_ref() + .and_then(|v| eval::eval_asset_label(v, &ctx).ok()) + }) .unwrap_or_else(|| "lbtc".to_string()); let amount_sat = ov .amount_sat .or_else(|| state_utxo.map(|u| u.amount_sat)) - .or_else(|| input.amount_sat.as_ref().and_then(|v| eval::eval_amount(v, &ctx).ok())) + .or_else(|| { + input + .amount_sat + .as_ref() + .and_then(|v| eval::eval_amount(v, &ctx).ok()) + }) .unwrap_or(0); println!( " {} {} txid={}… vout={} {} sat asset={} {}", @@ -503,8 +547,8 @@ pub fn run( asset, issuance_entropy: None, } - } else if let Some(provided) = instance - .and_then(|inst| inst.provided_inputs.get(&input.id)) + } else if let Some(provided) = + instance.and_then(|inst| inst.provided_inputs.get(&input.id)) { println!( " {} {} txid={}… vout={} {} sat asset={} {}", @@ -532,9 +576,10 @@ pub fn run( a.to_string() } }); - candidates.into_iter().find(|u| { - asset_filter.as_ref().is_none_or(|a| &u.asset == a) - }).cloned() + candidates + .into_iter() + .find(|u| asset_filter.as_ref().is_none_or(|a| &u.asset == a)) + .cloned() }); if let Some(utxo) = state_match { println!( @@ -563,8 +608,11 @@ pub fn run( &mut claimed, manual_inputs, loaded_wallet.as_ref().map(|w| { - if w.is_mainnet() { ElementsNetwork::Liquid } - else { ElementsNetwork::LiquidTestnet } + if w.is_mainnet() { + ElementsNetwork::Liquid + } else { + ElementsNetwork::LiquidTestnet + } }), &ctx, )? @@ -577,8 +625,11 @@ pub fn run( &mut claimed, manual_inputs, loaded_wallet.as_ref().map(|w| { - if w.is_mainnet() { ElementsNetwork::Liquid } - else { ElementsNetwork::LiquidTestnet } + if w.is_mainnet() { + ElementsNetwork::Liquid + } else { + ElementsNetwork::LiquidTestnet + } }), &ctx, )? @@ -589,7 +640,6 @@ pub fn run( println!(" (no inputs defined for this action)"); } - // ------------------------------------------------------------------ // Step 3a — Issuance asset IDs + on_resolved compile-param hooks // Must run before Step 3b so issuance-derived params (e.g. LENDER_NFT_ASSET_ID) @@ -599,9 +649,9 @@ pub fn run( match issuance_kind(inp) { Some("new") => { if let Some(resolved) = ctx.get_input(&inp.id) { - if let Ok((asset_id, token_id)) = pset_builder::compute_asset_ids_from_outpoint( - &resolved.txid, resolved.vout, - ) { + if let Ok((asset_id, token_id)) = + pset_builder::compute_asset_ids_from_outpoint(&resolved.txid, resolved.vout) + { ctx.set_input_attr(&inp.id, "asset", asset_id.to_string()); ctx.set_input_attr(&inp.id, "reissuance_token", token_id.to_string()); } @@ -625,7 +675,9 @@ pub fn run( } } for inp in action.inputs.as_deref().unwrap_or_default() { - let Some(hook) = &inp.on_resolved else { continue }; + let Some(hook) = &inp.on_resolved else { + continue; + }; let label = format!("[on_resolved: {}]", inp.id); run_hook_block(hook, &mut ctx, &label, Some(&inp.id)); } @@ -641,7 +693,8 @@ pub fn run( // ------------------------------------------------------------------ // Step 3b — Tapleaf-derived params (computed after hooks set asset IDs) // ------------------------------------------------------------------ - let net_for_hash = loaded_wallet.as_ref() + let net_for_hash = loaded_wallet + .as_ref() .map(wallet::elements_network) .unwrap_or(ElementsNetwork::LiquidTestnet); @@ -665,29 +718,42 @@ pub fn run( for (name, def) in simf_params { // If an override was supplied in Step 1 the param is already in ctx — skip. - if ctx.get_param(name).is_some() { continue; } + if ctx.get_param(name).is_some() { + continue; + } - let Some(crate::manifest::ParamCompute::SimfFn { simf, fn_name, compile_params: cp_names, input }) = - def.compute.as_ref().and_then(|c| c.as_spec()) else { continue }; + let Some(crate::manifest::ParamCompute::SimfFn { + simf, + fn_name, + compile_params: cp_names, + input, + }) = def.compute.as_ref().and_then(|c| c.as_spec()) + else { + continue; + }; // Build the compile-param subset that will become param:: constants. let mut cp_map = std::collections::HashMap::new(); for cp_name in cp_names { match ctx.get_compile_param(cp_name) { - Some(v) => { cp_map.insert(cp_name.clone(), v.to_string()); } + Some(v) => { + cp_map.insert(cp_name.clone(), v.to_string()); + } None => { println!( " {} {} — compile param '{}' not yet in ctx, skipping simf_fn compute", - style("[warn]").yellow(), name, cp_name + style("[warn]").yellow(), + name, + cp_name ); } } } // Resolve the runtime input value (e.g. "params.STATE_BYTES"). - let _input_hex: Option = input.as_deref().and_then(|path| { - eval::eval_expr_str(path, &ctx).ok() - }); + let _input_hex: Option = input + .as_deref() + .and_then(|path| eval::eval_expr_str(path, &ctx).ok()); let simf_path = manifest_file .parent() @@ -698,7 +764,10 @@ pub fn run( let input_hex_owned: String; let input_hex: &str = match _input_hex.as_deref() { Some(h) if h.starts_with("0x") || h.starts_with("0X") || h.is_empty() => h, - Some(h) => { input_hex_owned = format!("0x{h}"); &input_hex_owned }, + Some(h) => { + input_hex_owned = format!("0x{h}"); + &input_hex_owned + } None => "", }; match covenant::execute_simf_function( @@ -723,11 +792,18 @@ pub fn run( Err(e) => { println!( " {} {} — simf_fn failed: {}", - style("[error]").red(), name, e + style("[error]").red(), + name, + e ); // Fall back to interactive prompt so the user can supply the value manually. let default = def.default.as_deref(); - let value = prompt::prompt_param(name, &def.type_, def.description.as_deref(), default)?; + let value = prompt::prompt_param( + name, + &def.type_, + def.description.as_deref(), + default, + )?; ctx.set_param(name, value.clone()); ctx.set_compile_param(name, value); } @@ -771,8 +847,15 @@ pub fn run( println!(); println!("{}", step_header("Step 5: Fee")); let fee_rate = if let Some(ov) = overrides.get("fee_rate") { - let r: f64 = ov.parse().map_err(|e| anyhow::anyhow!("fee_rate in --params is not a number: {e}"))?; - println!(" {} Using fee rate: {} sat/vb {}", style("✓").green(), r, style("[from --params]").dim()); + let r: f64 = ov + .parse() + .map_err(|e| anyhow::anyhow!("fee_rate in --params is not a number: {e}"))?; + println!( + " {} Using fee rate: {} sat/vb {}", + style("✓").green(), + r, + style("[from --params]").dim() + ); r } else { let r = prompt::prompt_fee_rate()?; @@ -789,7 +872,10 @@ pub fn run( // Open a Wollet backed by persisted state — used for PSET building and finalization. let wollet_opt: Option = match &loaded_wallet { None => { - println!(" {} No wallet loaded — cannot build PSET.", style("[warn]").yellow()); + println!( + " {} No wallet loaded — cannot build PSET.", + style("[warn]").yellow() + ); None } Some(w) => { @@ -800,7 +886,10 @@ pub fn run( .with_context(|| format!("Cannot create data dir: {}", data_dir.display()))?; match FsPersister::new(data_dir, net, &desc) { Err(e) => { - println!(" {} Cannot open wallet state: {e}", style("[warn]").yellow()); + println!( + " {} Cannot open wallet state: {e}", + style("[warn]").yellow() + ); None } Ok(persister) => match lwk_wollet::Wollet::new(net, persister, desc) { @@ -843,13 +932,21 @@ pub fn run( let mut hints = compile_param_type_hints.clone(); if let Some(params) = &action.params { for (name, def) in params { - hints.entry(name.clone()).or_insert_with(|| def.type_.clone()); + hints + .entry(name.clone()) + .or_insert_with(|| def.type_.clone()); } } hints }; let pre_fields = eval_create_instance_fields( - ci, &ctx, manifest_file, &pre_hints, net_for_hash, false, include_debug_symbols, + ci, + &ctx, + manifest_file, + &pre_hints, + net_for_hash, + false, + include_debug_symbols, ); for (name, val) in pre_fields { ctx.set_compile_param(&name, val); @@ -888,15 +985,17 @@ pub fn run( }; if let (Some(wollet), Some(net)) = (&wollet_opt, network_for_asset) { - // ---- Populate input attrs for issuance inputs (needed by output asset resolution) ---- for inp in action.inputs.as_deref().unwrap_or_default() { match issuance_kind(inp) { Some("new") => { if let Some(resolved) = ctx.get_input(&inp.id) { - if let Ok((asset_id, token_id)) = pset_builder::compute_asset_ids_from_outpoint( - &resolved.txid, resolved.vout - ) { + if let Ok((asset_id, token_id)) = + pset_builder::compute_asset_ids_from_outpoint( + &resolved.txid, + resolved.vout, + ) + { ctx.set_input_attr(&inp.id, "asset", asset_id.to_string()); ctx.set_input_attr(&inp.id, "reissuance_token", token_id.to_string()); } @@ -910,7 +1009,8 @@ pub fn run( ctx.set_input_attr(&inp.id, "reissuance_token", &rt_asset); if let Some(entropy_hex) = entropy_hex_opt { if let Ok(entropy) = pset_builder::decode_entropy_hex(&entropy_hex) { - if let Ok(asset_id) = pset_builder::compute_asset_from_entropy(&entropy) { + if let Ok(asset_id) = pset_builder::compute_asset_from_entropy(&entropy) + { ctx.set_input_attr(&inp.id, "asset", asset_id.to_string()); } } @@ -922,7 +1022,9 @@ pub fn run( // ---- Evaluate on_resolved inline hooks ---- for inp in action.inputs.as_deref().unwrap_or_default() { - let Some(hook) = &inp.on_resolved else { continue }; + let Some(hook) = &inp.on_resolved else { + continue; + }; let label = format!("[on_resolved: {}]", inp.id); run_hook_block(hook, &mut ctx, &label, Some(&inp.id)); } @@ -937,20 +1039,32 @@ pub fn run( let iso_spec = match kind { Some("new") => { let v = inp.issuance.as_ref().unwrap(); - let asset_amount = v.get("asset_amount_sat") - .map(|a| eval::eval_amount(a, &ctx).unwrap_or(0)).unwrap_or(0); - let inflation_amount = v.get("inflation_amount_sat") - .map(|a| eval::eval_amount(a, &ctx).unwrap_or(0)).unwrap_or(0); - Some(pset_builder::IssuanceKind::New { asset_amount, inflation_amount }) + let asset_amount = v + .get("asset_amount_sat") + .map(|a| eval::eval_amount(a, &ctx).unwrap_or(0)) + .unwrap_or(0); + let inflation_amount = v + .get("inflation_amount_sat") + .map(|a| eval::eval_amount(a, &ctx).unwrap_or(0)) + .unwrap_or(0); + Some(pset_builder::IssuanceKind::New { + asset_amount, + inflation_amount, + }) } Some("reissue") => { let v = inp.issuance.as_ref().unwrap(); - let asset_amount = match v.get("asset_amount_sat") + let asset_amount = match v + .get("asset_amount_sat") .map(|a| eval::eval_amount(a, &ctx)) { Some(Ok(n)) => n, Some(Err(e)) => { - println!(" {} Input '{}' reissue amount eval failed: {e}", style("[error]").red(), inp.id); + println!( + " {} Input '{}' reissue amount eval failed: {e}", + style("[error]").red(), + inp.id + ); collect_inputs_ok = false; break; } @@ -961,7 +1075,11 @@ pub fn run( match pset_builder::decode_entropy_hex(hex) { Ok(e) => e, Err(err) => { - println!(" {} Input '{}' entropy decode failed: {err}", style("[error]").red(), inp.id); + println!( + " {} Input '{}' entropy decode failed: {err}", + style("[error]").red(), + inp.id + ); collect_inputs_ok = false; break; } @@ -972,11 +1090,18 @@ pub fn run( break; } } else { - println!(" {} Input '{}' not resolved", style("[error]").red(), inp.id); + println!( + " {} Input '{}' not resolved", + style("[error]").red(), + inp.id + ); collect_inputs_ok = false; break; }; - Some(pset_builder::IssuanceKind::Reissue { asset_amount, entropy }) + Some(pset_builder::IssuanceKind::Reissue { + asset_amount, + entropy, + }) } _ => None, }; @@ -985,7 +1110,11 @@ pub fn run( let input_sequence = match resolve_input_sequence(inp, &ctx) { Ok(s) => s, Err(e) => { - println!(" {} Input '{}' sequence: {e}", style("[error]").red(), inp.id); + println!( + " {} Input '{}' sequence: {e}", + style("[error]").red(), + inp.id + ); collect_inputs_ok = false; break; } @@ -993,12 +1122,14 @@ pub fn run( if inp.is_wallet_source() { let resolved_result: Result<(lwk_wollet::elements::Txid, u32)> = (|| { - let resolved = ctx.get_input(&inp.id) + let resolved = ctx + .get_input(&inp.id) .ok_or_else(|| anyhow::anyhow!("Input '{}' not resolved", inp.id))?; let txid = lwk_wollet::elements::Txid::from_str(&resolved.txid) .with_context(|| format!("Cannot parse txid '{}'", resolved.txid))?; Ok((txid, resolved.vout)) - })(); + })( + ); match resolved_result { Err(e) => { println!(" {} {e}", style("[error]").red()); @@ -1007,14 +1138,22 @@ pub fn run( } Ok((txid, vout)) => { // First try confidential (CT) wallet UTXOs. - if let Some(utxo) = available_utxos.iter().find(|u| u.outpoint.txid == txid && u.outpoint.vout == vout).cloned() { + if let Some(utxo) = available_utxos + .iter() + .find(|u| u.outpoint.txid == txid && u.outpoint.vout == vout) + .cloned() + { pset_inputs.push(pset_builder::PsetInput::Wallet { input_id: inp.id.clone(), utxo, issuance: iso_spec, sequence: input_sequence, }); - } else if let Some(ext) = available_explicit.iter().find(|u| u.outpoint.txid == txid && u.outpoint.vout == vout).cloned() { + } else if let Some(ext) = available_explicit + .iter() + .find(|u| u.outpoint.txid == txid && u.outpoint.vout == vout) + .cloned() + { // Explicit (non-confidential) wallet UTXO — treat like a covenant input. pset_inputs.push(pset_builder::PsetInput::Covenant { input_id: inp.id.clone(), @@ -1028,7 +1167,9 @@ pub fn run( } else { println!( " {} UTXO {}:{} not found in wallet state — run `sync` first", - style("[error]").red(), txid, vout + style("[error]").red(), + txid, + vout ); collect_inputs_ok = false; break; @@ -1052,21 +1193,47 @@ pub fn run( break; } }; - let inp_simf_path = inp_ut.script.as_ref() + let inp_simf_path = inp_ut + .script + .as_ref() .and_then(|s| s.source.as_deref()) - .map(|src| manifest_file.parent().unwrap_or(std::path::Path::new(".")).join(src)) + .map(|src| { + manifest_file + .parent() + .unwrap_or(std::path::Path::new(".")) + .join(src) + }) .unwrap_or_else(|| simf_path.clone()); - let (inp_params, inp_hints) = apply_utxo_compile_params(&compile_params_map, &compile_param_type_hints, inp_ut); + let (inp_params, inp_hints) = apply_utxo_compile_params( + &compile_params_map, + &compile_param_type_hints, + inp_ut, + ); // Per-input `utxo_source.compile_params` overrides (resolved against action // params), mirroring the output `destination.compile_params` form. let (inp_params, inp_hints) = apply_site_compile_param_overrides( - inp_params, inp_hints, inp.utxo_source.get("compile_params"), - action, &compile_param_type_hints, &ctx, + inp_params, + inp_hints, + inp.utxo_source.get("compile_params"), + action, + &compile_param_type_hints, + &ctx, ); - let script_pubkey = match pset_builder::covenant_script_pubkey(&inp_simf_path, &inp_params, &inp_hints, &leaf_payloads, net, include_debug_symbols) { + let script_pubkey = match pset_builder::covenant_script_pubkey( + &inp_simf_path, + &inp_params, + &inp_hints, + &leaf_payloads, + net, + include_debug_symbols, + ) { Ok(s) => s, Err(e) => { - println!(" {} Covenant address failed (input '{}'):", style("[error]").red(), inp.id); + println!( + " {} Covenant address failed (input '{}'):", + style("[error]").red(), + inp.id + ); for (i, cause) in e.chain().enumerate() { println!(" {i}: {cause}"); } @@ -1077,7 +1244,11 @@ pub fn run( let resolved = match ctx.get_input(&inp.id) { Some(r) => r.clone(), None => { - println!(" {} Input '{}' not resolved", style("[error]").red(), inp.id); + println!( + " {} Input '{}' not resolved", + style("[error]").red(), + inp.id + ); collect_inputs_ok = false; break; } @@ -1085,7 +1256,11 @@ pub fn run( let asset_id = match lwk_wollet::elements::AssetId::from_str(&resolved.asset) { Ok(a) => a, Err(e) => { - println!(" {} Input '{}' asset parse failed: {e}", style("[error]").red(), inp.id); + println!( + " {} Input '{}' asset parse failed: {e}", + style("[error]").red(), + inp.id + ); collect_inputs_ok = false; break; } @@ -1093,7 +1268,11 @@ pub fn run( let txid = match lwk_wollet::elements::Txid::from_str(&resolved.txid) { Ok(t) => t, Err(e) => { - println!(" {} Input '{}' txid parse failed: {e}", style("[error]").red(), inp.id); + println!( + " {} Input '{}' txid parse failed: {e}", + style("[error]").red(), + inp.id + ); collect_inputs_ok = false; break; } @@ -1131,9 +1310,16 @@ pub fn run( let is_op_return = matches!(dest_type, Some("op_return") | Some("burn")); let amount = match &output.amount_sat { None => { - if output.optional.unwrap_or(false) || is_change { continue; } - if is_op_return { 0u64 } else { - anyhow::bail!("Output '{}' has no amount_sat and is not optional.", output.id); + if output.optional.unwrap_or(false) || is_change { + continue; + } + if is_op_return { + 0u64 + } else { + anyhow::bail!( + "Output '{}' has no amount_sat and is not optional.", + output.id + ); } } Some(v) => match eval::eval_amount(v, &ctx) { @@ -1143,7 +1329,11 @@ pub fn run( println!(" {} Output '{}' amount_sat eval failed (optional — skipping): {e}", style("·").dim(), output.id); continue; } - println!(" {} Output '{}' amount_sat eval failed: {e}", style("[error]").red(), output.id); + println!( + " {} Output '{}' amount_sat eval failed: {e}", + style("[error]").red(), + output.id + ); collect_outputs_ok = false; break; } @@ -1151,34 +1341,52 @@ pub fn run( }; if output.optional.unwrap_or(false) && amount == 0 { - println!(" {} Output '{}' amount=0, optional — skipping.", style("·").dim(), output.id); + println!( + " {} Output '{}' amount=0, optional — skipping.", + style("·").dim(), + output.id + ); continue; } let asset_label = match output.asset.as_ref() { None => "lbtc".to_string(), - Some(v) => match eval::eval_asset_label(v, &ctx) { - Ok(a) => a, - Err(e) => { - if output.optional.unwrap_or(false) { - println!(" {} Output '{}' asset eval failed (optional — skipping): {e}", style("·").dim(), output.id); - continue; + Some(v) => { + match eval::eval_asset_label(v, &ctx) { + Ok(a) => a, + Err(e) => { + if output.optional.unwrap_or(false) { + println!(" {} Output '{}' asset eval failed (optional — skipping): {e}", style("·").dim(), output.id); + continue; + } + println!( + " {} Output '{}' asset eval failed: {e}", + style("[error]").red(), + output.id + ); + collect_outputs_ok = false; + break; } - println!(" {} Output '{}' asset eval failed: {e}", style("[error]").red(), output.id); - collect_outputs_ok = false; - break; } - }, + } }; let asset_id = match resolve_asset_id(&asset_label, net) { Ok(id) => id, Err(e) => { if output.optional.unwrap_or(false) { - println!(" {} Output '{}' asset ID failed (optional — skipping): {e}", style("·").dim(), output.id); + println!( + " {} Output '{}' asset ID failed (optional — skipping): {e}", + style("·").dim(), + output.id + ); continue; } - println!(" {} Output '{}' asset ID failed: {e}", style("[error]").red(), output.id); + println!( + " {} Output '{}' asset ID failed: {e}", + style("[error]").red(), + output.id + ); collect_outputs_ok = false; break; } @@ -1186,48 +1394,85 @@ pub fn run( match &output.destination { serde_json::Value::String(dest) if dest == "change" => { - println!(" {} Output '{}' → change (auto).", style("·").dim(), output.id); + println!( + " {} Output '{}' → change (auto).", + style("·").dim(), + output.id + ); continue; } serde_json::Value::Object(m) - if m.get("type").and_then(|v| v.as_str()) == Some("fee") => { continue; } + if m.get("type").and_then(|v| v.as_str()) == Some("fee") => + { + continue; + } serde_json::Value::Object(m) - if matches!(m.get("type").and_then(|v| v.as_str()), Some("op_return") | Some("burn")) => + if matches!( + m.get("type").and_then(|v| v.as_str()), + Some("op_return") | Some("burn") + ) => { // Bare `OP_RETURN` by default (sufficient for NFT burns); if a `data` // expression is present, embed its bytes so indexers can discover the tx. let script_pubkey = match &output.data { None => lwk_wollet::elements::Script::from(vec![0x6au8]), - Some(expr) => match eval::eval_op_return_data(expr, &ctx, &compile_param_type_hints) { + Some(expr) => match eval::eval_op_return_data( + expr, + &ctx, + &compile_param_type_hints, + ) { Ok(bytes) => lwk_wollet::elements::Script::new_op_return(&bytes), Err(e) => { - println!(" {} Output '{}' OP_RETURN data eval failed: {e}", style("[error]").red(), output.id); + println!( + " {} Output '{}' OP_RETURN data eval failed: {e}", + style("[error]").red(), + output.id + ); collect_outputs_ok = false; break; } }, }; - let data_note = if output.data.is_some() { format!(" ({} data bytes)", script_pubkey.len().saturating_sub(2)) } else { String::new() }; + let data_note = if output.data.is_some() { + format!(" ({} data bytes)", script_pubkey.len().saturating_sub(2)) + } else { + String::new() + }; println!( " {} Output '{}': {} sat {} → OP_RETURN{}", - style("+").green(), output.id, style(amount).yellow(), asset_label, data_note + style("+").green(), + output.id, + style(amount).yellow(), + asset_label, + data_note ); pset_outputs.push(pset_builder::PsetOutputSpec { - script_pubkey, amount, asset: asset_id, blinding_key: None, + script_pubkey, + amount, + asset: asset_id, + blinding_key: None, }); } serde_json::Value::Object(m) if m.contains_key("utxo_type") => { let type_name = match m["utxo_type"].as_str() { Some(s) => s, None => { - println!(" {} Output '{}' utxo_type is not a string — skipping.", style("[TODO]").yellow(), output.id); + println!( + " {} Output '{}' utxo_type is not a string — skipping.", + style("[TODO]").yellow(), + output.id + ); continue; } }; let ut = match manifest.utxo_type(type_name) { Ok(ut) => ut, Err(e) => { - println!(" {} Output '{}' utxo_type error: {e}", style("[error]").red(), output.id); + println!( + " {} Output '{}' utxo_type error: {e}", + style("[error]").red(), + output.id + ); collect_outputs_ok = false; break; } @@ -1236,26 +1481,56 @@ pub fn run( let leaf_payloads = match ut.resolve_extra_leaf_payloads(&ctx) { Ok(p) => p, Err(e) => { - println!(" {} Output '{}' extra leaves error: {e}", style("[warn]").yellow(), output.id); + println!( + " {} Output '{}' extra leaves error: {e}", + style("[warn]").yellow(), + output.id + ); collect_outputs_ok = false; break; } }; - let out_simf_path = ut.script.as_ref() + let out_simf_path = ut + .script + .as_ref() .and_then(|s| s.source.as_deref()) - .map(|src| manifest_file.parent().unwrap_or(std::path::Path::new(".")).join(src)) + .map(|src| { + manifest_file + .parent() + .unwrap_or(std::path::Path::new(".")) + .join(src) + }) .unwrap_or_else(|| simf_path.clone()); - let (out_params, out_hints) = apply_utxo_compile_params(&compile_params_map, &compile_param_type_hints, ut); + let (out_params, out_hints) = apply_utxo_compile_params( + &compile_params_map, + &compile_param_type_hints, + ut, + ); // Per-output `destination.compile_params` overrides (resolved against // action params), so a covenant can be keyed by a runtime value. let (out_params, out_hints) = apply_site_compile_param_overrides( - out_params, out_hints, m.get("compile_params"), - action, &compile_param_type_hints, &ctx, + out_params, + out_hints, + m.get("compile_params"), + action, + &compile_param_type_hints, + &ctx, ); - let script_pubkey = match pset_builder::covenant_script_pubkey(&out_simf_path, &out_params, &out_hints, &leaf_payloads, net, include_debug_symbols) { + let script_pubkey = match pset_builder::covenant_script_pubkey( + &out_simf_path, + &out_params, + &out_hints, + &leaf_payloads, + net, + include_debug_symbols, + ) { Ok(s) => s, Err(e) => { - println!(" {} Covenant address failed (output '{}'):", style("[error]").red(), output.id); + println!( + " {} Covenant address failed (output '{}'):", + style("[error]").red(), + output.id + ); for (i, cause) in e.chain().enumerate() { println!(" {i}: {cause}"); } @@ -1271,10 +1546,19 @@ pub fn run( } else { None }; - let conf_label = if confidential { "confidential" } else { "explicit" }; + let conf_label = if confidential { + "confidential" + } else { + "explicit" + }; println!( " {} Output '{}': {} sat {} → covenant ({}, {})", - style("+").green(), output.id, style(amount).yellow(), asset_label, type_name, conf_label + style("+").green(), + output.id, + style(amount).yellow(), + asset_label, + type_name, + conf_label ); covenant_output_meta.push(CovenantOutputMeta { utxo_type: type_name.to_string(), @@ -1284,14 +1568,21 @@ pub fn run( asset: asset_id, }); pset_outputs.push(pset_builder::PsetOutputSpec { - script_pubkey, amount, asset: asset_id, blinding_key, + script_pubkey, + amount, + asset: asset_id, + blinding_key, }); } serde_json::Value::String(dest) if dest == "wallet" => { let addr_result = match wollet.address(next_wallet_addr_idx) { Ok(a) => a, Err(e) => { - println!(" {} Output '{}' wallet address failed: {e}", style("[warn]").yellow(), output.id); + println!( + " {} Output '{}' wallet address failed: {e}", + style("[warn]").yellow(), + output.id + ); continue; } }; @@ -1299,41 +1590,71 @@ pub fn run( let addr = addr_result.address().clone(); // Resolution order: per-output → chain default. // Bitcoin does not support confidential outputs; Liquid defaults to confidential. - let chain_default = matches!(net, ElementsNetwork::Liquid | ElementsNetwork::LiquidTestnet); + let chain_default = matches!( + net, + ElementsNetwork::Liquid | ElementsNetwork::LiquidTestnet + ); let is_confidential = output.confidential.unwrap_or(chain_default); let bpk = if is_confidential { - addr.blinding_pubkey.map(|pk| lwk_wollet::elements::bitcoin::PublicKey { inner: pk, compressed: true }) + addr.blinding_pubkey.map(|pk| { + lwk_wollet::elements::bitcoin::PublicKey { + inner: pk, + compressed: true, + } + }) } else { None }; let addr_str = addr.to_string(); println!( " {} Output '{}': {} sat {} → wallet ({}…)", - style("+").green(), output.id, style(amount).yellow(), asset_label, + style("+").green(), + output.id, + style(amount).yellow(), + asset_label, &addr_str[..addr_str.len().min(24)] ); pset_outputs.push(pset_builder::PsetOutputSpec { - script_pubkey: addr.script_pubkey(), amount, asset: asset_id, blinding_key: bpk, + script_pubkey: addr.script_pubkey(), + amount, + asset: asset_id, + blinding_key: bpk, }); } serde_json::Value::String(dest) => { - let addr_str = eval::eval_destination_str(dest, &ctx) - .unwrap_or_else(|| dest.clone()); + let addr_str = + eval::eval_destination_str(dest, &ctx).unwrap_or_else(|| dest.clone()); let addr = match addr_str.trim().parse::() { Ok(a) => a, Err(e) => { - println!(" {} Output '{}' address parse failed ('{}': {e})", style("[warn]").yellow(), output.id, addr_str); + println!( + " {} Output '{}' address parse failed ('{}': {e})", + style("[warn]").yellow(), + output.id, + addr_str + ); continue; } }; - let bpk = addr.blinding_pubkey.map(|pk| lwk_wollet::elements::bitcoin::PublicKey { inner: pk, compressed: true }); + let bpk = addr.blinding_pubkey.map(|pk| { + lwk_wollet::elements::bitcoin::PublicKey { + inner: pk, + compressed: true, + } + }); println!( " {} Output '{}': {} sat {} → {}…", - style("+").green(), output.id, style(amount).yellow(), asset_label, + style("+").green(), + output.id, + style(amount).yellow(), + asset_label, &addr_str[..addr_str.len().min(24)] ); pset_outputs.push(pset_builder::PsetOutputSpec { - script_pubkey: addr.script_pubkey(), amount, asset: asset_id, blinding_key: bpk, + script_pubkey: addr.script_pubkey(), + amount, + asset: asset_id, + blinding_key: bpk, }); } serde_json::Value::Object(m) if m.contains_key("script_hash") => { @@ -1342,22 +1663,33 @@ pub fn run( .unwrap_or_else(|| hash_ref.to_string()); let clean = resolved.trim().trim_start_matches("0x"); if clean.len() != 64 { - println!(" {} Output '{}' script_hash must be 32 bytes hex (got {} chars)", style("[error]").red(), output.id, clean.len()); + println!( + " {} Output '{}' script_hash must be 32 bytes hex (got {} chars)", + style("[error]").red(), + output.id, + clean.len() + ); collect_outputs_ok = false; break; } let mut bytes = [0u8; 32]; for i in 0..32 { - bytes[i] = match u8::from_str_radix(&clean[i*2..i*2+2], 16) { + bytes[i] = match u8::from_str_radix(&clean[i * 2..i * 2 + 2], 16) { Ok(b) => b, Err(_) => { - println!(" {} Output '{}' script_hash invalid hex", style("[error]").red(), output.id); + println!( + " {} Output '{}' script_hash invalid hex", + style("[error]").red(), + output.id + ); collect_outputs_ok = false; break; } }; } - if !collect_outputs_ok { break; } + if !collect_outputs_ok { + break; + } // P2TR: OP_1 OP_PUSHBYTES_32 let mut script_bytes = Vec::with_capacity(34); script_bytes.push(0x51u8); // OP_1 @@ -1366,15 +1698,26 @@ pub fn run( let script_pubkey = lwk_wollet::elements::Script::from(script_bytes); println!( " {} Output '{}': {} sat {} → P2TR ({}…)", - style("+").green(), output.id, style(amount).yellow(), asset_label, + style("+").green(), + output.id, + style(amount).yellow(), + asset_label, &clean[..16] ); pset_outputs.push(pset_builder::PsetOutputSpec { - script_pubkey, amount, asset: asset_id, blinding_key: None, + script_pubkey, + amount, + asset: asset_id, + blinding_key: None, }); } other => { - println!(" {} Output '{}' unsupported destination: {}", style("[TODO]").yellow(), output.id, other); + println!( + " {} Output '{}' unsupported destination: {}", + style("[TODO]").yellow(), + output.id, + other + ); continue; } } @@ -1390,7 +1733,11 @@ pub fn run( if collect_inputs_ok && collect_outputs_ok { // Only build a change output if the action declared one. Otherwise the // fee absorbs the surplus and the output count stays exact (recursive covenants). - let build_change = action.outputs.as_deref().unwrap_or_default().iter() + let build_change = action + .outputs + .as_deref() + .unwrap_or_default() + .iter() .any(|o| o.destination.as_str() == Some("change")); let mut req = pset_builder::BuildPsetRequest { inputs: pset_inputs, @@ -1403,14 +1750,24 @@ pub fn run( // Resolve the `fee` keyword: estimate the fee from the current (fee=0) // draft, then re-evaluate any output amount that referenced `fee`. The // amounts don't affect the tx vsize, so the draft gives the right size. - if out_amount_formulas.iter().filter_map(|(_, f)| f.as_ref()).any(amount_uses_fee_keyword) { + if out_amount_formulas + .iter() + .filter_map(|(_, f)| f.as_ref()) + .any(amount_uses_fee_keyword) + { match pset_builder::estimate_fee(wollet, net, &req) { Ok(est) => { ctx.set_fee(est); - println!(" {} Estimated network fee: {} sat (resolves `fee`)", style("✓").green(), est); + println!( + " {} Estimated network fee: {} sat (resolves `fee`)", + style("✓").green(), + est + ); for (i, (out_id, formula)) in out_amount_formulas.iter().enumerate() { let Some(f) = formula else { continue }; - if !amount_uses_fee_keyword(f) { continue; } + if !amount_uses_fee_keyword(f) { + continue; + } match eval::eval_amount(f, &ctx) { Ok(a) if i < req.outputs.len() => { req.outputs[i].amount = a; @@ -1423,17 +1780,27 @@ pub fn run( } } Ok(_) => {} - Err(e) => println!(" {} Re-evaluating output #{i} with fee failed: {e}", style("[error]").red()), + Err(e) => println!( + " {} Re-evaluating output #{i} with fee failed: {e}", + style("[error]").red() + ), } } } - Err(e) => println!(" {} Fee estimation failed (`fee` stays 0): {e}", style("[warn]").yellow()), + Err(e) => println!( + " {} Fee estimation failed (`fee` stays 0): {e}", + style("[warn]").yellow() + ), } } println!(); - println!(" {} Building PSET ({} inputs, {} outputs)…", - style("·").dim(), req.inputs.len(), req.outputs.len()); + println!( + " {} Building PSET ({} inputs, {} outputs)…", + style("·").dim(), + req.inputs.len(), + req.outputs.len() + ); match pset_builder::build_pset(wollet, net, &req) { Err(e) => { @@ -1444,20 +1811,36 @@ pub fn run( } Ok(result) => { for iso in &result.issuances { - println!(" Issuance '{}': asset={}, token={}", iso.input_id, + println!( + " Issuance '{}': asset={}, token={}", + iso.input_id, style(&iso.asset_id.to_string()[..16]).yellow(), - style(&iso.token_id.to_string()[..16]).yellow()); + style(&iso.token_id.to_string()[..16]).yellow() + ); ctx.set_input_attr(&iso.input_id, "asset", iso.asset_id.to_string()); - ctx.set_input_attr(&iso.input_id, "reissuance_token", iso.token_id.to_string()); + ctx.set_input_attr( + &iso.input_id, + "reissuance_token", + iso.token_id.to_string(), + ); if let Some(entropy_bytes) = &iso.entropy { - let hex = entropy_bytes.iter().map(|b| format!("{b:02x}")).collect::(); + let hex = entropy_bytes + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); ctx.set_input_entropy(&iso.input_id, hex); } } - println!(" {} PSET constructed ({} outputs).", style("✓").green(), result.pset.outputs().len()); + println!( + " {} PSET constructed ({} outputs).", + style("✓").green(), + result.pset.outputs().len() + ); for (i, out) in result.pset.outputs().iter().enumerate() { if out.script_pubkey.is_empty() { - if let Some(amt) = out.amount { println!(" Output #{i}: fee {} sat", amt); } + if let Some(amt) = out.amount { + println!(" Output #{i}: fee {} sat", amt); + } } else { let blinded = out.amount.is_none() && out.amount_comm.is_some(); let label = if blinded { "confidential" } else { "explicit" }; @@ -1473,7 +1856,10 @@ pub fn run( } } } else { - println!(" {} No wallet/network — cannot build PSET.", style("[warn]").yellow()); + println!( + " {} No wallet/network — cannot build PSET.", + style("[warn]").yellow() + ); } // ------------------------------------------------------------------ @@ -1498,10 +1884,16 @@ pub fn run( } } (None, _) => { - println!(" {} No PSET to sign (not built in Step 7).", style("[skip]").yellow()); + println!( + " {} No PSET to sign (not built in Step 7).", + style("[skip]").yellow() + ); } (_, None) => { - println!(" {} No wallet loaded — cannot sign.", style("[warn]").yellow()); + println!( + " {} No wallet loaded — cannot sign.", + style("[warn]").yellow() + ); } } @@ -1511,13 +1903,19 @@ pub fn run( println!(); println!("{}", step_header("Step 9: Dry-run")); { - let covenant_inputs: Vec<_> = action.inputs.as_deref().unwrap_or_default() + let covenant_inputs: Vec<_> = action + .inputs + .as_deref() + .unwrap_or_default() .iter() .filter(|i| i.utxo_type_name().is_some()) .collect(); if covenant_inputs.is_empty() { - println!(" {} No Simplicity covenant inputs — dry-run skipped.", style("·").dim()); + println!( + " {} No Simplicity covenant inputs — dry-run skipped.", + style("·").dim() + ); } else { println!( " {} {} covenant input(s) to verify.", @@ -1528,25 +1926,53 @@ pub fn run( for inp in &covenant_inputs { let type_name = inp.utxo_type_name().unwrap(); let check_ut = manifest.utxo_type(&type_name).ok(); - let check_simf_path = check_ut.as_ref() - .and_then(|ut| ut.script.as_ref().and_then(|s| s.source.as_deref()).map(|src| { - manifest_file.parent().unwrap_or(std::path::Path::new(".")).join(src) - })) + let check_simf_path = check_ut + .as_ref() + .and_then(|ut| { + ut.script + .as_ref() + .and_then(|s| s.source.as_deref()) + .map(|src| { + manifest_file + .parent() + .unwrap_or(std::path::Path::new(".")) + .join(src) + }) + }) .unwrap_or_else(|| simf_path.clone()); let (check_params, check_hints) = check_ut - .map(|ut| apply_utxo_compile_params(&compile_params_map, &compile_param_type_hints, ut)) - .unwrap_or_else(|| (compile_params_map.clone(), compile_param_type_hints.clone())); + .map(|ut| { + apply_utxo_compile_params( + &compile_params_map, + &compile_param_type_hints, + ut, + ) + }) + .unwrap_or_else(|| { + (compile_params_map.clone(), compile_param_type_hints.clone()) + }); let (check_params, check_hints) = apply_site_compile_param_overrides( - check_params, check_hints, inp.utxo_source.get("compile_params"), - action, &compile_param_type_hints, &ctx, + check_params, + check_hints, + inp.utxo_source.get("compile_params"), + action, + &compile_param_type_hints, + &ctx, ); print!( " {} Input '{}' ({}) — compiling… ", - style("·").dim(), inp.id, type_name + style("·").dim(), + inp.id, + type_name ); use std::io::Write; let _ = std::io::stdout().flush(); - match covenant::check_compile(&check_simf_path, &check_params, &check_hints, include_debug_symbols) { + match covenant::check_compile( + &check_simf_path, + &check_params, + &check_hints, + include_debug_symbols, + ) { Ok(()) => println!("{}", style("OK").green()), Err(e) => { println!("{}", style("FAILED").red()); @@ -1566,7 +1992,7 @@ pub fn run( } Ok(tx) => { use std::sync::Arc; - + let tx = Arc::new(tx); let witness_utxos: Vec> = pset @@ -1597,41 +2023,64 @@ pub fn run( Err(e) => { println!( " {} utxo_type for '{}': {e}", - style("[error]").red(), action_inp.id + style("[error]").red(), + action_inp.id ); exec_all_ok = false; continue; } }; - let leaf_payloads = match dry_ut.resolve_extra_leaf_payloads(&ctx) { - Ok(p) => p, - Err(e) => { - println!( - " {} leaf_payloads for '{}': {e}", - style("[error]").red(), action_inp.id - ); - exec_all_ok = false; - continue; - } - }; - let dry_simf_path = dry_ut.script.as_ref() + let leaf_payloads = + match dry_ut.resolve_extra_leaf_payloads(&ctx) { + Ok(p) => p, + Err(e) => { + println!( + " {} leaf_payloads for '{}': {e}", + style("[error]").red(), + action_inp.id + ); + exec_all_ok = false; + continue; + } + }; + let dry_simf_path = dry_ut + .script + .as_ref() .and_then(|s| s.source.as_deref()) - .map(|src| manifest_file.parent().unwrap_or(std::path::Path::new(".")).join(src)) + .map(|src| { + manifest_file + .parent() + .unwrap_or(std::path::Path::new(".")) + .join(src) + }) .unwrap_or_else(|| simf_path.clone()); - let (dry_params, dry_hints) = apply_utxo_compile_params(&compile_params_map, &compile_param_type_hints, dry_ut); - let (dry_params, dry_hints) = apply_site_compile_param_overrides( - dry_params, dry_hints, action_inp.utxo_source.get("compile_params"), - action, &compile_param_type_hints, &ctx, + let (dry_params, dry_hints) = apply_utxo_compile_params( + &compile_params_map, + &compile_param_type_hints, + dry_ut, ); + let (dry_params, dry_hints) = + apply_site_compile_param_overrides( + dry_params, + dry_hints, + action_inp.utxo_source.get("compile_params"), + action, + &compile_param_type_hints, + &ctx, + ); use std::io::Write; print!( " {} Input '{}' ({}) — executing… ", - style("·").dim(), action_inp.id, type_name + style("·").dim(), + action_inp.id, + type_name ); let _ = std::io::stdout().flush(); - let dry_witnesses = action_inp.witnesses.as_ref() + let dry_witnesses = action_inp + .witnesses + .as_ref() .map(|w| eval::resolve_witness_refs(w, &ctx)); let dry_inp_witnesses = action_inp.witnesses.clone(); let dry_params_snap = compile_params_map.clone(); @@ -1711,7 +2160,10 @@ pub fn run( // Set final_script_witness on every covenant PSET input so that wollet.finalize() // only needs to handle wallet inputs. Must run after a successful dry-run (Step 9). { - let covenant_input_count = action.inputs.as_deref().unwrap_or_default() + let covenant_input_count = action + .inputs + .as_deref() + .unwrap_or_default() .iter() .filter(|i| i.utxo_type_name().is_some()) .count(); @@ -1721,7 +2173,9 @@ pub fn run( println!("{}", step_header("Step 9c: Covenant Finalization")); if let Some(ref mut pset) = signed_pset { - let witness_utxos: Vec> = pset.inputs().iter() + let witness_utxos: Vec> = pset + .inputs() + .iter() .map(|inp| inp.witness_utxo.clone()) .collect(); @@ -1741,21 +2195,24 @@ pub fn run( ), Ok(tx) => { use std::sync::Arc; - + let tx = Arc::new(tx); let genesis_hash = network_genesis_hash(net_for_hash); let action_inputs = action.inputs.as_deref().unwrap_or_default(); let mut all_finalized = true; for (pset_idx, action_inp) in action_inputs.iter().enumerate() { - let Some(type_name) = action_inp.utxo_type_name() else { continue }; + let Some(type_name) = action_inp.utxo_type_name() else { + continue; + }; let fin_ut = match manifest.utxo_type(&type_name) { Ok(ut) => ut, Err(e) => { println!( " {} utxo_type '{}': {e}", - style("[error]").red(), type_name + style("[error]").red(), + type_name ); all_finalized = false; continue; @@ -1766,31 +2223,43 @@ pub fn run( Err(e) => { println!( " {} leaf_payloads for '{}': {e}", - style("[error]").red(), action_inp.id + style("[error]").red(), + action_inp.id ); all_finalized = false; continue; } }; - let fin_simf_path = fin_ut.script.as_ref() + let fin_simf_path = fin_ut + .script + .as_ref() .and_then(|s| s.source.as_deref()) .map(|src| { - manifest_file.parent() + manifest_file + .parent() .unwrap_or(std::path::Path::new(".")) .join(src) }) .unwrap_or_else(|| simf_path.clone()); let (fin_params, fin_hints) = apply_utxo_compile_params( - &compile_params_map, &compile_param_type_hints, fin_ut, + &compile_params_map, + &compile_param_type_hints, + fin_ut, ); let (fin_params, fin_hints) = apply_site_compile_param_overrides( - fin_params, fin_hints, action_inp.utxo_source.get("compile_params"), - action, &compile_param_type_hints, &ctx, + fin_params, + fin_hints, + action_inp.utxo_source.get("compile_params"), + action, + &compile_param_type_hints, + &ctx, ); print!( " {} Input '{}' ({}) — finalizing… ", - style("·").dim(), action_inp.id, type_name + style("·").dim(), + action_inp.id, + type_name ); use std::io::Write as _; let _ = std::io::stdout().flush(); @@ -1798,7 +2267,9 @@ pub fn run( // Build a signer closure for any "type": "Signature" witnesses. // Resolves the key reference from compile_params, then signs // the hash with the wallet key. - let fin_witnesses = action_inp.witnesses.as_ref() + let fin_witnesses = action_inp + .witnesses + .as_ref() .map(|w| eval::resolve_witness_refs(w, &ctx)); let inp_witnesses = action_inp.witnesses.clone(); let params_snap = compile_params_map.clone(); @@ -1845,10 +2316,7 @@ pub fn run( } if all_finalized { - println!( - " {} All covenant inputs finalized.", - style("✓").green() - ); + println!(" {} All covenant inputs finalized.", style("✓").green()); } else { println!( " {} One or more covenant inputs failed to finalize.", @@ -1877,7 +2345,9 @@ pub fn run( let mut hints = compile_param_type_hints.clone(); if let Some(params) = &action.params { for (name, def) in params { - hints.entry(name.clone()).or_insert_with(|| def.type_.clone()); + hints + .entry(name.clone()) + .or_insert_with(|| def.type_.clone()); } } hints @@ -1887,7 +2357,13 @@ pub fn run( println!(); println!("{}", step_header("Step 9b: Creating Instance")); let fields = eval_create_instance_fields( - ci, &ctx, manifest_file, &create_instance_hints, net_for_hash, true, include_debug_symbols, + ci, + &ctx, + manifest_file, + &create_instance_hints, + net_for_hash, + true, + include_debug_symbols, ); let inst = crate::instance::InstanceFile { instance: Some(crate::instance::InstanceData { @@ -1922,18 +2398,26 @@ pub fn run( " {} No signed PSET available — cannot broadcast.", style("[warn]").yellow() ); - println!(" Complete Steps 7 and 8 first (requires an action with concrete address outputs)."); + println!( + " Complete Steps 7 and 8 first (requires an action with concrete address outputs)." + ); println!(); return Ok(()); } // --export-pset: write PSET (base64) + tx (hex) to separate files, skip broadcast. - if let (Some(export_path), Some(mut pset), Some(wollet)) = (export_pset_path, signed_pset.clone(), &wollet_opt) { - println!("{}", style("=== Exporting PSET (no broadcast) ===").bold().cyan()); + if let (Some(export_path), Some(mut pset), Some(wollet)) = + (export_pset_path, signed_pset.clone(), &wollet_opt) + { + println!( + "{}", + style("=== Exporting PSET (no broadcast) ===").bold().cyan() + ); // Derive tx path: replace/add .tx.hex extension alongside the pset file. let tx_path = { - let stem = export_path.file_stem() + let stem = export_path + .file_stem() .and_then(|s| s.to_str()) .unwrap_or("export"); let parent = export_path.parent().unwrap_or(std::path::Path::new(".")); @@ -1944,7 +2428,11 @@ pub fn run( use base64::Engine as _; let pset_b64 = base64::engine::general_purpose::STANDARD.encode(&pset_bytes); match std::fs::write(export_path, &pset_b64) { - Ok(()) => println!(" {} PSET (base64): {}", style("✓").green(), export_path.display()), + Ok(()) => println!( + " {} PSET (base64): {}", + style("✓").green(), + export_path.display() + ), Err(e) => println!(" {} PSET write failed: {e}", style("[error]").red()), } @@ -1952,7 +2440,11 @@ pub fn run( Ok(tx) => { let tx_hex = hex_bytes(&lwk_wollet::elements::encode::serialize(&tx)); match std::fs::write(&tx_path, &tx_hex) { - Ok(()) => println!(" {} TX (hex): {}", style("✓").green(), tx_path.display()), + Ok(()) => println!( + " {} TX (hex): {}", + style("✓").green(), + tx_path.display() + ), Err(e) => println!(" {} TX write failed: {e}", style("[error]").red()), } } @@ -1967,14 +2459,17 @@ pub fn run( action: action_name, compile_params: ctx.all_compile_params(), params: ctx.all_params(), - inputs: ctx.all_inputs().map(|i| RunOutputInput { - id: i.id.clone(), - txid: i.txid.clone(), - vout: i.vout, - amount_sat: i.amount_sat, - asset: i.asset.clone(), - issuance_entropy: i.issuance_entropy.clone(), - }).collect(), + inputs: ctx + .all_inputs() + .map(|i| RunOutputInput { + id: i.id.clone(), + txid: i.txid.clone(), + vout: i.vout, + amount_sat: i.amount_sat, + asset: i.asset.clone(), + issuance_entropy: i.issuance_entropy.clone(), + }) + .collect(), fee_rate_sat_per_vb: fee_rate, txid: None, }; @@ -1986,8 +2481,15 @@ pub fn run( let run_file = data_dir.join(format!("run_{safe_action}_{epoch}.json")); if let Ok(json) = serde_json::to_string_pretty(&run_output) { match std::fs::write(&run_file, json) { - Ok(()) => println!(" {} Run output: {}", style("✓").green(), run_file.display()), - Err(e) => println!(" {} Could not write run output: {e}", style("[warn]").yellow()), + Ok(()) => println!( + " {} Run output: {}", + style("✓").green(), + run_file.display() + ), + Err(e) => println!( + " {} Could not write run output: {e}", + style("[warn]").yellow() + ), } } return Ok(()); @@ -2013,7 +2515,10 @@ pub fn run( .collect(), }), Err(e) => { - println!(" {} Could not compute verified wallet delta for preview: {e}", style("[warn]").yellow()); + println!( + " {} Could not compute verified wallet delta for preview: {e}", + style("[warn]").yellow() + ); None } }, @@ -2027,7 +2532,9 @@ pub fn run( let confirmed = prompt::confirm_broadcast()?; if confirmed { - if let (Some(mut pset), Some(_w), Some(_wollet)) = (signed_pset, &loaded_wallet, &wollet_opt) { + if let (Some(mut pset), Some(_w), Some(_wollet)) = + (signed_pset, &loaded_wallet, &wollet_opt) + { // Save signed PSET (hex) before finalization for external inspection. let safe_action_name = action_name.replace(['/', '\\', ' '], "_"); std::fs::create_dir_all(data_dir).ok(); @@ -2052,7 +2559,9 @@ pub fn run( continue; // already finalized (Simplicity covenant) } // P2WPKH wallet input: partial_sigs has exactly one entry after signing. - let partial: Vec<_> = pset.inputs()[i].partial_sigs.iter() + let partial: Vec<_> = pset.inputs()[i] + .partial_sigs + .iter() .map(|(pk, sig)| (pk.to_bytes(), sig.clone())) .collect(); if partial.is_empty() { @@ -2087,7 +2596,9 @@ pub fn run( // Save finalized TX hex for external inspection / manual broadcast. let tx_file = data_dir.join(format!("tx_{safe_action_name}.hex")); match std::fs::write(&tx_file, &tx_hex) { - Ok(()) => println!(" {} TX saved: {}", style("·").dim(), tx_file.display()), + Ok(()) => { + println!(" {} TX saved: {}", style("·").dim(), tx_file.display()) + } Err(e) => println!(" {} Could not save TX: {e}", style("[warn]").yellow()), } println!( @@ -2105,11 +2616,18 @@ pub fn run( } for (i, out) in tx.output.iter().enumerate() { let val_desc = match &out.value { - lwk_wollet::elements::confidential::Value::Explicit(v) => format!("{v} sat explicit"), - lwk_wollet::elements::confidential::Value::Confidential(_) => "confidential".to_string(), + lwk_wollet::elements::confidential::Value::Explicit(v) => { + format!("{v} sat explicit") + } + lwk_wollet::elements::confidential::Value::Confidential(_) => { + "confidential".to_string() + } lwk_wollet::elements::confidential::Value::Null => "null".to_string(), }; - println!(" output #{i}: {val_desc} spk_len={}", out.script_pubkey.len()); + println!( + " output #{i}: {val_desc} spk_len={}", + out.script_pubkey.len() + ); } if tx_hex.len() <= 512 { println!(" {} TX hex: {}", style("·").dim(), tx_hex); @@ -2124,118 +2642,118 @@ pub fn run( let cfg = config::load(); match broadcast_finalized_tx(&cfg, &tx, &tx_hex, net_for_hash) { Ok(txid) => { - broadcast_txid = Some(txid.clone()); - println!( - " {} txid: {}", - style("Broadcast").green().bold(), - style(&txid).yellow() - ); - println!(" Run `sync` after confirmation to update wallet state."); + broadcast_txid = Some(txid.clone()); + println!( + " {} txid: {}", + style("Broadcast").green().bold(), + style(&txid).yellow() + ); + println!(" Run `sync` after confirmation to update wallet state."); - // --- Method-level on_post_broadcast hook --- - if let Some(hook) = &action.on_post_broadcast { - ctx.set_param("broadcast_txid", &txid); - run_hook_block(hook, &mut ctx, "[on_post_broadcast]", None); - } + // --- Method-level on_post_broadcast hook --- + if let Some(hook) = &action.on_post_broadcast { + ctx.set_param("broadcast_txid", &txid); + run_hook_block(hook, &mut ctx, "[on_post_broadcast]", None); + } - // --- Update and write state file --- - let mut new_state = contract_state.take() - .unwrap_or_else(|| ContractState::new(action_name)); - new_state.last_action = action_name.to_string(); - // Record which instance file this contract belongs to: the - // just-written output for constructors, else the loaded input. - let recorded_instance = if action.create_instance.is_some() { - Some(effective_instance_out.as_path()) - } else { - instance_in_path - }; - new_state.instance = - recorded_instance.map(|p| p.display().to_string()); - // Remove spent covenant inputs. - for inp in action.inputs.as_deref().unwrap_or_default() { - if inp.utxo_type_name().is_some() { - if let Some(r) = ctx.get_input(&inp.id) { - new_state.remove_spent(&r.txid, r.vout); - } + // --- Update and write state file --- + let mut new_state = contract_state + .take() + .unwrap_or_else(|| ContractState::new(action_name)); + new_state.last_action = action_name.to_string(); + // Record which instance file this contract belongs to: the + // just-written output for constructors, else the loaded input. + let recorded_instance = if action.create_instance.is_some() { + Some(effective_instance_out.as_path()) + } else { + instance_in_path + }; + new_state.instance = recorded_instance.map(|p| p.display().to_string()); + // Remove spent covenant inputs. + for inp in action.inputs.as_deref().unwrap_or_default() { + if inp.utxo_type_name().is_some() { + if let Some(r) = ctx.get_input(&inp.id) { + new_state.remove_spent(&r.txid, r.vout); } } - // Add new covenant outputs by matching script_pubkeys in the tx. - // First, drop any existing UTXOs of the types being produced — - // this action supersedes them. - for meta in &covenant_output_meta { - new_state.utxos.retain(|u| u.utxo_type != meta.utxo_type); - } - // Match each meta entry to the correct output vout. - // Multiple outputs can share the same script_pubkey (e.g. four - // prelock_script_auth outputs for four different NFTs), so we - // also match on asset and amount, and consume each position at - // most once to avoid duplicates. - let mut used_vouts: std::collections::HashSet = - std::collections::HashSet::new(); - for meta in &covenant_output_meta { - let found = tx.output.iter().enumerate().find(|(i, o)| { - if used_vouts.contains(i) { - return false; - } - if o.script_pubkey != meta.script_pubkey { - return false; - } - let asset_ok = matches!( - &o.asset, - lwk_wollet::elements::confidential::Asset::Explicit(a) - if *a == meta.asset - ); - let value_ok = matches!( - &o.value, - lwk_wollet::elements::confidential::Value::Explicit(v) - if *v == meta.amount_sat - ); - asset_ok && value_ok - }); - if let Some((vout, _)) = found { - used_vouts.insert(vout); - new_state.utxos.push(StateUtxo { - utxo_type: meta.utxo_type.clone(), - utxo_id: meta.output_id.clone(), - txid: txid.clone(), - vout: vout as u32, - amount_sat: meta.amount_sat, - asset: meta.asset.to_string(), - }); + } + // Add new covenant outputs by matching script_pubkeys in the tx. + // First, drop any existing UTXOs of the types being produced — + // this action supersedes them. + for meta in &covenant_output_meta { + new_state.utxos.retain(|u| u.utxo_type != meta.utxo_type); + } + // Match each meta entry to the correct output vout. + // Multiple outputs can share the same script_pubkey (e.g. four + // prelock_script_auth outputs for four different NFTs), so we + // also match on asset and amount, and consume each position at + // most once to avoid duplicates. + let mut used_vouts: std::collections::HashSet = + std::collections::HashSet::new(); + for meta in &covenant_output_meta { + let found = tx.output.iter().enumerate().find(|(i, o)| { + if used_vouts.contains(i) { + return false; + } + if o.script_pubkey != meta.script_pubkey { + return false; } + let asset_ok = matches!( + &o.asset, + lwk_wollet::elements::confidential::Asset::Explicit(a) + if *a == meta.asset + ); + let value_ok = matches!( + &o.value, + lwk_wollet::elements::confidential::Value::Explicit(v) + if *v == meta.amount_sat + ); + asset_ok && value_ok + }); + if let Some((vout, _)) = found { + used_vouts.insert(vout); + new_state.utxos.push(StateUtxo { + utxo_type: meta.utxo_type.clone(), + utxo_id: meta.output_id.clone(), + txid: txid.clone(), + vout: vout as u32, + amount_sat: meta.amount_sat, + asset: meta.asset.to_string(), + }); } - match new_state.write(&effective_state_out) { - Ok(()) => { - println!( - " {} State written: {}", + } + match new_state.write(&effective_state_out) { + Ok(()) => { + println!( + " {} State written: {}", + style("✓").green(), + effective_state_out.display() + ); + let hist_path = history_path(&history_seed); + let entry = HistoryEntry { + action: action_name.to_string(), + txid: txid.clone(), + utxos: new_state.utxos.clone(), + }; + match StateHistory::load(&hist_path) + .and_then(|mut h| h.append(entry, &hist_path)) + { + Ok(()) => println!( + " {} History appended: {}", style("✓").green(), - effective_state_out.display() - ); - let hist_path = history_path(&history_seed); - let entry = HistoryEntry { - action: action_name.to_string(), - txid: txid.clone(), - utxos: new_state.utxos.clone(), - }; - match StateHistory::load(&hist_path) - .and_then(|mut h| h.append(entry, &hist_path)) - { - Ok(()) => println!( - " {} History appended: {}", - style("✓").green(), - hist_path.display() - ), - Err(e) => println!( - " {} Could not write history file: {e}", - style("[warn]").yellow() - ), - } + hist_path.display() + ), + Err(e) => println!( + " {} Could not write history file: {e}", + style("[warn]").yellow() + ), } - Err(e) => println!( - " {} Could not write state file: {e}", - style("[warn]").yellow() - ), } + Err(e) => println!( + " {} Could not write state file: {e}", + style("[warn]").yellow() + ), + } } Err(msg) => { println!(" {} Broadcast failed: {msg}", style("[error]").red()); @@ -2256,14 +2774,17 @@ pub fn run( action: action_name, compile_params: ctx.all_compile_params(), params: ctx.all_params(), - inputs: ctx.all_inputs().map(|i| RunOutputInput { - id: i.id.clone(), - txid: i.txid.clone(), - vout: i.vout, - amount_sat: i.amount_sat, - asset: i.asset.clone(), - issuance_entropy: i.issuance_entropy.clone(), - }).collect(), + inputs: ctx + .all_inputs() + .map(|i| RunOutputInput { + id: i.id.clone(), + txid: i.txid.clone(), + vout: i.vout, + amount_sat: i.amount_sat, + asset: i.asset.clone(), + issuance_entropy: i.issuance_entropy.clone(), + }) + .collect(), fee_rate_sat_per_vb: fee_rate, txid: broadcast_txid, }; @@ -2277,9 +2798,15 @@ pub fn run( match serde_json::to_string_pretty(&run_output) { Ok(json) => match std::fs::write(&run_file, json) { Ok(()) => println!(" {} Run saved: {}", style("✓").green(), run_file.display()), - Err(e) => println!(" {} Could not write run file: {e}", style("[warn]").yellow()), + Err(e) => println!( + " {} Could not write run file: {e}", + style("[warn]").yellow() + ), }, - Err(e) => println!(" {} Could not serialize run output: {e}", style("[warn]").yellow()), + Err(e) => println!( + " {} Could not serialize run output: {e}", + style("[warn]").yellow() + ), } println!(); @@ -2299,15 +2826,15 @@ fn network_genesis_hash(network: ElementsNetwork) -> lwk_wollet::elements::Block use std::str::FromStr; match network { // Liquid mainnet genesis: 1466275836220db2944ca059a3a10ef6fd2ea684b0688d2c379296888a206003 - ElementsNetwork::Liquid => BlockHash::from_str( - "1466275836220db2944ca059a3a10ef6fd2ea684b0688d2c379296888a206003", - ) - .expect("hardcoded Liquid genesis hash is valid"), + ElementsNetwork::Liquid => { + BlockHash::from_str("1466275836220db2944ca059a3a10ef6fd2ea684b0688d2c379296888a206003") + .expect("hardcoded Liquid genesis hash is valid") + } // Liquid Testnet genesis: a771da8e52ee6ad581ed1e9a99825e5b3b7992225534eaa2ae23244fe26ab1c1 - ElementsNetwork::LiquidTestnet => BlockHash::from_str( - "a771da8e52ee6ad581ed1e9a99825e5b3b7992225534eaa2ae23244fe26ab1c1", - ) - .expect("hardcoded Liquid Testnet genesis hash is valid"), + ElementsNetwork::LiquidTestnet => { + BlockHash::from_str("a771da8e52ee6ad581ed1e9a99825e5b3b7992225534eaa2ae23244fe26ab1c1") + .expect("hardcoded Liquid Testnet genesis hash is valid") + } // Regtest has no fixed genesis hash; fall back to all-zero bytes (used in tests only) ElementsNetwork::ElementsRegtest { .. } => BlockHash::all_zeros(), } @@ -2331,7 +2858,9 @@ fn resolve_asset_id( /// screen even when resolution then fails. Silent for inputs with no declared label, so manifests /// that carry no UI hints log exactly as before. fn print_input_intent(input: &Input) { - let Some(label) = input.ui_label() else { return }; + let Some(label) = input.ui_label() else { + return; + }; // Separator is ':' rather than a dash — labels commonly contain an em-dash of their own, // and two dashes on one line read as a single run-on sentence. match input.ui_role() { @@ -2364,7 +2893,9 @@ fn select_input( // resolution source came up empty (--input override, instance.provided_inputs, // and the state file), so fail loudly with the fix rather than fabricating a UTXO. if !input.is_wallet_source() { - let utxo_type = input.utxo_type_name().unwrap_or_else(|| "[complex]".to_string()); + let utxo_type = input + .utxo_type_name() + .unwrap_or_else(|| "[complex]".to_string()); // Say what the input IS, not just its id — this error is the one place a covenant input's // absence surfaces, and "active_offer_in could not be resolved" alone gives the reader // nothing to act on. Broken across lines: the label is a sentence, so inlining it into the @@ -2428,7 +2959,10 @@ fn select_input( resolved.parse::().ok() }; - enum AmountConstraint { Exact(u64), AtLeast(u64) } + enum AmountConstraint { + Exact(u64), + AtLeast(u64), + } let amount_constraint: Option = input.amount_sat.as_ref().and_then(|v| { if let Some(n) = v.as_u64() { @@ -2463,18 +2997,32 @@ fn select_input( // Optional address pin: restrict selection to UTXOs at this exact scriptPubKey. // Resolves a reference (instance./params.) or a literal address string. - let from_spk: Option = input.from_address.as_ref().and_then(|s| { - let resolved = eval::eval_destination_str(s, ctx).unwrap_or_else(|| s.clone()); - match resolved.trim().parse::() { - Ok(a) => Some(a.script_pubkey()), - Err(e) => { - println!(" {} Input '{}' from_address '{}' is not a valid address: {e}", style("[warn]").yellow(), input.id, resolved); - None + let from_spk: Option = + input.from_address.as_ref().and_then(|s| { + let resolved = eval::eval_destination_str(s, ctx).unwrap_or_else(|| s.clone()); + match resolved.trim().parse::() { + Ok(a) => Some(a.script_pubkey()), + Err(e) => { + println!( + " {} Input '{}' from_address '{}' is not a valid address: {e}", + style("[warn]").yellow(), + input.id, + resolved + ); + None + } } - } - }); - let spk_matches_wt = |u: &lwk_wollet::WalletTxOut| from_spk.as_ref().map_or(true, |spk| &u.script_pubkey == spk); - let spk_matches_ext = |u: &lwk_wollet::ExternalUtxo| from_spk.as_ref().map_or(true, |spk| &u.txout.script_pubkey == spk); + }); + let spk_matches_wt = |u: &lwk_wollet::WalletTxOut| { + from_spk + .as_ref() + .map_or(true, |spk| &u.script_pubkey == spk) + }; + let spk_matches_ext = |u: &lwk_wollet::ExternalUtxo| { + from_spk + .as_ref() + .map_or(true, |spk| &u.txout.script_pubkey == spk) + }; // Check confidential UTXOs first. if let Some(asset_id) = required_asset { @@ -2516,7 +3064,11 @@ fn select_input( } } - let raw_label = input.asset.as_ref().and_then(|v| v.as_str()).unwrap_or("unknown"); + let raw_label = input + .asset + .as_ref() + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); let asset_label = if let Some(k) = raw_label .strip_prefix("instance.") .or_else(|| raw_label.strip_prefix("compile_params.")) @@ -2528,7 +3080,8 @@ fn select_input( raw_label }; // Build a per-asset balance summary for the error message. - let mut balance: std::collections::BTreeMap = std::collections::BTreeMap::new(); + let mut balance: std::collections::BTreeMap = + std::collections::BTreeMap::new(); for u in available { let e = balance.entry(u.unblinded.asset.to_string()).or_default(); e.0 += u.unblinded.value; @@ -2542,9 +3095,18 @@ fn select_input( let balance_lines: Vec = if balance.is_empty() { vec![" (no UTXOs — run `sync` first)".to_string()] } else { - balance.iter().map(|(asset, (sats, count))| { - format!(" {} sat ({} UTXO{}) asset: {}", sats, count, if *count == 1 { "" } else { "s" }, asset) - }).collect() + balance + .iter() + .map(|(asset, (sats, count))| { + format!( + " {} sat ({} UTXO{}) asset: {}", + sats, + count, + if *count == 1 { "" } else { "s" }, + asset + ) + }) + .collect() }; let balance_str = balance_lines.join("\n"); @@ -2600,7 +3162,10 @@ fn apply_utxo_compile_params( base: &std::collections::HashMap, base_hints: &std::collections::HashMap, ut: &crate::manifest::UtxoType, -) -> (std::collections::HashMap, std::collections::HashMap) { +) -> ( + std::collections::HashMap, + std::collections::HashMap, +) { let cp_map = match ut.script.as_ref() { Some(s) if !s.compile_params.is_empty() => &s.compile_params, _ => return (base.clone(), base_hints.clone()), @@ -2627,7 +3192,8 @@ fn amount_uses_fee_keyword(v: &serde_json::Value) -> bool { serde_json::Value::Object(m) => m.get("value").and_then(|x| x.as_str()).unwrap_or(""), _ => "", }; - s.split(|c: char| !c.is_alphanumeric() && c != '_').any(|tok| tok == "fee") + s.split(|c: char| !c.is_alphanumeric() && c != '_') + .any(|tok| tok == "fee") } /// Resolve a witness `source.key` reference to a concrete pubkey hex for signing. @@ -2675,21 +3241,29 @@ fn apply_site_compile_param_overrides( action: &crate::manifest::Action, base_hints: &std::collections::HashMap, ctx: &ExecutionContext, -) -> (std::collections::HashMap, std::collections::HashMap) { +) -> ( + std::collections::HashMap, + std::collections::HashMap, +) { let Some(map) = overrides.and_then(|v| v.as_object()) else { return (params, hints); }; for (simf_key, raw_val) in map { - let Some(raw) = raw_val.as_str() else { continue }; + let Some(raw) = raw_val.as_str() else { + continue; + }; let raw = raw.trim(); let value = eval::resolve_compile_param_value(raw, ctx); params.insert(simf_key.clone(), value); // Carry the declared type of whatever the value references so the // covenant compiler can type the argument. - let param_type = |m: &Option>, k: &str| { - m.as_ref().and_then(|defs| defs.get(k)).map(|p| p.type_.clone()) - }; + let param_type = + |m: &Option>, k: &str| { + m.as_ref() + .and_then(|defs| defs.get(k)) + .map(|p| p.type_.clone()) + }; let hint = if let Some(k) = raw.strip_prefix("params.") { param_type(&action.params, k) } else if let Some(k) = raw @@ -2712,7 +3286,6 @@ fn apply_site_compile_param_overrides( (params, hints) } - /// Broadcast a finalized transaction through the configured backend, returning the /// txid on success. Esplora uses a direct HTTP `POST /tx`; Electrum goes through the /// `Backend` client. Errors are returned as display strings so the caller can print @@ -2728,7 +3301,11 @@ fn broadcast_finalized_tx( BackendKind::Esplora => { let url = format!("{}/tx", cfg.esplora_url().trim_end_matches('/')); println!(" {} POST {}", style("→").cyan(), style(&url).underlined()); - println!(" {} body: {} chars of hex", style("→").cyan(), tx_hex.len()); + println!( + " {} body: {} chars of hex", + style("→").cyan(), + tx_hex.len() + ); match ureq::post(&url) .set("Content-Type", "text/plain") .send_string(tx_hex) @@ -2758,8 +3335,8 @@ fn broadcast_finalized_tx( style("→").cyan(), style(url).underlined() ); - let backend = Backend::connect(BackendKind::Electrum, url, network) - .map_err(|e| e.to_string())?; + let backend = + Backend::connect(BackendKind::Electrum, url, network).map_err(|e| e.to_string())?; backend .broadcast(tx) .map(|txid| txid.to_string()) @@ -2794,7 +3371,8 @@ fn run_hook_block( println!( " {} hook set '{}' — only expression values run in a hook; \ tapleaf/simf_fn/wallet are rejected by `validate`.", - style("[warn]").yellow(), target + style("[warn]").yellow(), + target ); continue; }; @@ -2806,16 +3384,18 @@ fn run_hook_block( .get_input_attr(id, "asset") .map(str::to_string) .or_else(|| ctx.get_input(id).map(|r| r.asset.clone())), - (Some(id), "reissuance_token") => { - ctx.get_input_attr(id, "reissuance_token").map(str::to_string) - } + (Some(id), "reissuance_token") => ctx + .get_input_attr(id, "reissuance_token") + .map(str::to_string), _ => eval::eval_expr_str(expr, ctx).ok(), }; let Some(v) = value else { println!( " {} hook set '{}' = '{}' — could not evaluate.", - style("[warn]").yellow(), target, expr + style("[warn]").yellow(), + target, + expr ); continue; }; @@ -2830,7 +3410,8 @@ fn run_hook_block( } else { println!( " {} hook set '{}' — unknown namespace (expected instance./params.).", - style("[warn]").yellow(), target + style("[warn]").yellow(), + target ); continue; } @@ -2877,7 +3458,10 @@ fn resolve_create_instance_leaves( for item in &leaf.payload { match item { serde_json::Value::String(s) => { - match eval::encode_leaf_bytes(&serde_json::json!({ "type": "bytes", "value": s }), s) { + match eval::encode_leaf_bytes( + &serde_json::json!({ "type": "bytes", "value": s }), + s, + ) { Ok(b) => bytes.extend_from_slice(&b), Err(_) => return None, } @@ -2897,7 +3481,8 @@ fn resolve_create_instance_leaves( None => return None, } } else { - fields.get(key) + fields + .get(key) .cloned() .or_else(|| ctx.get_compile_param(key).map(str::to_string)) .or_else(|| ctx.get_param(key).map(str::to_string)) @@ -2948,8 +3533,7 @@ fn eval_create_instance_fields( let value: Option = match field_value { ComputeSpec::Expr(expr) => { // $params.X / $instance.X → direct lookup; other → eval_expr_str - expr - .strip_prefix("$params.") + expr.strip_prefix("$params.") .or_else(|| expr.strip_prefix("$instance.")) .or_else(|| expr.strip_prefix("$compile_params.")) .and_then(|name| { @@ -2961,62 +3545,118 @@ fn eval_create_instance_fields( } ComputeSpec::Compute(compute) => { match compute { - crate::manifest::ParamCompute::Tapleaf { simf, params, depends_on, extra_leaves } => { + crate::manifest::ParamCompute::Tapleaf { + simf, + params, + depends_on, + extra_leaves, + } => { // Build simf_params: if params is empty use depends_on (or all ctx params) - let simf_params: Option> = if params.is_empty() { - let gate_names: Vec = match depends_on { - Some(deps) => deps.clone(), - None => fields.keys().cloned() - .chain(ctx.all_compile_params().keys().cloned()) - .chain(ctx.all_params().keys().cloned()) - .collect(), - }; - let mut resolved = std::collections::HashMap::new(); - let mut ok = true; - for cp_name in &gate_names { - let from_ctx = !computed_field_names.contains(cp_name.as_str()); - let v = fields.get(cp_name.as_str()) - .map(String::as_str) - .or_else(|| if from_ctx { ctx.get_compile_param(cp_name) } else { None }) - .or_else(|| if from_ctx { ctx.get_param(cp_name) } else { None }); - match v { - Some(val) => { resolved.insert(cp_name.clone(), val.to_string()); } - None => { ok = false; break; } + let simf_params: Option> = + if params.is_empty() { + let gate_names: Vec = match depends_on { + Some(deps) => deps.clone(), + None => fields + .keys() + .cloned() + .chain(ctx.all_compile_params().keys().cloned()) + .chain(ctx.all_params().keys().cloned()) + .collect(), + }; + let mut resolved = std::collections::HashMap::new(); + let mut ok = true; + for cp_name in &gate_names { + let from_ctx = + !computed_field_names.contains(cp_name.as_str()); + let v = fields + .get(cp_name.as_str()) + .map(String::as_str) + .or_else(|| { + if from_ctx { + ctx.get_compile_param(cp_name) + } else { + None + } + }) + .or_else(|| { + if from_ctx { + ctx.get_param(cp_name) + } else { + None + } + }); + match v { + Some(val) => { + resolved.insert(cp_name.clone(), val.to_string()); + } + None => { + ok = false; + break; + } + } } - } - if ok { Some(resolved) } else { None } - } else { - let mut resolved = std::collections::HashMap::new(); - let mut ok = true; - for (k, p) in params { - let v = p.value.as_str(); - let val = if v.parse::().is_ok() || v == "true" || v == "false" { - p.value.clone() + if ok { + Some(resolved) } else { - // If `v` names another field in this create_instance block, - // only look in `fields` (the in-progress map) — never in ctx. - // ctx may hold a stale value from the previously saved instance, - // and using it here would compute this field with outdated deps. - let from_ctx = !computed_field_names.contains(v); - match fields.get(v) - .map(String::as_str) - .or_else(|| if from_ctx { ctx.get_compile_param(v) } else { None }) - .or_else(|| if from_ctx { ctx.get_param(v) } else { None }) + None + } + } else { + let mut resolved = std::collections::HashMap::new(); + let mut ok = true; + for (k, p) in params { + let v = p.value.as_str(); + let val = if v.parse::().is_ok() + || v == "true" + || v == "false" { - Some(s) => s.to_string(), - None => { ok = false; break; } - } - }; - resolved.insert(k.clone(), val); - } - if ok { Some(resolved) } else { None } - }; + p.value.clone() + } else { + // If `v` names another field in this create_instance block, + // only look in `fields` (the in-progress map) — never in ctx. + // ctx may hold a stale value from the previously saved instance, + // and using it here would compute this field with outdated deps. + let from_ctx = !computed_field_names.contains(v); + match fields + .get(v) + .map(String::as_str) + .or_else(|| { + if from_ctx { + ctx.get_compile_param(v) + } else { + None + } + }) + .or_else(|| { + if from_ctx { + ctx.get_param(v) + } else { + None + } + }) { + Some(s) => s.to_string(), + None => { + ok = false; + break; + } + } + }; + resolved.insert(k.clone(), val); + } + if ok { + Some(resolved) + } else { + None + } + }; match simf_params { None => None, // deps not yet available — retry in a later pass Some(p) => { - let mut hints = p.keys() - .filter_map(|k| type_hints.get(k).map(|t| (k.clone(), t.clone()))) + let mut hints = p + .keys() + .filter_map(|k| { + type_hints.get(k).map(|t| (k.clone(), t.clone())) + }) .collect::>(); // For explicit param overrides, inherit type from the referenced name, // then apply any inline type overrides. @@ -3033,7 +3673,8 @@ fn eval_create_instance_fields( } } - let simf_path = manifest_file.parent() + let simf_path = manifest_file + .parent() .unwrap_or(std::path::Path::new(".")) .join(simf.as_str()); @@ -3042,15 +3683,30 @@ fn eval_create_instance_fields( // create_instance `fields` (then ctx). None => storage-less hash. let leaves_result: Option>> = match extra_leaves { None => Some(vec![]), - Some(specs) => resolve_create_instance_leaves(specs, &fields, ctx, &computed_field_names), + Some(specs) => resolve_create_instance_leaves( + specs, + &fields, + ctx, + &computed_field_names, + ), }; match leaves_result { None => None, // a leaf ref not yet computed — retry in a later pass Some(leaves) => { - match covenant::compute_covenant_script_hash_with_leaves(&simf_path, &p, &hints, &leaves, network, include_debug_symbols) { - Ok(hash_bytes) => { - Some(hash_bytes.iter().map(|b| format!("{b:02x}")).collect()) - } + match covenant::compute_covenant_script_hash_with_leaves( + &simf_path, + &p, + &hints, + &leaves, + network, + include_debug_symbols, + ) { + Ok(hash_bytes) => Some( + hash_bytes + .iter() + .map(|b| format!("{b:02x}")) + .collect(), + ), Err(e) => { println!( " {} create_instance script_hash '{}' failed: {e}", @@ -3176,24 +3832,28 @@ pub fn run_headless( let state = ContractState { instance: None, last_action: "prior".to_string(), - utxos: utxos.iter().map(|u| StateUtxo { - utxo_type: u.utxo_type.clone(), - utxo_id: String::new(), - txid: u.txid.clone(), - vout: u.vout, - amount_sat: u.amount_sat, - asset: u.asset.clone(), - }).collect(), + utxos: utxos + .iter() + .map(|u| StateUtxo { + utxo_type: u.utxo_type.clone(), + utxo_id: String::new(), + txid: u.txid.clone(), + vout: u.vout, + amount_sat: u.amount_sat, + asset: u.asset.clone(), + }) + .collect(), }; let state_path = data_dir.join(format!("_hl_state_{ns}.json")); - state.write(&state_path).context("cannot write headless state file")?; + state + .write(&state_path) + .context("cannot write headless state file")?; // Write temporary params override file. - let params_json = serde_json::to_string(extra_params) - .context("cannot serialize headless params")?; + let params_json = + serde_json::to_string(extra_params).context("cannot serialize headless params")?; let params_path = data_dir.join(format!("_hl_params_{ns}.json")); - std::fs::write(¶ms_path, ¶ms_json) - .context("cannot write headless params file")?; + std::fs::write(¶ms_path, ¶ms_json).context("cannot write headless params file")?; // lifecycle::run writes PSET base64 to export_path and tx hex to .tx.hex. let export_path = data_dir.join(format!("_hl_export_{ns}.pset")); @@ -3215,16 +3875,16 @@ pub fn run_headless( Some(network), Some(¶ms_path), loaded_instance.as_ref(), - instance_path, // instance_in_path - instance_path, // instance_out_path (read-only actions; unused) - Some(&state_path), // state_in_path - Some(&state_path), // state_out_path + instance_path, // instance_in_path + instance_path, // instance_out_path (read-only actions; unused) + Some(&state_path), // state_in_path + Some(&state_path), // state_out_path &std::collections::HashMap::new(), // provided_inputs (none in headless) wallet_path, data_dir, - false, // manual_inputs + false, // manual_inputs Some(&export_path), - false, // debug_jets + false, // debug_jets ); // Best-effort cleanup of temp files regardless of run_result. @@ -3251,8 +3911,8 @@ pub fn run_headless( #[cfg(test)] mod tests { use super::*; - use std::collections::BTreeMap; use crate::manifest::{ComputeSpec, InstanceCreate}; + use std::collections::BTreeMap; #[test] fn outpoint_override_parses_txid_vout() { @@ -3260,7 +3920,10 @@ mod tests { "fd6c7a7b01c6dee573081c8c69587eefd79df15a833a61e67a6607449418ac90:1", ) .unwrap(); - assert_eq!(ov.txid, "fd6c7a7b01c6dee573081c8c69587eefd79df15a833a61e67a6607449418ac90"); + assert_eq!( + ov.txid, + "fd6c7a7b01c6dee573081c8c69587eefd79df15a833a61e67a6607449418ac90" + ); assert_eq!(ov.vout, 1); assert!(ov.amount_sat.is_none() && ov.asset.is_none()); } @@ -3286,7 +3949,10 @@ mod tests { let mut ctx = ExecutionContext::new(); // Simulate template-fields loading from a previous instance file. - ctx.set_compile_param("BORROWER_PUB_KEY", "1d4c354f5f91613f50ba8f59361bc5fb0d0e01fbb90495b7fbfc744e8f5d2253"); + ctx.set_compile_param( + "BORROWER_PUB_KEY", + "1d4c354f5f91613f50ba8f59361bc5fb0d0e01fbb90495b7fbfc744e8f5d2253", + ); // Simulate the fixed action-params handler: both writes now happen. let fresh_key = "c21eda458165b99ce9309896df32ea7470ee6c03d26f54b49fbd56df2295bdb8"; @@ -3299,13 +3965,12 @@ mod tests { Some(fresh_key), "compile_params must be overwritten with the fresh wallet key", ); - assert_eq!( - ctx.get_param("BORROWER_PUB_KEY"), - Some(fresh_key), - ); + assert_eq!(ctx.get_param("BORROWER_PUB_KEY"), Some(fresh_key),); // Verify the full map that tapleaf code reads has the fresh value. assert_eq!( - ctx.all_compile_params().get("BORROWER_PUB_KEY").map(String::as_str), + ctx.all_compile_params() + .get("BORROWER_PUB_KEY") + .map(String::as_str), Some(fresh_key), ); } @@ -3324,10 +3989,11 @@ mod tests { let base_hints: HashMap = HashMap::new(); - let mut cp_map: std::collections::HashMap = std::collections::HashMap::new(); + let mut cp_map: std::collections::HashMap = + std::collections::HashMap::new(); cp_map.insert("ASSET_ID".to_string(), "LENDER_NFT_ASSET_ID".to_string()); // key reference - cp_map.insert("ASSET_AMOUNT".to_string(), "1".to_string()); // literal - cp_map.insert("WITH_ASSET_BURN".to_string(), "true".to_string()); // literal + cp_map.insert("ASSET_AMOUNT".to_string(), "1".to_string()); // literal + cp_map.insert("WITH_ASSET_BURN".to_string(), "true".to_string()); // literal let ut = UtxoType { description: "test".to_string(), @@ -3344,12 +4010,21 @@ mod tests { let (params, _hints) = apply_utxo_compile_params(&base, &base_hints, &ut); - assert_eq!(params.get("ASSET_ID").map(String::as_str), Some("deadbeef"), - "key reference should resolve to value from base"); - assert_eq!(params.get("ASSET_AMOUNT").map(String::as_str), Some("1"), - "literal '1' must pass through even though it is not a key in base"); - assert_eq!(params.get("WITH_ASSET_BURN").map(String::as_str), Some("true"), - "literal 'true' must pass through even though it is not a key in base"); + assert_eq!( + params.get("ASSET_ID").map(String::as_str), + Some("deadbeef"), + "key reference should resolve to value from base" + ); + assert_eq!( + params.get("ASSET_AMOUNT").map(String::as_str), + Some("1"), + "literal '1' must pass through even though it is not a key in base" + ); + assert_eq!( + params.get("WITH_ASSET_BURN").map(String::as_str), + Some("true"), + "literal 'true' must pass through even though it is not a key in base" + ); } /// Per-output `destination.compile_params` (and per-input `utxo_source.compile_params`) @@ -3366,7 +4041,8 @@ mod tests { "params": { "pubkey": { "type": "pubkey", "description": "recipient key" } } - })).expect("deserialize action"); + })) + .expect("deserialize action"); let mut ctx = ExecutionContext::new(); let key = "c21eda458165b99ce9309896df32ea7470ee6c03d26f54b49fbd56df2295bdb8"; @@ -3376,11 +4052,19 @@ mod tests { let overrides = serde_json::json!({ "PUB_KEY": "params.pubkey" }); let (params, hints) = apply_site_compile_param_overrides( - HashMap::new(), HashMap::new(), Some(&overrides), &action, &base_hints, &ctx, + HashMap::new(), + HashMap::new(), + Some(&overrides), + &action, + &base_hints, + &ctx, ); - assert_eq!(params.get("PUB_KEY").map(String::as_str), Some(key), - "PUB_KEY must resolve to the action param's runtime value"); + assert_eq!( + params.get("PUB_KEY").map(String::as_str), + Some(key), + "PUB_KEY must resolve to the action param's runtime value" + ); assert_eq!(hints.get("PUB_KEY").map(String::as_str), Some("pubkey"), "type hint must be carried from the referenced action param (PUB_KEY is not name-inferable)"); } @@ -3398,14 +4082,41 @@ mod tests { compile_params.insert("BORROWER_PUB_KEY".to_string(), "bb22".to_string()); // action param (the p2pk runtime-key case) - assert_eq!(resolve_witness_signing_key("params.pubkey", &action_params, &compile_params), "aa11"); + assert_eq!( + resolve_witness_signing_key("params.pubkey", &action_params, &compile_params), + "aa11" + ); // legacy compile-param forms (as used by the lending example) - assert_eq!(resolve_witness_signing_key("$params.BORROWER_PUB_KEY", &action_params, &compile_params), "bb22"); - assert_eq!(resolve_witness_signing_key("instance.BORROWER_PUB_KEY", &action_params, &compile_params), "bb22"); + assert_eq!( + resolve_witness_signing_key( + "$params.BORROWER_PUB_KEY", + &action_params, + &compile_params + ), + "bb22" + ); + assert_eq!( + resolve_witness_signing_key( + "instance.BORROWER_PUB_KEY", + &action_params, + &compile_params + ), + "bb22" + ); // Deprecated alias still accepted during the transition. - assert_eq!(resolve_witness_signing_key("compile_params.BORROWER_PUB_KEY", &action_params, &compile_params), "bb22"); + assert_eq!( + resolve_witness_signing_key( + "compile_params.BORROWER_PUB_KEY", + &action_params, + &compile_params + ), + "bb22" + ); // unknown / literal passes through verbatim - assert_eq!(resolve_witness_signing_key("cc33ddee", &action_params, &compile_params), "cc33ddee"); + assert_eq!( + resolve_witness_signing_key("cc33ddee", &action_params, &compile_params), + "cc33ddee" + ); } /// A literal value in a per-site override passes through unchanged (no reference match). @@ -3419,11 +4130,19 @@ mod tests { let overrides = serde_json::json!({ "COUNT": "7" }); let (params, _hints) = apply_site_compile_param_overrides( - HashMap::new(), HashMap::new(), Some(&overrides), &action, &HashMap::new(), &ctx, + HashMap::new(), + HashMap::new(), + Some(&overrides), + &action, + &HashMap::new(), + &ctx, ); - assert_eq!(params.get("COUNT").map(String::as_str), Some("7"), - "an unreferencing literal must pass through verbatim"); + assert_eq!( + params.get("COUNT").map(String::as_str), + Some("7"), + "an unreferencing literal must pass through verbatim" + ); } /// `eval_create_instance_fields` with a `"$params.KEY"` expression must prefer @@ -3479,7 +4198,10 @@ mod tests { let (_class, _class_def, action) = manifest .find_template_action("CreateOffer") .expect("CreateOffer action exists"); - let ci = action.create_instance.as_ref().expect("CreateOffer has create_instance"); + let ci = action + .create_instance + .as_ref() + .expect("CreateOffer has create_instance"); // Live offer 43ab4efe parameters (same as examples/lending_recon.rs). let collateral = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"; @@ -3500,7 +4222,10 @@ mod tests { ctx.set_param("PRINCIPAL_INTEREST_RATE", "10000"); ctx.set_param("LOAN_EXPIRATION_TIME", "2536857"); ctx.set_param("ZERO_HASH", &"00".repeat(32)); - ctx.set_param("FACTORY_ASSET_ID", "0101010101010101010101010101010101010101010101010101010101010101"); + ctx.set_param( + "FACTORY_ASSET_ID", + "0101010101010101010101010101010101010101010101010101010101010101", + ); // Protocol message-type tag constant (a param default in the manifest; here set directly // as a compile param since the test drives create_instance without Step 1). ctx.set_compile_param("LENDING_PROGRAM_ID", "f80c6162"); @@ -3517,28 +4242,56 @@ mod tests { } if let Some(params) = &action.params { for (pname, pdef) in params { - hints.entry(pname.clone()).or_insert_with(|| pdef.type_.clone()); + hints + .entry(pname.clone()) + .or_insert_with(|| pdef.type_.clone()); } } - let fields = eval_create_instance_fields( - ci, &ctx, &manifest_path, &hints, net, false, true, - ); + let fields = + eval_create_instance_fields(ci, &ctx, &manifest_path, &hints, net, false, true); // The 5 nested hashes must match the independently-verified recon values. - assert_eq!(fields.get("FINALIZED_LENDER_VAULT_COV_HASH").map(String::as_str), - Some("686766f422bca200851234cc787902d105ae91e7acc97977ff32b84263b286c6"), "F_lender"); - assert_eq!(fields.get("LENDER_VAULT_COV_HASH").map(String::as_str), - Some("54a0e779d4324f5f5ef45e0e615b34eb0091c4b88a08bfee3ce4fe0e760cf872"), "A_lender"); - assert_eq!(fields.get("FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH").map(String::as_str), - Some("9c2a221b8457112075bf80b46b32878e34a023e3f67653c54d041897926a49bb"), "F_proto"); - assert_eq!(fields.get("PROTOCOL_FEE_VAULT_COV_HASH").map(String::as_str), - Some("2a887b2cbd477c94f4b14c03d32216ccb0faeb087ab08fd3862e105ddcdf5e71"), "A_proto"); - assert_eq!(fields.get("PRINCIPAL_OUTPUT_SCRIPT_HASH").map(String::as_str), - Some("88c5f4e880bed03eb4e59f99f8d60534cd8c3dc9b405f2af72da2b8c358c7eb6"), "principal_out"); + assert_eq!( + fields + .get("FINALIZED_LENDER_VAULT_COV_HASH") + .map(String::as_str), + Some("686766f422bca200851234cc787902d105ae91e7acc97977ff32b84263b286c6"), + "F_lender" + ); + assert_eq!( + fields.get("LENDER_VAULT_COV_HASH").map(String::as_str), + Some("54a0e779d4324f5f5ef45e0e615b34eb0091c4b88a08bfee3ce4fe0e760cf872"), + "A_lender" + ); + assert_eq!( + fields + .get("FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH") + .map(String::as_str), + Some("9c2a221b8457112075bf80b46b32878e34a023e3f67653c54d041897926a49bb"), + "F_proto" + ); + assert_eq!( + fields + .get("PROTOCOL_FEE_VAULT_COV_HASH") + .map(String::as_str), + Some("2a887b2cbd477c94f4b14c03d32216ccb0faeb087ab08fd3862e105ddcdf5e71"), + "A_proto" + ); + assert_eq!( + fields + .get("PRINCIPAL_OUTPUT_SCRIPT_HASH") + .map(String::as_str), + Some("88c5f4e880bed03eb4e59f99f8d60534cd8c3dc9b405f2af72da2b8c358c7eb6"), + "principal_out" + ); // CURRENT_DEBT (task 10): principal + principal*bps/10000 = 1000 + 1000*10000/10000 = 2000. - assert_eq!(fields.get("CURRENT_DEBT").map(String::as_str), Some("2000"), "current_debt"); + assert_eq!( + fields.get("CURRENT_DEBT").map(String::as_str), + Some("2000"), + "current_debt" + ); // Drive the ACTUAL lending_collateral utxo_type end-to-end: fold the computed create_instance // fields into ctx, resolve the utxo_type's compile_params + computed storage leaves, and @@ -3549,56 +4302,90 @@ mod tests { let ut = manifest .utxo_type("lending_collateral") .expect("lending_collateral utxo_type exists"); - let base_params: std::collections::HashMap = - ctx.all_compile_params().iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let base_params: std::collections::HashMap = ctx + .all_compile_params() + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); let (lending_params, lending_hints) = apply_utxo_compile_params(&base_params, &hints, ut); - let leaves = ut.resolve_extra_leaf_payloads(&ctx).expect("resolve storage leaves"); + let leaves = ut + .resolve_extra_leaf_payloads(&ctx) + .expect("resolve storage leaves"); assert_eq!(leaves.len(), 2, "two storage slots"); assert_eq!(leaves[0], vec![0u8; 32], "slot0 = is_active zero"); let mut expect_slot1 = vec![0u8; 32]; expect_slot1[24..32].copy_from_slice(&2000u64.to_be_bytes()); - assert_eq!(leaves[1], expect_slot1, "slot1 = current_debt u64 BE, right-aligned in 32 bytes"); + assert_eq!( + leaves[1], expect_slot1, + "slot1 = current_debt u64 BE, right-aligned in 32 bytes" + ); let lending_simf = manifest_path.parent().unwrap().join("lending.simf"); let addr = crate::covenant::compute_covenant_address( - &lending_simf, &lending_params, &lending_hints, &leaves, net, true, - ).expect("compute lending covenant address"); + &lending_simf, + &lending_params, + &lending_hints, + &leaves, + net, + true, + ) + .expect("compute lending covenant address"); assert_eq!(format!("{:x}", addr.script_pubkey()), out5, "manifest utxo_type (create_instance chain + computed storage leaves) must reproduce live offer out[5]"); // Task 11: LENDING_COV_SCRIPT_HASH = sha256(out[5] spk, WITH storage), computed via a // tapleaf-over-lending.simf that folds the same storage leaves. - assert_eq!(fields.get("LENDING_COV_SCRIPT_HASH").map(String::as_str), + assert_eq!( + fields.get("LENDING_COV_SCRIPT_HASH").map(String::as_str), Some("2f40d78cbd15bd847a995719d707e623520dae2e223f66d77a76599f95685b19"), - "LENDING_COV_SCRIPT_HASH must equal sha256(out[5] scriptPubKey)"); + "LENDING_COV_SCRIPT_HASH must equal sha256(out[5] scriptPubKey)" + ); // Cross-check: it really is sha256 of the out[5] spk we just reproduced. { use lwk_wollet::elements::hashes::{sha256, Hash}; let h = sha256::Hash::hash(addr.script_pubkey().as_bytes()).to_byte_array(); let hh: String = h.iter().map(|b| format!("{b:02x}")).collect(); - assert_eq!(fields.get("LENDING_COV_SCRIPT_HASH").map(String::as_str), Some(hh.as_str())); + assert_eq!( + fields.get("LENDING_COV_SCRIPT_HASH").map(String::as_str), + Some(hh.as_str()) + ); } // The lender_nft_script_auth covenant (out[3]) compiles from that script hash. - let sa_ut = manifest.utxo_type("lender_nft_script_auth").expect("script_auth utxo_type"); - let (sa_params, sa_hints) = apply_utxo_compile_params(&{ - let m: std::collections::HashMap = - ctx.all_compile_params().iter().map(|(k, v)| (k.clone(), v.clone())).collect(); - m - }, &hints, sa_ut); - assert_eq!(sa_params.get("SCRIPT_HASH").map(String::as_str), + let sa_ut = manifest + .utxo_type("lender_nft_script_auth") + .expect("script_auth utxo_type"); + let (sa_params, sa_hints) = apply_utxo_compile_params( + &{ + let m: std::collections::HashMap = ctx + .all_compile_params() + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + m + }, + &hints, + sa_ut, + ); + assert_eq!( + sa_params.get("SCRIPT_HASH").map(String::as_str), Some("2f40d78cbd15bd847a995719d707e623520dae2e223f66d77a76599f95685b19"), - "script_auth SCRIPT_HASH resolves to the with-storage lending cov hash"); + "script_auth SCRIPT_HASH resolves to the with-storage lending cov hash" + ); let sa_simf = manifest_path.parent().unwrap().join("script_auth.simf"); crate::covenant::compute_covenant_address(&sa_simf, &sa_params, &sa_hints, &[], net, true) .expect("out[3] lender_nft_script_auth covenant address compiles"); // out[4]: the wired OP_RETURN output reproduces the on-chain 50-byte lending metadata // (same offer params as examples/opreturn_recon.rs → identical payload). - let op_out = action.outputs.as_ref().unwrap().iter() - .find(|o| o.id == "creation_op_return").expect("creation_op_return output"); + let op_out = action + .outputs + .as_ref() + .unwrap() + .iter() + .find(|o| o.id == "creation_op_return") + .expect("creation_op_return output"); let op_data = op_out.data.as_ref().expect("op_return has data"); - let op_bytes = eval::eval_op_return_data(op_data, &ctx, &hints) - .expect("eval op_return"); + let op_bytes = eval::eval_op_return_data(op_data, &ctx, &hints).expect("eval op_return"); let op_hex: String = op_bytes.iter().map(|b| format!("{b:02x}")).collect(); assert_eq!(op_bytes.len(), 50, "lending creation metadata is 50 bytes"); assert_eq!(op_hex, @@ -3607,47 +4394,102 @@ mod tests { // out[1]: factory covenant recreated resolves to the fixed (2,0) factory address even in // the offer context (ISSUING_UTXOS_COUNT/REISSUANCE_FLAGS come from lending_contract fields). - let fac_ut = manifest.utxo_type("issuance_factory").expect("issuance_factory utxo_type"); - let fac_base: std::collections::HashMap = - ctx.all_compile_params().iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let fac_ut = manifest + .utxo_type("issuance_factory") + .expect("issuance_factory utxo_type"); + let fac_base: std::collections::HashMap = ctx + .all_compile_params() + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); let (fac_params, fac_hints) = apply_utxo_compile_params(&fac_base, &hints, fac_ut); - let fac_simf = manifest_path.parent().unwrap().join("issuance_factory.simf"); - let fac_addr = crate::covenant::compute_covenant_address(&fac_simf, &fac_params, &fac_hints, &[], net, true) - .expect("factory covenant address"); - assert_eq!(format!("{:x}", fac_addr.script_pubkey()), + let fac_simf = manifest_path + .parent() + .unwrap() + .join("issuance_factory.simf"); + let fac_addr = crate::covenant::compute_covenant_address( + &fac_simf, + &fac_params, + &fac_hints, + &[], + net, + true, + ) + .expect("factory covenant address"); + assert_eq!( + format!("{:x}", fac_addr.script_pubkey()), "5120456881785cc7d561caaa059e02f1a2823066bd860423996bea3e92c621bb064b", - "out[1] factory covenant must be the fixed (2,0) address"); + "out[1] factory covenant must be the fixed (2,0) address" + ); // --- AcceptOffer (task 08) covenant outputs --- // ctx already holds every computed create_instance field as a compile param (set above). - let base_now: std::collections::HashMap = - ctx.all_compile_params().iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let base_now: std::collections::HashMap = ctx + .all_compile_params() + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); // out[0]: active lending covenant (storage slot0 = is_active=1) — the storage-transition // address from examples/lending_active_recon.rs. - let act_ut = manifest.utxo_type("lending_collateral_active").expect("active utxo_type"); + let act_ut = manifest + .utxo_type("lending_collateral_active") + .expect("active utxo_type"); let (act_params, act_hints) = apply_utxo_compile_params(&base_now, &hints, act_ut); - let act_leaves = act_ut.resolve_extra_leaf_payloads(&ctx).expect("active storage leaves"); - assert_eq!(act_leaves[0][31], 1, "active slot0 byte[31] = 1 (is_active)"); + let act_leaves = act_ut + .resolve_extra_leaf_payloads(&ctx) + .expect("active storage leaves"); + assert_eq!( + act_leaves[0][31], 1, + "active slot0 byte[31] = 1 (is_active)" + ); let act_addr = crate::covenant::compute_covenant_address( - &lending_simf, &act_params, &act_hints, &act_leaves, net, true).expect("active address"); - assert_eq!(format!("{:x}", act_addr.script_pubkey()), + &lending_simf, + &act_params, + &act_hints, + &act_leaves, + net, + true, + ) + .expect("active address"); + assert_eq!( + format!("{:x}", act_addr.script_pubkey()), "51202451da2d003a9fd5cffe1ed523cded17cda7a39604f02642d56d503bdef3eb77", - "AcceptOffer out[0] active lending covenant address (storage transition)"); - assert_ne!(act_addr.script_pubkey(), addr.script_pubkey(), "active differs from pending"); + "AcceptOffer out[0] active lending covenant address (storage transition)" + ); + assert_ne!( + act_addr.script_pubkey(), + addr.script_pubkey(), + "active differs from pending" + ); // out[1]: principal AssetAuth(borrower_nft, 1, false) — cross-check sha256(spk) == PRINCIPAL_OUTPUT_SCRIPT_HASH. - let pa_ut = manifest.utxo_type("principal_asset_auth").expect("principal_asset_auth utxo_type"); + let pa_ut = manifest + .utxo_type("principal_asset_auth") + .expect("principal_asset_auth utxo_type"); let (pa_params, pa_hints) = apply_utxo_compile_params(&base_now, &hints, pa_ut); let pa_simf = manifest_path.parent().unwrap().join("asset_auth.simf"); - let pa_addr = crate::covenant::compute_covenant_address(&pa_simf, &pa_params, &pa_hints, &[], net, true) - .expect("principal_asset_auth address"); + let pa_addr = crate::covenant::compute_covenant_address( + &pa_simf, + &pa_params, + &pa_hints, + &[], + net, + true, + ) + .expect("principal_asset_auth address"); { use lwk_wollet::elements::hashes::{sha256, Hash}; let pa_hash: String = sha256::Hash::hash(pa_addr.script_pubkey().as_bytes()) - .to_byte_array().iter().map(|b| format!("{b:02x}")).collect(); - assert_eq!(pa_hash, fields.get("PRINCIPAL_OUTPUT_SCRIPT_HASH").cloned().unwrap(), - "AcceptOffer out[1] AssetAuth spk hash must equal PRINCIPAL_OUTPUT_SCRIPT_HASH"); + .to_byte_array() + .iter() + .map(|b| format!("{b:02x}")) + .collect(); + assert_eq!( + pa_hash, + fields.get("PRINCIPAL_OUTPUT_SCRIPT_HASH").cloned().unwrap(), + "AcceptOffer out[1] AssetAuth spk hash must equal PRINCIPAL_OUTPUT_SCRIPT_HASH" + ); } } @@ -3678,23 +4520,44 @@ mod tests { let (_class, _class_def, create) = manifest .find_template_action("CreateOffer") .expect("CreateOffer action exists"); - let ci = create.create_instance.as_ref().expect("CreateOffer has create_instance"); + let ci = create + .create_instance + .as_ref() + .expect("CreateOffer has create_instance"); // Same live-offer 43ab4efe parameters as the out[5] reproduction test, so the vault // hashes computed here are the verified ones. let mut ctx = ExecutionContext::new(); - ctx.set_param("COLLATERAL_ASSET_ID", "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"); - ctx.set_param("PRINCIPAL_ASSET_ID", "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"); - ctx.set_param("PROTOCOL_FEE_KEEPER_ASSET_ID", "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5"); + ctx.set_param( + "COLLATERAL_ASSET_ID", + "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49", + ); + ctx.set_param( + "PRINCIPAL_ASSET_ID", + "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5", + ); + ctx.set_param( + "PROTOCOL_FEE_KEEPER_ASSET_ID", + "38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5", + ); ctx.set_param("COLLATERAL_AMOUNT", "21000"); ctx.set_param("PRINCIPAL_AMOUNT", "1000"); ctx.set_param("PRINCIPAL_INTEREST_RATE", "10000"); ctx.set_param("LOAN_EXPIRATION_TIME", "2536857"); ctx.set_param("ZERO_HASH", &"00".repeat(32)); - ctx.set_param("FACTORY_ASSET_ID", "0101010101010101010101010101010101010101010101010101010101010101"); + ctx.set_param( + "FACTORY_ASSET_ID", + "0101010101010101010101010101010101010101010101010101010101010101", + ); ctx.set_compile_param("LENDING_PROGRAM_ID", "f80c6162"); - ctx.set_compile_param("BORROWER_NFT_ASSET_ID", "78d61185c79f855fac51a87c191b00266f02d28752f50b3d9092ccf6b978181e"); - ctx.set_compile_param("LENDER_NFT_ASSET_ID", "213462821a5cdb96f435f5ea6597e8937359d6fd5a64b6ac8ef4262bc279fcfb"); + ctx.set_compile_param( + "BORROWER_NFT_ASSET_ID", + "78d61185c79f855fac51a87c191b00266f02d28752f50b3d9092ccf6b978181e", + ); + ctx.set_compile_param( + "LENDER_NFT_ASSET_ID", + "213462821a5cdb96f435f5ea6597e8937359d6fd5a64b6ac8ef4262bc279fcfb", + ); let mut hints: std::collections::HashMap = std::collections::HashMap::new(); if let Some((_, template_def, _)) = manifest.find_template_action("CreateOffer") { @@ -3704,57 +4567,92 @@ mod tests { } if let Some(params) = &create.params { for (pname, pdef) in params { - hints.entry(pname.clone()).or_insert_with(|| pdef.type_.clone()); + hints + .entry(pname.clone()) + .or_insert_with(|| pdef.type_.clone()); } } - let fields = eval_create_instance_fields(ci, &ctx, &manifest_path, &hints, net, false, true); + let fields = + eval_create_instance_fields(ci, &ctx, &manifest_path, &hints, net, false, true); for (k, v) in &fields { ctx.set_compile_param(k, v); } // ZERO_HASH must reach the instance: the vault utxo_types reference it by name to pick up // its declared `bytes32` type. Inlined as a literal it would infer as u64 (all digits). - assert_eq!(fields.get("ZERO_HASH").map(String::as_str), Some("00".repeat(32).as_str()), - "ZERO_HASH must be carried into the instance for the vault utxo_types to type it"); - assert_eq!(hints.get("ZERO_HASH").map(String::as_str), Some("bytes32"), - "ZERO_HASH must be declared bytes32, not left to value-based inference"); + assert_eq!( + fields.get("ZERO_HASH").map(String::as_str), + Some("00".repeat(32).as_str()), + "ZERO_HASH must be carried into the instance for the vault utxo_types to type it" + ); + assert_eq!( + hints.get("ZERO_HASH").map(String::as_str), + Some("bytes32"), + "ZERO_HASH must be declared bytes32, not left to value-based inference" + ); - let base: std::collections::HashMap = - ctx.all_compile_params().iter().map(|(k, v)| (k.clone(), v.clone())).collect(); - let vault_simf = manifest_path.parent().unwrap().join("asset_auth_vault.simf"); + let base: std::collections::HashMap = ctx + .all_compile_params() + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let vault_simf = manifest_path + .parent() + .unwrap() + .join("asset_auth_vault.simf"); // out[1] — the lender's finalized vault. - let lender_ut = manifest.utxo_type("lender_vault_finalized").expect("lender_vault_finalized utxo_type"); + let lender_ut = manifest + .utxo_type("lender_vault_finalized") + .expect("lender_vault_finalized utxo_type"); let (lp, lh) = apply_utxo_compile_params(&base, &hints, lender_ut); - assert_eq!(lh.get("FINALIZED_VAULT_COV_HASH").map(String::as_str), Some("bytes32"), - "the zero finalized-hash must carry a bytes32 hint into the compiler"); - let lender_addr = crate::covenant::compute_covenant_address(&vault_simf, &lp, &lh, &[], net, true) - .expect("lender_vault_finalized address compiles"); + assert_eq!( + lh.get("FINALIZED_VAULT_COV_HASH").map(String::as_str), + Some("bytes32"), + "the zero finalized-hash must carry a bytes32 hint into the compiler" + ); + let lender_addr = + crate::covenant::compute_covenant_address(&vault_simf, &lp, &lh, &[], net, true) + .expect("lender_vault_finalized address compiles"); let lender_hash: String = sha256::Hash::hash(lender_addr.script_pubkey().as_bytes()) - .to_byte_array().iter().map(|b| format!("{b:02x}")).collect(); + .to_byte_array() + .iter() + .map(|b| format!("{b:02x}")) + .collect(); assert_eq!(Some(lender_hash.as_str()), fields.get("FINALIZED_LENDER_VAULT_COV_HASH").map(String::as_str), "RepayLoan out[1] spk hash must equal the FINALIZED_LENDER_VAULT_COV_HASH the covenant enforces"); // out[2] — the protocol-fee finalized vault (keeper burn = false, unlike the lender's). - let proto_ut = manifest.utxo_type("protocol_fee_vault_finalized").expect("protocol_fee_vault_finalized utxo_type"); + let proto_ut = manifest + .utxo_type("protocol_fee_vault_finalized") + .expect("protocol_fee_vault_finalized utxo_type"); let (pp, ph) = apply_utxo_compile_params(&base, &hints, proto_ut); - let proto_addr = crate::covenant::compute_covenant_address(&vault_simf, &pp, &ph, &[], net, true) - .expect("protocol_fee_vault_finalized address compiles"); + let proto_addr = + crate::covenant::compute_covenant_address(&vault_simf, &pp, &ph, &[], net, true) + .expect("protocol_fee_vault_finalized address compiles"); let proto_hash: String = sha256::Hash::hash(proto_addr.script_pubkey().as_bytes()) - .to_byte_array().iter().map(|b| format!("{b:02x}")).collect(); + .to_byte_array() + .iter() + .map(|b| format!("{b:02x}")) + .collect(); assert_eq!(Some(proto_hash.as_str()), fields.get("FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH").map(String::as_str), "RepayLoan out[2] spk hash must equal the FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH the covenant enforces"); // The two vaults must be distinct covenants — a keeper/burn-flag mix-up would collapse them. - assert_ne!(lender_hash, proto_hash, "lender and protocol-fee vaults must be different covenants"); + assert_ne!( + lender_hash, proto_hash, + "lender and protocol-fee vaults must be different covenants" + ); // The repayment split must reproduce the covenant's own arithmetic, floor-division and all: // total_fee = 1000 * 10000/10000 = 1000 // protocol_fee = 1000 * 1000/10000 = 100 (10% of the interest) // lender share = CURRENT_DEBT(2000) - 100 = 1900 // Sum must be exactly the debt — the covenant's split_repayment_by_fees leaves no dust. - let (_, _, repay) = manifest.find_template_action("RepayLoan").expect("RepayLoan action exists"); + let (_, _, repay) = manifest + .find_template_action("RepayLoan") + .expect("RepayLoan action exists"); let rp = repay.params.as_ref().expect("RepayLoan has params"); let formula_of = |name: &str| { rp.get(name) @@ -3767,8 +4665,14 @@ mod tests { .expect("TOTAL_PROTOCOL_FEE evaluates"); let lender_amount = crate::eval::eval_expr_str(&formula_of("LENDER_VAULT_AMOUNT"), &ctx) .expect("LENDER_VAULT_AMOUNT evaluates"); - assert_eq!(protocol_fee, "100", "protocol fee = 10% of the 1000 interest"); - assert_eq!(lender_amount, "1900", "lender receives the debt less the protocol fee"); + assert_eq!( + protocol_fee, "100", + "protocol fee = 10% of the 1000 interest" + ); + assert_eq!( + lender_amount, "1900", + "lender receives the debt less the protocol fee" + ); let debt: u64 = fields.get("CURRENT_DEBT").unwrap().parse().unwrap(); assert_eq!( protocol_fee.parse::().unwrap() + lender_amount.parse::().unwrap(), @@ -3778,24 +4682,50 @@ mod tests { // The FullRepayment witness carries the debt, so its `instance.CURRENT_DEBT` ref must // resolve to a literal the SimplicityHL value parser can read. - let offer_in = repay.inputs.as_ref().unwrap().iter() - .find(|i| i.id == "active_offer_in").expect("active_offer_in input"); + let offer_in = repay + .inputs + .as_ref() + .unwrap() + .iter() + .find(|i| i.id == "active_offer_in") + .expect("active_offer_in input"); let wits = crate::eval::resolve_witness_refs( - offer_in.witnesses.as_ref().expect("active_offer_in has witnesses"), &ctx); - assert_eq!(wits["PATH"]["value"].as_str(), Some("Right(Left(Right(2000)))"), - "FullRepayment witness must resolve to PATH::Right(Left(Right(current_debt)))"); + offer_in + .witnesses + .as_ref() + .expect("active_offer_in has witnesses"), + &ctx, + ); + assert_eq!( + wits["PATH"]["value"].as_str(), + Some("Right(Left(Right(2000)))"), + "FullRepayment witness must resolve to PATH::Right(Left(Right(current_debt)))" + ); // The offer input spends the ACTIVE covenant AcceptOffer produced — same storage, so the // same address (this is what the covenant re-derives from the witness debt and compares). - let act_ut = manifest.utxo_type("lending_collateral_active").expect("active utxo_type"); + let act_ut = manifest + .utxo_type("lending_collateral_active") + .expect("active utxo_type"); let (ap, ah) = apply_utxo_compile_params(&base, &hints, act_ut); - let act_leaves = act_ut.resolve_extra_leaf_payloads(&ctx).expect("active storage leaves"); + let act_leaves = act_ut + .resolve_extra_leaf_payloads(&ctx) + .expect("active storage leaves"); let lending_simf = manifest_path.parent().unwrap().join("lending.simf"); - let act_addr = crate::covenant::compute_covenant_address(&lending_simf, &ap, &ah, &act_leaves, net, true) - .expect("active address"); - assert_eq!(format!("{:x}", act_addr.script_pubkey()), + let act_addr = crate::covenant::compute_covenant_address( + &lending_simf, + &ap, + &ah, + &act_leaves, + net, + true, + ) + .expect("active address"); + assert_eq!( + format!("{:x}", act_addr.script_pubkey()), "51202451da2d003a9fd5cffe1ed523cded17cda7a39604f02642d56d503bdef3eb77", - "RepayLoan in[1] must be the same active covenant AcceptOffer created"); + "RepayLoan in[1] must be the same active covenant AcceptOffer created" + ); } /// Task 10 — a computed u64 storage leaf encodes right-aligned, big-endian, in a @@ -3815,7 +4745,10 @@ mod tests { let item_left = serde_json::json!({ "value": "8", "type": "u8", "pad_to": 4, "align": "left" }); - assert_eq!(crate::eval::encode_leaf_value(&item_left, &ctx).unwrap(), vec![8u8, 0, 0, 0]); + assert_eq!( + crate::eval::encode_leaf_value(&item_left, &ctx).unwrap(), + vec![8u8, 0, 0, 0] + ); } /// The dex (Tessera) example's `MakeOffer.create_instance` must compute MAKER_SPK @@ -3840,7 +4773,10 @@ mod tests { let (_class, template_def, action) = manifest .find_template_action("MakeOffer") .expect("MakeOffer method exists"); - let ci = action.create_instance.as_ref().expect("MakeOffer has create_instance"); + let ci = action + .create_instance + .as_ref() + .expect("MakeOffer has create_instance"); // Track the manifest's own setting — debug symbols change the CMR, so a hardcoded // value here would verify a compilation mode the CLI never actually runs. @@ -3866,29 +4802,40 @@ mod tests { } if let Some(params) = &action.params { for (pname, pdef) in params { - hints.entry(pname.clone()).or_insert_with(|| pdef.type_.clone()); + hints + .entry(pname.clone()) + .or_insert_with(|| pdef.type_.clone()); } } - let fields = eval_create_instance_fields(ci, &ctx, &manifest_path, &hints, net, false, debug); + let fields = + eval_create_instance_fields(ci, &ctx, &manifest_path, &hints, net, false, debug); // MAKER_SPK is sha256 of the maker_payout covenant's scriptPubKey — verify against the // program itself rather than a copied constant. let payout_simf = manifest_path.parent().unwrap().join("maker_payout.simf"); let payout_params: std::collections::HashMap = - [("PUB_KEY".to_string(), maker_pub_key.to_string())].into_iter().collect(); + [("PUB_KEY".to_string(), maker_pub_key.to_string())] + .into_iter() + .collect(); let payout_hints: std::collections::HashMap = - [("PUB_KEY".to_string(), "pubkey".to_string())].into_iter().collect(); + [("PUB_KEY".to_string(), "pubkey".to_string())] + .into_iter() + .collect(); let payout_addr = crate::covenant::compute_covenant_address( - &payout_simf, &payout_params, &payout_hints, &[], net, debug, + &payout_simf, + &payout_params, + &payout_hints, + &[], + net, + debug, ) .expect("maker_payout covenant address compiles"); - let expect_spk_hash: String = - sha256::Hash::hash(payout_addr.script_pubkey().as_bytes()) - .to_byte_array() - .iter() - .map(|b| format!("{b:02x}")) - .collect(); + let expect_spk_hash: String = sha256::Hash::hash(payout_addr.script_pubkey().as_bytes()) + .to_byte_array() + .iter() + .map(|b| format!("{b:02x}")) + .collect(); assert_eq!( fields.get("MAKER_SPK").map(String::as_str), @@ -3906,9 +4853,14 @@ mod tests { for (k, v) in &fields { ctx.set_compile_param(k, v); } - let ut = manifest.utxo_type("tessera_offer").expect("tessera_offer utxo_type exists"); - let base: std::collections::HashMap = - ctx.all_compile_params().iter().map(|(k, v)| (k.clone(), v.clone())).collect(); + let ut = manifest + .utxo_type("tessera_offer") + .expect("tessera_offer utxo_type exists"); + let base: std::collections::HashMap = ctx + .all_compile_params() + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); let (offer_params, offer_hints) = apply_utxo_compile_params(&base, &hints, ut); assert_eq!( @@ -3918,7 +4870,12 @@ mod tests { ); let offer_simf = manifest_path.parent().unwrap().join("tessera.simf"); let offer_addr = crate::covenant::compute_covenant_address( - &offer_simf, &offer_params, &offer_hints, &[], net, debug, + &offer_simf, + &offer_params, + &offer_hints, + &[], + net, + debug, ) .expect("tessera_offer covenant address compiles from create_instance output"); @@ -3926,7 +4883,12 @@ mod tests { let mut bumped = offer_params.clone(); bumped.insert("AMOUNT_B".to_string(), "50001".to_string()); let bumped_addr = crate::covenant::compute_covenant_address( - &offer_simf, &bumped, &offer_hints, &[], net, debug, + &offer_simf, + &bumped, + &offer_hints, + &[], + net, + debug, ) .expect("bumped offer address compiles"); assert_ne!( @@ -3941,7 +4903,12 @@ mod tests { other_side.insert("OFFER_ASSET_ID".to_string(), lbtc_testnet.to_string()); other_side.insert("OFFER_AMOUNT".to_string(), "999".to_string()); let other_side_addr = crate::covenant::compute_covenant_address( - &offer_simf, &other_side, &offer_hints, &[], net, debug, + &offer_simf, + &other_side, + &offer_hints, + &[], + net, + debug, ) .expect("offer address compiles with a different asset A"); assert_eq!( diff --git a/txmanifest_lib/src/manifest.rs b/txmanifest_lib/src/manifest.rs index 59e4129..34ed4f6 100644 --- a/txmanifest_lib/src/manifest.rs +++ b/txmanifest_lib/src/manifest.rs @@ -223,7 +223,9 @@ pub enum ParamCompute { /// wallet-derived?" is a single check on `compute` before dispatching on /// `wallet` — and so adding a new wallet-derived value does not grow the /// top-level variant list. - Wallet { wallet: WalletValue }, + Wallet { + wallet: WalletValue, + }, /// Call a named function in a `.simf` file after inputs are resolved. /// The function is compiled with `compile_params` as param:: constants. /// Its runtime input is read from `input` (a dot-path into ctx, e.g. `"params.STATE_BYTES"`). @@ -472,7 +474,6 @@ pub struct Input { pub ui: Option, } - impl Input { /// This input's short human-readable label, if it declares one. /// @@ -1122,7 +1123,11 @@ mod tests { for n in ["K", "H", "A"] { // A wallet value is not reproducible from the manifest, so it must never // be mistaken for an expression the engine could evaluate itself. - assert_eq!(spec(n).as_expr(), None, "{n} must not read as an expression"); + assert_eq!( + spec(n).as_expr(), + None, + "{n} must not read as an expression" + ); } assert!(spec("E").as_wallet().is_none()); assert_eq!(spec("E").as_expr(), Some("1 + 1")); @@ -1237,7 +1242,10 @@ mod tests { // (compute spec, the substring the error must contain) for (compute, needle) in [ // An unknown key inside an otherwise well-formed spec. - (r#"{ "type": "tapleaf", "simf": "./a.simf", "bogus": 1 }"#, "bogus"), + ( + r#"{ "type": "tapleaf", "simf": "./a.simf", "bogus": 1 }"#, + "bogus", + ), // An unknown discriminator. (r#"{ "type": "no_such_kind" }"#, "no_such_kind"), // A required field missing from a known variant. @@ -1323,6 +1331,9 @@ mod tests { fn type_wins_when_both_keys_present() { // `type` is canonical; a stray `lang` must not override it. let fv = parse_field_value(r#"{ "type": "expr", "lang": "tapleaf", "expr": "1 + 1" }"#); - assert!(matches!(fv, ComputeSpec::Compute(ParamCompute::Expr { .. }))); + assert!(matches!( + fv, + ComputeSpec::Compute(ParamCompute::Expr { .. }) + )); } } diff --git a/txmanifest_lib/src/params.rs b/txmanifest_lib/src/params.rs index 01d194b..44d6a50 100644 --- a/txmanifest_lib/src/params.rs +++ b/txmanifest_lib/src/params.rs @@ -54,7 +54,11 @@ impl ParamOverrides { // Instance fields are authoritative (locked at deploy time from chain data). // Legacy flat instance_params loaded first; new instance.fields takes precedence. if let Some(inst) = instance { - values.extend(inst.instance_params.iter().map(|(k, v)| (k.clone(), v.clone()))); + values.extend( + inst.instance_params + .iter() + .map(|(k, v)| (k.clone(), v.clone())), + ); if let Some(idata) = &inst.instance { values.extend(idata.fields.iter().map(|(k, v)| (k.clone(), v.clone()))); } @@ -85,6 +89,10 @@ fn network_params_path(manifest_file: &Path, network: &str) -> PathBuf { fn load_params_file(path: &Path) -> Result> { let raw = std::fs::read_to_string(path) .with_context(|| format!("Cannot read params file: {}", path.display()))?; - serde_json::from_str(&raw) - .with_context(|| format!("Cannot parse params file (expected flat string→string JSON object): {}", path.display())) + serde_json::from_str(&raw).with_context(|| { + format!( + "Cannot parse params file (expected flat string→string JSON object): {}", + path.display() + ) + }) } diff --git a/txmanifest_lib/src/prepare.rs b/txmanifest_lib/src/prepare.rs index d3405de..82ec527 100644 --- a/txmanifest_lib/src/prepare.rs +++ b/txmanifest_lib/src/prepare.rs @@ -25,20 +25,27 @@ pub struct PrepareOpts<'a> { } pub fn prepare(opts: PrepareOpts<'_>) -> Result<()> { - let action = opts.manifest.actions.get(opts.action_name).with_context(|| { - let available: Vec<&str> = opts.manifest.actions.keys().map(String::as_str).collect(); - format!( - "Action '{}' not found. Available: {}", - opts.action_name, - available.join(", ") - ) - })?; + let action = opts + .manifest + .actions + .get(opts.action_name) + .with_context(|| { + let available: Vec<&str> = opts.manifest.actions.keys().map(String::as_str).collect(); + format!( + "Action '{}' not found. Available: {}", + opts.action_name, + available.join(", ") + ) + })?; // Step 1 — analyse what wallet inputs the action needs let needed = needed_wallet_inputs(action, opts.manifest)?; if needed.is_empty() { - println!(" No wallet inputs required for '{}' — nothing to prepare.", opts.action_name); + println!( + " No wallet inputs required for '{}' — nothing to prepare.", + opts.action_name + ); return Ok(()); } @@ -62,13 +69,12 @@ pub fn prepare(opts: PrepareOpts<'_>) -> Result<()> { ) .map_err(|e| anyhow::anyhow!("Cannot open wallet: {e}"))?; - let utxos = wollet.utxos() + let utxos = wollet + .utxos() .map_err(|e| anyhow::anyhow!("Cannot read UTXOs: {e}"))?; if utxos.is_empty() { - bail!( - "Wallet has no UTXOs. Fund the address shown by `info` then run `sync` first." - ); + bail!("Wallet has no UTXOs. Fund the address shown by `info` then run `sync` first."); } // Step 3 — for each required asset, count how many UTXOs we already have @@ -78,7 +84,10 @@ pub fn prepare(opts: PrepareOpts<'_>) -> Result<()> { use std::collections::BTreeMap; let mut needed_by_asset: BTreeMap> = BTreeMap::new(); for ni in &needed { - needed_by_asset.entry(ni.asset_label.clone()).or_default().push(ni); + needed_by_asset + .entry(ni.asset_label.clone()) + .or_default() + .push(ni); } let mut splits_required: Vec<(String, usize)> = Vec::new(); // (asset_label, extra needed) @@ -86,7 +95,8 @@ pub fn prepare(opts: PrepareOpts<'_>) -> Result<()> { for (asset_label, inputs) in &needed_by_asset { let required_count = inputs.len(); let asset_id = resolve_asset(asset_label, network)?; - let available_count = utxos.iter() + let available_count = utxos + .iter() .filter(|u| u.unblinded.asset == asset_id) .count(); @@ -122,15 +132,16 @@ pub fn prepare(opts: PrepareOpts<'_>) -> Result<()> { bail!( "Splitting non-L-BTC assets is not yet supported. \ Please manually send {} {} UTXO(s) to your wallet address.", - extra, asset_label + extra, + asset_label ); } - let receive_addr = wollet.address(None) + let receive_addr = wollet + .address(None) .map_err(|e| anyhow::anyhow!("Cannot derive address: {e}"))?; - let mut builder = wollet.tx_builder() - .fee_rate(Some(100.0)); // 0.1 sat/vb + let mut builder = wollet.tx_builder().fee_rate(Some(100.0)); // 0.1 sat/vb for _ in 0..*extra { builder = builder @@ -138,7 +149,8 @@ pub fn prepare(opts: PrepareOpts<'_>) -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to add recipient: {e}"))?; } - let mut pset = builder.finish() + let mut pset = builder + .finish() .map_err(|e| anyhow::anyhow!("Failed to build PSET: {e}"))?; // Preview — extract fee from the built PSET before signing @@ -146,10 +158,17 @@ pub fn prepare(opts: PrepareOpts<'_>) -> Result<()> { println!(); println!("{}", style(" Transaction preview:").bold()); - println!(" Outputs : {} × {} sats {} each", extra, opts.split_amount, asset_label); + println!( + " Outputs : {} × {} sats {} each", + extra, opts.split_amount, asset_label + ); println!(" Total : {} sats", (*extra as u64) * opts.split_amount); println!(" Fee : {} sats", fee); - println!(" To : {} (your wallet, index {})", receive_addr.address(), receive_addr.index()); + println!( + " To : {} (your wallet, index {})", + receive_addr.address(), + receive_addr.index() + ); println!(); let confirmed = Confirm::new() @@ -169,7 +188,8 @@ pub fn prepare(opts: PrepareOpts<'_>) -> Result<()> { .map_err(|e| anyhow::anyhow!("Failed to sign PSET: {e}"))?; // Finalize - let tx = wollet.finalize(&mut pset) + let tx = wollet + .finalize(&mut pset) .map_err(|e| anyhow::anyhow!("Failed to finalize PSET: {e}"))?; // Broadcast @@ -207,7 +227,10 @@ fn needed_wallet_inputs(action: &Action, manifest: &Manifest) -> Result Result u64 { - pset.outputs().iter().filter_map(|o| { - // Fee output has no script (empty scriptpubkey in the PSET output) - if o.script_pubkey.is_empty() { - o.amount - } else { - None - } - }).sum() + pset.outputs() + .iter() + .filter_map(|o| { + // Fee output has no script (empty scriptpubkey in the PSET output) + if o.script_pubkey.is_empty() { + o.amount + } else { + None + } + }) + .sum() } /// Resolve an asset label ("lbtc" or a hex asset ID) to an `AssetId`. -fn resolve_asset( - label: &str, - network: ElementsNetwork, -) -> Result { +fn resolve_asset(label: &str, network: ElementsNetwork) -> Result { use std::str::FromStr; match label { "lbtc" | "bitcoin" => Ok(network.policy_asset()), diff --git a/txmanifest_lib/src/preview.rs b/txmanifest_lib/src/preview.rs index 4cd67d9..f85a759 100644 --- a/txmanifest_lib/src/preview.rs +++ b/txmanifest_lib/src/preview.rs @@ -52,8 +52,14 @@ pub struct AssetMeta { pub fn lookup_asset(label: &str) -> AssetMeta { let l = label.trim().to_lowercase(); match l.as_str() { - "lbtc" | "bitcoin" | TLBTC_ASSET_ID => AssetMeta { symbol: "tL-BTC".into(), precision: 8 }, - TUSD_ASSET_ID => AssetMeta { symbol: "tUSD".into(), precision: 8 }, + "lbtc" | "bitcoin" | TLBTC_ASSET_ID => AssetMeta { + symbol: "tL-BTC".into(), + precision: 8, + }, + TUSD_ASSET_ID => AssetMeta { + symbol: "tUSD".into(), + precision: 8, + }, // Unknown asset: show a short id, count in base units. other => { let sym = if other.len() > 12 { @@ -61,7 +67,10 @@ pub fn lookup_asset(label: &str) -> AssetMeta { } else { other.to_string() }; - AssetMeta { symbol: sym, precision: 0 } + AssetMeta { + symbol: sym, + precision: 0, + } } } } @@ -132,11 +141,8 @@ fn resolve_token(token: &str, ctx: &ExecutionContext) -> Option { /// Returns `None` when the reference doesn't resolve (rather than the literal), /// so callers can flag authoring mistakes. fn resolve_ref(reference: &str, ctx: &ExecutionContext) -> Option { - let resolved = eval::eval_asset_label( - &serde_json::Value::String(reference.to_string()), - ctx, - ) - .ok()?; + let resolved = + eval::eval_asset_label(&serde_json::Value::String(reference.to_string()), ctx).ok()?; // `eval_asset_label` echoes unknown refs back as a literal; treat an // unchanged echo of a `namespace.key` reference as "unresolved". if resolved == reference && reference.contains('.') { @@ -164,7 +170,11 @@ pub struct WalletDelta { /// Render `- 0.000034 tL-BTC` style, returning `(is_credit, "amount symbol")`. fn format_signed(units: i64, meta: &AssetMeta) -> (bool, String) { let credit = units >= 0; - let text = format!("{} {}", format_amount(units.unsigned_abs(), meta.precision), meta.symbol); + let text = format!( + "{} {}", + format_amount(units.unsigned_abs(), meta.precision), + meta.symbol + ); (credit, text) } @@ -241,9 +251,17 @@ pub fn render_preview( for bucket in &buckets { println!(" {}", style(format!("({})", bucket.heading)).bold()); for leg in &bucket.legs { - let sign = if leg.credit { style("+").green() } else { style("−").red() }; + let sign = if leg.credit { + style("+").green() + } else { + style("−").red() + }; match leg.amount_text() { - Some(a) => println!(" {sign} {} {}", style(a).yellow(), style(&leg.label).dim()), + Some(a) => println!( + " {sign} {} {}", + style(a).yellow(), + style(&leg.label).dim() + ), None => println!(" {sign} {}", style(&leg.label).dim()), } } @@ -268,7 +286,11 @@ fn render_net_summary(buckets: &[Bucket], fee_sat: Option, wallet: Option<& } for (meta, units, exact) in &nets { let (credit, text) = format_signed(*units, meta); - let sign = if credit { style("+").green() } else { style("−").red() }; + let sign = if credit { + style("+").green() + } else { + style("−").red() + }; let approx = if *exact { "" } else { "≈ " }; println!(" {sign} {approx}{}", style(&text).yellow()); } @@ -314,7 +336,9 @@ fn wallet_nets(buckets: &[Bucket], wallet: Option<&WalletDelta>) -> Vec<(AssetMe let mut order: Vec = Vec::new(); let mut acc: std::collections::HashMap = Default::default(); for leg in &bucket.legs { - let Some(sym) = leg.asset.clone() else { continue }; + let Some(sym) = leg.asset.clone() else { + continue; + }; let e = acc.entry(sym.clone()).or_insert_with(|| { order.push(sym.clone()); (leg.precision, 0, true) @@ -330,12 +354,20 @@ fn wallet_nets(buckets: &[Bucket], wallet: Option<&WalletDelta>) -> Vec<(AssetMe .filter_map(|sym| { let (prec, units, exact) = acc[&sym]; // Drop assets that merely round-trip, but keep inexact ones visible. - (units != 0 || !exact).then(|| (AssetMeta { symbol: sym, precision: prec }, units, exact)) + (units != 0 || !exact).then(|| { + ( + AssetMeta { + symbol: sym, + precision: prec, + }, + units, + exact, + ) + }) }) .collect() } - /// The policy-asset symbol the network fee is denominated in. (Registry is out of /// scope; on testnet L-BTC is the fee/policy asset.) const POLICY_SYMBOL: &str = "tL-BTC"; @@ -383,7 +415,9 @@ fn build_net_effect(action: &Action, ctx: &ExecutionContext, fee_sat: Option = BTreeMap::new(); for output in outputs { if is_change(output) { - *change_leg_count.entry(output_asset_symbol(output, ctx)).or_default() += 1; + *change_leg_count + .entry(output_asset_symbol(output, ctx)) + .or_default() += 1; } } @@ -392,7 +426,10 @@ fn build_net_effect(action: &Action, ctx: &ExecutionContext, fee_sat: Option continue, Some(n) => push( heading, - Leg { credit: true, units: Some(n), asset: Some(sym), precision: prec, label }, + Leg { + credit: true, + units: Some(n), + asset: Some(sym), + precision: prec, + label, + }, ), // Fee unknown (e.g. dry run) — fall back to the placeholder label. None => push( @@ -492,9 +535,12 @@ fn build_net_effect(action: &Action, ctx: &ExecutionContext, fee_sat: Option Option<(u64, String, u .get_input(&input.id) .map(|r| r.amount_sat) .filter(|n| *n > 0) - .or_else(|| input.amount_sat.as_ref().and_then(|v| eval::eval_amount(v, ctx).ok()))?; + .or_else(|| { + input + .amount_sat + .as_ref() + .and_then(|v| eval::eval_amount(v, ctx).ok()) + })?; Some((amount, meta.symbol, meta.precision)) } @@ -686,9 +737,18 @@ mod tests { ("COLLATERAL_AMOUNT", "3400"), ("PRINCIPAL_ASSET_ID", PRINCIPAL_ID), ("COLLATERAL_ASSET_ID", COLLATERAL_ID), - ("FACTORY_ASSET_ID", "c6b7a5fdf1a01787af534dc9252d1c99908d929a16f8862b8925dcf53d089c6b"), - ("BORROWER_NFT_ASSET_ID", "1c424b82d66f37b9efea9f55bb5fab6dd2524742f8cc2741ed1be185a848c507"), - ("LENDER_NFT_ASSET_ID", "7eae7d537d90257c78220a1fd89915b39a2cb293111914e5a2e20d965acf361f"), + ( + "FACTORY_ASSET_ID", + "c6b7a5fdf1a01787af534dc9252d1c99908d929a16f8862b8925dcf53d089c6b", + ), + ( + "BORROWER_NFT_ASSET_ID", + "1c424b82d66f37b9efea9f55bb5fab6dd2524742f8cc2741ed1be185a848c507", + ), + ( + "LENDER_NFT_ASSET_ID", + "7eae7d537d90257c78220a1fd89915b39a2cb293111914e5a2e20d965acf361f", + ), ] { ctx.set_compile_param(k, v); } @@ -710,8 +770,9 @@ mod tests { fn ui_role_reads_detail_form_only() { use crate::manifest::UiSpec; let detail: UiSpec = serde_json::from_value( - serde_json::json!({ "label": "the live loan", "role": "covenant" }) - ).unwrap(); + serde_json::json!({ "label": "the live loan", "role": "covenant" }), + ) + .unwrap(); assert_eq!(detail.label(), Some("the live loan")); assert_eq!(detail.role(), Some("covenant")); @@ -736,8 +797,14 @@ mod tests { #[test] fn format_signed_directions_and_precision() { let lbtc = lookup_asset("lbtc"); - assert_eq!(format_signed(-3626, &lbtc), (false, "0.00003626 tL-BTC".to_string())); - assert_eq!(format_signed(100_000_000, &lbtc), (true, "1 tL-BTC".to_string())); + assert_eq!( + format_signed(-3626, &lbtc), + (false, "0.00003626 tL-BTC".to_string()) + ); + assert_eq!( + format_signed(100_000_000, &lbtc), + (true, "1 tL-BTC".to_string()) + ); let nft = lookup_asset("1c424b82d66f37b9efea9f55bb5fab6dd2524742f8cc2741ed1be185a848c507"); let (credit, text) = format_signed(1, &nft); assert!(credit); @@ -747,7 +814,10 @@ mod tests { #[test] fn unresolved_reference_stays_literal() { let ctx = ExecutionContext::new(); - assert_eq!(interpolate("x {instance.NOPE} y", &ctx), "x {instance.NOPE} y"); + assert_eq!( + interpolate("x {instance.NOPE} y", &ctx), + "x {instance.NOPE} y" + ); } #[test] @@ -761,9 +831,15 @@ mod tests { assert!(headings.contains(&"covenant: lending_collateral")); // The collateral output lands in the lending covenant as a debit-free credit. - let cov = buckets.iter().find(|b| b.heading == "covenant: lending_collateral").unwrap(); + let cov = buckets + .iter() + .find(|b| b.heading == "covenant: lending_collateral") + .unwrap(); let collateral_leg = cov.legs.iter().find(|l| l.credit).unwrap(); - assert_eq!(collateral_leg.amount_text().as_deref(), Some("0.000034 tL-BTC")); + assert_eq!( + collateral_leg.amount_text().as_deref(), + Some("0.000034 tL-BTC") + ); } #[test] @@ -803,8 +879,12 @@ mod tests { let (manifest, mut ctx) = create_offer_ctx(); for (id, asset) in [("collateral_in", COLLATERAL_ID), ("fee_input", "lbtc")] { ctx.set_input(ResolvedInput { - id: id.into(), txid: "00".repeat(32), vout: 0, - amount_sat: 49_470, asset: asset.into(), issuance_entropy: None, + id: id.into(), + txid: "00".repeat(32), + vout: 0, + amount_sat: 49_470, + asset: asset.into(), + issuance_entropy: None, }); } let action = create_offer(&manifest); @@ -824,13 +904,15 @@ mod tests { } } // Sanity: the factory-asset debit and credit are adjacent. - let factory_sym = lookup_asset( - "c6b7a5fdf1a01787af534dc9252d1c99908d929a16f8862b8925dcf53d089c6b", - ) - .symbol; - let idxs: Vec = wallet.legs.iter().enumerate() + let factory_sym = + lookup_asset("c6b7a5fdf1a01787af534dc9252d1c99908d929a16f8862b8925dcf53d089c6b").symbol; + let idxs: Vec = wallet + .legs + .iter() + .enumerate() .filter(|(_, l)| l.asset.as_deref() == Some(factory_sym.as_str())) - .map(|(i, _)| i).collect(); + .map(|(i, _)| i) + .collect(); assert_eq!(idxs.len(), 2); assert_eq!(idxs[1], idxs[0] + 1); } @@ -847,29 +929,53 @@ mod tests { for (k, v) in [ ("PRINCIPAL_AMOUNT", "1000"), ("PRINCIPAL_ASSET_ID", PRINCIPAL_ID), - ("BORROWER_NFT_ASSET_ID", "a2f1d6000000000000000000000000000000000000000000000000000000001059"), + ( + "BORROWER_NFT_ASSET_ID", + "a2f1d6000000000000000000000000000000000000000000000000000000001059", + ), ] { ctx.set_compile_param(k, v); } for (id, vout, amount, asset) in [ ("principal_asset_auth_in", 0u32, 1000u64, PRINCIPAL_ID), - ("borrower_nft_in", 1, 1, "a2f1d6000000000000000000000000000000000000000000000000000000001059"), + ( + "borrower_nft_in", + 1, + 1, + "a2f1d6000000000000000000000000000000000000000000000000000000001059", + ), ("fee_input", 2, 92_509, "lbtc"), ] { ctx.set_input(ResolvedInput { - id: id.into(), txid: "00".repeat(32), vout, - amount_sat: amount, asset: asset.into(), issuance_entropy: None, + id: id.into(), + txid: "00".repeat(32), + vout, + amount_sat: amount, + asset: asset.into(), + issuance_entropy: None, }); } - let action = manifest.contract_templates.as_ref().unwrap().get("lending_contract").unwrap() - .actions.get("ClaimPrincipal").unwrap(); + let action = manifest + .contract_templates + .as_ref() + .unwrap() + .get("lending_contract") + .unwrap() + .actions + .get("ClaimPrincipal") + .unwrap(); let buckets = build_net_effect(action, &ctx, Some(195)); let nets = wallet_nets(&buckets, None); - let rendered: Vec<(String, i64)> = - nets.iter().map(|(m, u, _)| (m.symbol.clone(), *u)).collect(); + let rendered: Vec<(String, i64)> = nets + .iter() + .map(|(m, u, _)| (m.symbol.clone(), *u)) + .collect(); // The NFT round-trip is gone; L-BTC is exactly −fee; principal is +1000. - assert_eq!(rendered, vec![("tL-BTC".to_string(), -195), ("tUSD".to_string(), 1000)]); + assert_eq!( + rendered, + vec![("tL-BTC".to_string(), -195), ("tUSD".to_string(), 1000)] + ); assert!(nets.iter().all(|(_, _, exact)| *exact)); } @@ -913,6 +1019,9 @@ mod tests { let buckets = build_net_effect(action, &ctx, None); // fee unknown let wallet = buckets.iter().find(|b| b.heading == "your wallet").unwrap(); // With no fee we cannot be exact, so change stays a labelled placeholder. - assert!(wallet.legs.iter().any(|l| l.units.is_none() && l.label.contains("if any"))); + assert!(wallet + .legs + .iter() + .any(|l| l.units.is_none() && l.label.contains("if any"))); } } diff --git a/txmanifest_lib/src/prompt.rs b/txmanifest_lib/src/prompt.rs index f97404a..99dfb9c 100644 --- a/txmanifest_lib/src/prompt.rs +++ b/txmanifest_lib/src/prompt.rs @@ -5,8 +5,8 @@ use anyhow::Result; use console::style; use dialoguer::{Confirm, Input}; -use crate::manifest; use crate::context::ResolvedInput; +use crate::manifest; // --------------------------------------------------------------------------- // Generic param prompt @@ -47,16 +47,19 @@ pub fn prompt_param( .default(def_bool) .interact() .map_err(|e| anyhow::anyhow!("prompt error for '{name}': {e}"))?; - if confirmed { "true".to_string() } else { "false".to_string() } + if confirmed { + "true".to_string() + } else { + "false".to_string() + } } - "u8" => prompt_integer::(name, type_, default)?, + "u8" => prompt_integer::(name, type_, default)?, "u16" => prompt_integer::(name, type_, default)?, "u32" => prompt_integer::(name, type_, default)?, "u64" => prompt_integer::(name, type_, default)?, _ => { let hint = type_hint(type_); - let mut input = Input::::new() - .with_prompt(format!(" {name}{hint}")); + let mut input = Input::::new().with_prompt(format!(" {name}{hint}")); if let Some(dv) = default { input = input.default(dv.to_string()).show_default(false); } @@ -88,8 +91,7 @@ where T::Err: std::fmt::Display, { loop { - let mut input = Input::::new() - .with_prompt(format!(" {name} [{}]", type_)); + let mut input = Input::::new().with_prompt(format!(" {name} [{}]", type_)); if let Some(dv) = default { input = input.default(dv.to_string()).show_default(false); } @@ -180,7 +182,9 @@ pub fn prompt_input_selection(input: &manifest::Input) -> Result // Protocol/covenant UTXOs are never fabricated: a stub would silently build a // transaction against a UTXO that does not exist. Callers resolve these from // --input / instance.provided_inputs / the state file, and error otherwise. - let utxo_type = input.utxo_type_name().unwrap_or_else(|| "[complex]".to_string()); + let utxo_type = input + .utxo_type_name() + .unwrap_or_else(|| "[complex]".to_string()); anyhow::bail!( "Input '{}' (utxo_type '{}') must be resolved from --input, \ instance.provided_inputs, or the state file — it cannot be prompted for.", diff --git a/txmanifest_lib/src/pset_builder.rs b/txmanifest_lib/src/pset_builder.rs index 4cb90d2..980c023 100644 --- a/txmanifest_lib/src/pset_builder.rs +++ b/txmanifest_lib/src/pset_builder.rs @@ -8,8 +8,8 @@ use lwk_wollet::{ hashes::{sha256, Hash as _}, pset::{Input, Output, PartiallySignedTransaction}, secp256k1_zkp::{RangeProof, SurjectionProof, Tweak}, - AssetId, ContractHash, OutPoint, Script, Sequence, Txid, TxOut, TxOutWitness, - BlindAssetProofs, BlindValueProofs, TxOutSecrets, + AssetId, BlindAssetProofs, BlindValueProofs, ContractHash, OutPoint, Script, Sequence, + TxOut, TxOutSecrets, TxOutWitness, Txid, }, ElementsNetwork, WalletTxOut, Wollet, EC, }; @@ -103,7 +103,11 @@ pub struct BuildPsetResult { // Public entry point // --------------------------------------------------------------------------- -pub fn build_pset(wollet: &Wollet, network: ElementsNetwork, req: &BuildPsetRequest) -> Result { +pub fn build_pset( + wollet: &Wollet, + network: ElementsNetwork, + req: &BuildPsetRequest, +) -> Result { let secp = EC.clone(); let mut rng = thread_rng(); @@ -116,8 +120,16 @@ pub fn build_pset(wollet: &Wollet, network: ElementsNetwork, req: &BuildPsetRequ let wallet_blinding_pk_btc = btc_pubkey(wallet_blinding_pk); // First pass: temp fee=1 to estimate weight. - let (temp_pset, temp_sec, _) = - build_inner(wollet, &secp, &mut rng, req, 1, wallet_blinding_pk_btc, network, false)?; + let (temp_pset, temp_sec, _) = build_inner( + wollet, + &secp, + &mut rng, + req, + 1, + wallet_blinding_pk_btc, + network, + false, + )?; let fee = { let mut tmp = temp_pset.clone(); let mut tmp_rng = thread_rng(); @@ -135,8 +147,16 @@ pub fn build_pset(wollet: &Wollet, network: ElementsNetwork, req: &BuildPsetRequ }; // Second pass: real fee. - let (mut pset, inp_txout_sec, issuances) = - build_inner(wollet, &secp, &mut rng, req, fee, wallet_blinding_pk_btc, network, false)?; + let (mut pset, inp_txout_sec, issuances) = build_inner( + wollet, + &secp, + &mut rng, + req, + fee, + wallet_blinding_pk_btc, + network, + false, + )?; wollet .add_details(&mut pset) @@ -168,10 +188,13 @@ fn pset_has_confidential_output(pset: &PartiallySignedTransaction) -> bool { fn estimated_input_witness_weight(req: &BuildPsetRequest) -> usize { const WALLET_INPUT_WU: usize = 108; const COVENANT_INPUT_WU: usize = 800; - req.inputs.iter().map(|i| match i { - PsetInput::Wallet { .. } => WALLET_INPUT_WU, - PsetInput::Covenant { .. } => COVENANT_INPUT_WU, - }).sum() + req.inputs + .iter() + .map(|i| match i { + PsetInput::Wallet { .. } => WALLET_INPUT_WU, + PsetInput::Covenant { .. } => COVENANT_INPUT_WU, + }) + .sum() } /// Estimate the network fee (sats) for `req`, from the resulting transaction's @@ -182,7 +205,11 @@ fn estimated_input_witness_weight(req: &BuildPsetRequest) -> usize { /// Note: like the builder's own estimate, this counts a fixed witness allowance /// for wallet inputs but not the (large, variable) Simplicity witness of covenant /// inputs — so covenant spends are under-counted, same as elsewhere in the tool. -pub fn estimate_fee(wollet: &Wollet, network: ElementsNetwork, req: &BuildPsetRequest) -> Result { +pub fn estimate_fee( + wollet: &Wollet, + network: ElementsNetwork, + req: &BuildPsetRequest, +) -> Result { let secp = EC.clone(); let mut rng = thread_rng(); @@ -194,8 +221,16 @@ pub fn estimate_fee(wollet: &Wollet, network: ElementsNetwork, req: &BuildPsetRe .context("Wallet address has no blinding key — not a CT descriptor")?; let wallet_blinding_pk_btc = btc_pubkey(wallet_blinding_pk); - let (draft_pset, draft_sec, _) = - build_inner(wollet, &secp, &mut rng, req, 0, wallet_blinding_pk_btc, network, true)?; + let (draft_pset, draft_sec, _) = build_inner( + wollet, + &secp, + &mut rng, + req, + 0, + wallet_blinding_pk_btc, + network, + true, + )?; let mut tmp = draft_pset; if pset_has_confidential_output(&tmp) { tmp.blind_last(&mut rng, &secp, &draft_sec) @@ -226,7 +261,11 @@ fn build_inner( // Estimation pass: don't enforce balance or add change — the fee absorbs any // surplus (possibly 0). Used only to measure the resulting tx's vsize. draft: bool, -) -> Result<(PartiallySignedTransaction, HashMap, Vec)> { +) -> Result<( + PartiallySignedTransaction, + HashMap, + Vec, +)> { let mut pset = PartiallySignedTransaction::new_v2(); let mut inp_txout_sec: HashMap = HashMap::new(); let mut issuances: Vec = Vec::new(); @@ -237,7 +276,12 @@ fn build_inner( // Add inputs for pset_input in &req.inputs { match pset_input { - PsetInput::Wallet { input_id, utxo, issuance, sequence } => { + PsetInput::Wallet { + input_id, + utxo, + issuance, + sequence, + } => { let idx = add_wallet_input(&mut pset, &mut inp_txout_sec, wollet, secp, rng, utxo)?; apply_sequence(&mut pset, idx, *sequence); if let Some(iso) = issuance { @@ -254,16 +298,37 @@ fn build_inner( } IssuanceKind::Reissue { entropy, .. } => Some(*entropy), }; - issuances.push(IssuanceResult { input_id: input_id.clone(), asset_id, token_id, entropy }); + issuances.push(IssuanceResult { + input_id: input_id.clone(), + asset_id, + token_id, + entropy, + }); } if utxo.unblinded.asset == req.policy_asset { total_lbtc_in += utxo.unblinded.value; } else { - *wallet_asset_in.entry(utxo.unblinded.asset).or_default() += utxo.unblinded.value; + *wallet_asset_in.entry(utxo.unblinded.asset).or_default() += + utxo.unblinded.value; } } - PsetInput::Covenant { input_id, outpoint, script_pubkey, asset, amount, issuance, sequence } => { - let idx = add_covenant_input(&mut pset, &mut inp_txout_sec, *outpoint, script_pubkey.clone(), *asset, *amount)?; + PsetInput::Covenant { + input_id, + outpoint, + script_pubkey, + asset, + amount, + issuance, + sequence, + } => { + let idx = add_covenant_input( + &mut pset, + &mut inp_txout_sec, + *outpoint, + script_pubkey.clone(), + *asset, + *amount, + )?; apply_sequence(&mut pset, idx, *sequence); if let Some(iso) = issuance { // A covenant input may carry either a NEW issuance (e.g. an issuance-factory @@ -283,7 +348,12 @@ fn build_inner( } }; let (asset_id, token_id) = pset.inputs()[idx].issuance_ids(); - issuances.push(IssuanceResult { input_id: input_id.clone(), asset_id, token_id, entropy }); + issuances.push(IssuanceResult { + input_id: input_id.clone(), + asset_id, + token_id, + entropy, + }); } if *asset == req.policy_asset { total_lbtc_in += amount; @@ -293,7 +363,9 @@ fn build_inner( } // L-BTC accounting - let total_lbtc_out: u64 = req.outputs.iter() + let total_lbtc_out: u64 = req + .outputs + .iter() .filter(|o| o.asset == req.policy_asset) .map(|o| o.amount) .sum(); @@ -308,7 +380,10 @@ fn build_inner( if total_lbtc_in < lbtc_needed { anyhow::bail!( "Insufficient L-BTC: have {} sat, need {} sat (outputs {} + fee {})", - total_lbtc_in, lbtc_needed, total_lbtc_out, fee + total_lbtc_in, + lbtc_needed, + total_lbtc_out, + fee ); } (total_lbtc_in - lbtc_needed, fee) @@ -324,47 +399,78 @@ fn build_inner( // blinder_index must reference an input whose secrets are in inp_txout_sec (i.e. a wallet // input). Inputs may arrive in any order so we pick the first wallet input by key. - let blinder_idx = inp_txout_sec - .keys() - .copied() - .min() - .unwrap_or(0) as u32; + let blinder_idx = inp_txout_sec.keys().copied().min().unwrap_or(0) as u32; // Add specified outputs for o in &req.outputs { - pset.add_output(build_output(o.script_pubkey.clone(), o.amount, o.asset, o.blinding_key, blinder_idx)); + pset.add_output(build_output( + o.script_pubkey.clone(), + o.amount, + o.asset, + o.blinding_key, + blinder_idx, + )); } // L-BTC change output (if any) if change > 0 { - let change_addr = wollet.change(None).context("Cannot derive change address")?.address().clone(); + let change_addr = wollet + .change(None) + .context("Cannot derive change address")? + .address() + .clone(); let change_bpk = change_addr .blinding_pubkey .map(btc_pubkey) .unwrap_or(wallet_blinding_pk); pset.add_output(confidential_output( - change_addr.script_pubkey(), change, req.policy_asset, change_bpk, blinder_idx + change_addr.script_pubkey(), + change, + req.policy_asset, + change_bpk, + blinder_idx, )); } // Non-LBTC change outputs: for any wallet-input asset where the input exceeds the outputs. - let total_non_lbtc_out: HashMap = req.outputs.iter() + let total_non_lbtc_out: HashMap = req + .outputs + .iter() .filter(|o| o.asset != req.policy_asset) - .fold(HashMap::new(), |mut m, o| { *m.entry(o.asset).or_default() += o.amount; m }); + .fold(HashMap::new(), |mut m, o| { + *m.entry(o.asset).or_default() += o.amount; + m + }); for (asset, in_amt) in &wallet_asset_in { let out_amt = total_non_lbtc_out.get(asset).copied().unwrap_or(0); if *in_amt > out_amt { let surplus = in_amt - out_amt; - let change_addr = wollet.change(None).context("Cannot derive change address")?.address().clone(); - let change_bpk = change_addr.blinding_pubkey.map(btc_pubkey).unwrap_or(wallet_blinding_pk); + let change_addr = wollet + .change(None) + .context("Cannot derive change address")? + .address() + .clone(); + let change_bpk = change_addr + .blinding_pubkey + .map(btc_pubkey) + .unwrap_or(wallet_blinding_pk); pset.add_output(confidential_output( - change_addr.script_pubkey(), surplus, *asset, change_bpk, blinder_idx + change_addr.script_pubkey(), + surplus, + *asset, + change_bpk, + blinder_idx, )); } } // Fee output - pset.add_output(Output::new_explicit(Script::default(), fee, req.policy_asset, None)); + pset.add_output(Output::new_explicit( + Script::default(), + fee, + req.policy_asset, + None, + )); let _ = network; // reserved for future address encoding Ok((pset, inp_txout_sec, issuances)) @@ -409,20 +515,32 @@ fn add_wallet_input( asset_bf: AssetBlindingFactor::zero(), } } else { - let value_comm = txout.value.commitment() + let value_comm = txout + .value + .commitment() .ok_or_else(|| anyhow::anyhow!("Input TxOut value is not a commitment"))?; - let asset_gen = txout.asset.commitment() + let asset_gen = txout + .asset + .commitment() .ok_or_else(|| anyhow::anyhow!("Input TxOut asset is not a commitment"))?; input.in_utxo_rangeproof = txout.witness.rangeproof.take(); input.witness_utxo = Some(txout); input.blind_asset_proof = Some(Box::new( - SurjectionProof::blind_asset_proof(rng, secp, utxo.unblinded.asset, utxo.unblinded.asset_bf) - .map_err(|e| anyhow::anyhow!("blind_asset_proof failed: {e}"))?, + SurjectionProof::blind_asset_proof( + rng, + secp, + utxo.unblinded.asset, + utxo.unblinded.asset_bf, + ) + .map_err(|e| anyhow::anyhow!("blind_asset_proof failed: {e}"))?, )); input.blind_value_proof = Some(Box::new( RangeProof::blind_value_proof( - rng, secp, - utxo.unblinded.value, value_comm, asset_gen, + rng, + secp, + utxo.unblinded.value, + value_comm, + asset_gen, utxo.unblinded.value_bf, ) .map_err(|e| anyhow::anyhow!("blind_value_proof failed: {e}"))?, @@ -462,12 +580,15 @@ fn add_covenant_input( input.amount = Some(amount); pset.add_input(input); let idx = pset.inputs().len() - 1; - inp_txout_sec.insert(idx, TxOutSecrets { - value: amount, - value_bf: ValueBlindingFactor::zero(), - asset, - asset_bf: AssetBlindingFactor::zero(), - }); + inp_txout_sec.insert( + idx, + TxOutSecrets { + value: amount, + value_bf: ValueBlindingFactor::zero(), + asset, + asset_bf: AssetBlindingFactor::zero(), + }, + ); Ok(idx) } @@ -480,8 +601,16 @@ fn apply_sequence(pset: &mut PartiallySignedTransaction, idx: usize, sequence: O } } -fn apply_new_issuance(pset: &mut PartiallySignedTransaction, idx: usize, iso: &IssuanceKind) -> Result<()> { - if let IssuanceKind::New { asset_amount, inflation_amount } = iso { +fn apply_new_issuance( + pset: &mut PartiallySignedTransaction, + idx: usize, + iso: &IssuanceKind, +) -> Result<()> { + if let IssuanceKind::New { + asset_amount, + inflation_amount, + } = iso + { let input = &mut pset.inputs_mut()[idx]; if *asset_amount > 0 { input.issuance_value_amount = Some(*asset_amount); @@ -495,17 +624,25 @@ fn apply_new_issuance(pset: &mut PartiallySignedTransaction, idx: usize, iso: &I Ok(()) } -fn apply_reissuance(pset: &mut PartiallySignedTransaction, idx: usize, iso: &IssuanceKind) -> Result<()> { - if let IssuanceKind::Reissue { asset_amount, entropy } = iso { +fn apply_reissuance( + pset: &mut PartiallySignedTransaction, + idx: usize, + iso: &IssuanceKind, +) -> Result<()> { + if let IssuanceKind::Reissue { + asset_amount, + entropy, + } = iso + { let input = &mut pset.inputs_mut()[idx]; input.issuance_value_amount = Some(*asset_amount); input.issuance_asset_entropy = Some(*entropy); input.blinded_issuance = Some(0x00); // 0x00 = explicit (not confidential) - // issuance_blinding_nonce must be non-zero so issuance_ids() takes the re-issuance - // code path (entropy used directly) rather than the new-issuance path (entropy derived - // from outpoint). For explicit (non-confidential) RT UTXOs the actual asset blinding - // factor is zero, but ZERO_TWEAK would be misread as "new issuance". Use the minimal - // non-zero scalar [0..0, 1] as a conventional explicit-reissuance marker. + // issuance_blinding_nonce must be non-zero so issuance_ids() takes the re-issuance + // code path (entropy used directly) rather than the new-issuance path (entropy derived + // from outpoint). For explicit (non-confidential) RT UTXOs the actual asset blinding + // factor is zero, but ZERO_TWEAK would be misread as "new issuance". Use the minimal + // non-zero scalar [0..0, 1] as a conventional explicit-reissuance marker. let mut nonce_bytes = [0u8; 32]; nonce_bytes[31] = 1; input.issuance_blinding_nonce = Some( @@ -561,7 +698,10 @@ fn confidential_output( /// entropy = fast_merkle_root([prevout_hash, zero_contract_hash]) /// asset = SHA256(entropy || 0x00) as Midstate /// token = SHA256(entropy || 0x01) as Midstate (explicit, confidential=false) -pub fn compute_asset_ids_from_outpoint(txid_display: &str, vout: u32) -> Result<(AssetId, AssetId)> { +pub fn compute_asset_ids_from_outpoint( + txid_display: &str, + vout: u32, +) -> Result<(AssetId, AssetId)> { let txid = Txid::from_str(txid_display) .map_err(|e| anyhow::anyhow!("Cannot parse txid '{txid_display}': {e}"))?; let outpoint = OutPoint::new(txid, vout); @@ -581,8 +721,13 @@ pub fn compute_asset_from_entropy(entropy: &[u8; 32]) -> Result { // Utility // --------------------------------------------------------------------------- -fn btc_pubkey(pk: lwk_wollet::elements::secp256k1_zkp::PublicKey) -> lwk_wollet::elements::bitcoin::PublicKey { - lwk_wollet::elements::bitcoin::PublicKey { inner: pk, compressed: true } +fn btc_pubkey( + pk: lwk_wollet::elements::secp256k1_zkp::PublicKey, +) -> lwk_wollet::elements::bitcoin::PublicKey { + lwk_wollet::elements::bitcoin::PublicKey { + inner: pk, + compressed: true, + } } /// Resolve the covenant address for a utxo_type and return its script_pubkey. @@ -594,8 +739,15 @@ pub fn covenant_script_pubkey( network: ElementsNetwork, include_debug_symbols: bool, ) -> Result