From ecb2cc640ae40abac978e3283d48cbb740bc58df Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:06:02 +0000 Subject: [PATCH] chore: remove TUI Zone code (#3439) --- Cargo.lock | 26 - Cargo.toml | 2 - deployment/tui-zone/Cargo.toml | 41 -- deployment/tui-zone/README.manual-demo.md | 332 ------------ deployment/tui-zone/src/cli.rs | 364 ------------- deployment/tui-zone/src/lib.rs | 10 - deployment/tui-zone/src/main.rs | 32 -- deployment/tui-zone/src/message.rs | 61 --- .../tui-zone/src/run_commands/driver.rs | 177 ------- deployment/tui-zone/src/run_commands/mod.rs | 48 -- .../tui-zone/src/run_commands/run_balance.rs | 24 - .../tui-zone/src/run_commands/run_config.rs | 409 --------------- .../tui-zone/src/run_commands/run_deposit.rs | 94 ---- .../tui-zone/src/run_commands/run_inscribe.rs | 253 --------- .../tui-zone/src/run_commands/run_keygen.rs | 10 - .../tui-zone/src/run_commands/run_withdraw.rs | 318 ------------ deployment/tui-zone/src/run_commands/types.rs | 205 -------- .../tui-zone/src/run_commands/unit_tests.rs | 491 ------------------ deployment/tui-zone/src/run_commands/utils.rs | 417 --------------- deployment/tui-zone/src/state.rs | 205 -------- deployment/tui-zone/src/ui.rs | 70 --- tests/Cargo.toml | 1 - .../manual_transactions/command_file_utils.rs | 5 - 23 files changed, 3595 deletions(-) delete mode 100644 deployment/tui-zone/Cargo.toml delete mode 100644 deployment/tui-zone/README.manual-demo.md delete mode 100644 deployment/tui-zone/src/cli.rs delete mode 100644 deployment/tui-zone/src/lib.rs delete mode 100644 deployment/tui-zone/src/main.rs delete mode 100644 deployment/tui-zone/src/message.rs delete mode 100644 deployment/tui-zone/src/run_commands/driver.rs delete mode 100644 deployment/tui-zone/src/run_commands/mod.rs delete mode 100644 deployment/tui-zone/src/run_commands/run_balance.rs delete mode 100644 deployment/tui-zone/src/run_commands/run_config.rs delete mode 100644 deployment/tui-zone/src/run_commands/run_deposit.rs delete mode 100644 deployment/tui-zone/src/run_commands/run_inscribe.rs delete mode 100644 deployment/tui-zone/src/run_commands/run_keygen.rs delete mode 100644 deployment/tui-zone/src/run_commands/run_withdraw.rs delete mode 100644 deployment/tui-zone/src/run_commands/types.rs delete mode 100644 deployment/tui-zone/src/run_commands/unit_tests.rs delete mode 100644 deployment/tui-zone/src/run_commands/utils.rs delete mode 100644 deployment/tui-zone/src/state.rs delete mode 100644 deployment/tui-zone/src/ui.rs diff --git a/Cargo.lock b/Cargo.lock index e6db7ae98..45ebc64c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5102,7 +5102,6 @@ dependencies = [ "logos-blockchain-mmr", "logos-blockchain-node", "logos-blockchain-storage-service", - "logos-blockchain-tui-zone", "logos-blockchain-tx-service", "logos-blockchain-utils", "logos-blockchain-utxotree", @@ -5220,30 +5219,6 @@ dependencies = [ "tracing-subscriber 0.3.23", ] -[[package]] -name = "logos-blockchain-tui-zone" -version = "0.0.0" -dependencies = [ - "bincode", - "chrono", - "clap", - "hex", - "logos-blockchain-codec", - "logos-blockchain-core", - "logos-blockchain-groth16", - "logos-blockchain-key-management-system-service", - "logos-blockchain-zone-sdk", - "rand 0.8.6", - "reqwest 0.12.28", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-appender", - "tracing-subscriber 0.3.23", - "uuid", -] - [[package]] name = "logos-blockchain-tx-service" version = "0.0.0" @@ -9383,7 +9358,6 @@ checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.3", "js-sys", - "serde_core", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index b58c1166c..22f29478c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ members = [ "consensus/cryptarchia-sync", "core", "deployment/faucet", - "deployment/tui-zone", "kms/keys", "kms/macros", "kms/operators", @@ -158,7 +157,6 @@ lb-testing-framework = { default-features = false, package = "test lb-time-service = { default-features = false, package = "logos-blockchain-time-service", path = "./services/time" } lb-tracing = { default-features = false, package = "logos-blockchain-tracing", path = "./tracing" } lb-tracing-service = { default-features = false, package = "logos-blockchain-tracing-service", path = "./services/tracing" } -lb-tui-zone = { default-features = false, package = "logos-blockchain-tui-zone", path = "./deployment/tui-zone" } lb-tx-service = { default-features = false, package = "logos-blockchain-tx-service", path = "./services/tx-service" } lb-utils = { default-features = false, package = "logos-blockchain-utils", path = "./utils" } lb-utxotree = { default-features = false, package = "logos-blockchain-utxotree", path = "merkle/utxotree" } diff --git a/deployment/tui-zone/Cargo.toml b/deployment/tui-zone/Cargo.toml deleted file mode 100644 index fd8278710..000000000 --- a/deployment/tui-zone/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -categories = { workspace = true } -description = "Terminal UI zone sequencer - prompts for messages and publishes them as zone blocks" -edition = { workspace = true } -keywords = { workspace = true } -license = { workspace = true } -name = "logos-blockchain-tui-zone" -readme = { workspace = true } -repository = { workspace = true } -version = { workspace = true } - -[lib] -name = "logos_blockchain_tui_zone" -path = "src/lib.rs" - -[[bin]] -name = "tui-sequencer" -path = "src/main.rs" - -[lints] -workspace = true - -[dependencies] -bincode = { workspace = true } -chrono = { features = ["alloc"], workspace = true } -clap = { features = ["derive", "env", "error-context", "help", "std", "suggestions", "usage"], workspace = true } -hex = { workspace = true } -lb-codec = { workspace = true } -lb-core = { workspace = true } -lb-groth16 = { workspace = true } -lb-key-management-system-service = { workspace = true } -lb-zone-sdk = { workspace = true } -rand = { workspace = true } -reqwest = { workspace = true } -serde = { features = ["derive"], workspace = true } -serde_json = { workspace = true } -tokio = { features = ["macros", "rt-multi-thread", "signal", "sync", "time"], workspace = true } -tracing = { workspace = true } -tracing-appender = { workspace = true } -tracing-subscriber = { features = ["env-filter", "registry"], workspace = true } -uuid = { features = ["serde", "v4"], workspace = true } diff --git a/deployment/tui-zone/README.manual-demo.md b/deployment/tui-zone/README.manual-demo.md deleted file mode 100644 index f9299309c..000000000 --- a/deployment/tui-zone/README.manual-demo.md +++ /dev/null @@ -1,332 +0,0 @@ -# Manual TUI Zone Deposit/Withdrawal Demo - -This demo uses the existing cucumber `Transactions manual control` scenario as -the chain and wallet bootstrapper, then uses `tui-sequencer` subcommands for -file-based Zone deposit and withdrawal. - -The wallet export format is demo-only. `include_secret true` writes private -wallet material to disk so the TUI command can spend exported cucumber wallet -UTXOs into a Zone deposit. - -## Start from a clean slate - -Remove prior demo files and create the working directories: - -```sh -rm -rf /tmp/tui-zone -mkdir -p /tmp/tui-zone/keys /tmp/tui-zone/artifacts -``` - -## [Cucumber] Start cucumber manual control - -```sh -export CUCUMBER_MANUAL_COMMAND_FILE=/tmp/cucumber-manual-commands.txt -export CUCUMBER_LOG_LEVEL=trace -export CUCUMBER_VERBOSE_CONSOLE=true -cargo test -p logos-blockchain-tests --features cucumber --test cucumber -- --name "Transactions manual control" -``` - -Wait until the scenario reaches: - -```text -When I perform manual control of transactions for all wallets no time-out -``` - -## [Cucumber] Export wallet funds - -Append commands to `/tmp/cucumber-manual-commands.txt`: - -```text -EXPORT_FUNDS, wallet 'WALLET_1A', value 1000, output '/tmp/tui-zone/artifacts/funds-wallet-1a.json', include_secret true -EXPORT_FUNDS, wallet 'WALLET_2A', value 1000, output '/tmp/tui-zone/artifacts/funds-wallet-2a.json', include_secret true -``` - -Processed commands are marked with `---->`. Invalid commands are marked with -`== ERROR == >`. - -The exported funds are not committed on-chain yet, so cucumber wallet balances -will not change until after the corresponding deposit transaction is mined. - -Use the `node_url` field from the exported funds JSON files if your node is not -available at `http://localhost:`. - -## [Sequencer] Create and validate a new channel - -Start the sequencer with the channel admin key: - -```sh -cargo run -p logos-blockchain-tui-zone -- run \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key -``` - -Post one inscription and wait until it is shown as adopted/published: - -```text -a1 -``` - -Then stop the sequencer with `CTRL-C`. - -Create the second, third, and fourth signer keys: - -```sh -cargo run -p logos-blockchain-tui-zone -- keygen \ - --key-path /tmp/tui-zone/keys/seq-b.key - -cargo run -p logos-blockchain-tui-zone -- keygen \ - --key-path /tmp/tui-zone/keys/seq-c.key - -cargo run -p logos-blockchain-tui-zone -- keygen \ - --key-path /tmp/tui-zone/keys/seq-d.key -``` - -`seq-a.key` is the channel admin key. `seq-b.key`, `seq-c.key`, and -`seq-d.key` are only accredited later by channel config commands. - -Print the channel balance at any point: - -```sh -cargo run -p logos-blockchain-tui-zone -- state balance \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key -``` - -## [Sequencer] Deposit - -Deposit the first exported wallet funds: - -```sh -cargo run -p logos-blockchain-tui-zone -- deposit \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --funds /tmp/tui-zone/artifacts/funds-wallet-1a.json \ - --amount 1000 \ - --metadata "demo deposit" \ - --message "deposit wallet 1a" -``` - -Deposit the second exported wallet funds: - -```sh -cargo run -p logos-blockchain-tui-zone -- deposit \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --funds /tmp/tui-zone/artifacts/funds-wallet-2a.json \ - --amount 1000 \ - --metadata "demo deposit" \ - --message "deposit wallet 2a" -``` - -## [Cucumber] Check deposit balances - -Append: - -```text -BALANCE, wallet 'WALLET_1A' -BALANCE, wallet 'WALLET_2A' -``` - -If a balance does not reflect the deposit yet, wait for the deposit transaction -to be mined and observed by the cucumber wallet before proceeding. - -## [Sequencer] Single-signer withdrawal - -Prepare the withdrawal intent: - -```sh -cargo run -p logos-blockchain-tui-zone -- withdraw prepare \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --amount 500 \ - --recipient-funds /tmp/tui-zone/artifacts/funds-wallet-1a.json \ - --message "withdraw wallet 1a" \ - --out /tmp/tui-zone/artifacts/withdraw.intent.json -``` - -Sign it with the channel admin key: - -```sh -cargo run -p logos-blockchain-tui-zone -- withdraw sign \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --in /tmp/tui-zone/artifacts/withdraw.intent.json \ - --out /tmp/tui-zone/artifacts/sig-a.json -``` - -Combine the intent and signature: - -```sh -cargo run -p logos-blockchain-tui-zone -- withdraw combine \ - --in /tmp/tui-zone/artifacts/withdraw.intent.json \ - --sig /tmp/tui-zone/artifacts/sig-a.json \ - --out /tmp/tui-zone/artifacts/withdraw.signed.json -``` - -Submit the signed withdrawal: - -```sh -cargo run -p logos-blockchain-tui-zone -- withdraw submit \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --in /tmp/tui-zone/artifacts/withdraw.signed.json -``` - -## [Cucumber] Check single-signer withdrawal balance - -Append: - -```text -BALANCE, wallet 'WALLET_1A' -``` - -## [Sequencer] Configure multi-signers - -Configure the Zone channel created by `seq-a.key` so the accredited withdrawal -keys contain the first three local sequencer keys, the withdrawal threshold is -`2`, and future configuration changes require `2` signatures: - -```sh -cargo run -p logos-blockchain-tui-zone -- config apply \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --authorized-key-path /tmp/tui-zone/keys/seq-a.key \ - --authorized-key-path /tmp/tui-zone/keys/seq-b.key \ - --authorized-key-path /tmp/tui-zone/keys/seq-c.key \ - --configuration-threshold 2 \ - --withdraw-threshold 2 \ - --posting-timeframe 30 \ - --posting-timeout 30 -``` - -The `--key-path` key signs this threshold-1 update and is kept at authorized key -index `0`; duplicate `--authorized-key-path` entries are ignored. - -## [Sequencer] Multi-signer withdrawal - -Prepare the 2-of-3 withdrawal intent: - -```sh -cargo run -p logos-blockchain-tui-zone -- withdraw prepare \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --amount 500 \ - --recipient-funds /tmp/tui-zone/artifacts/funds-wallet-1a.json \ - --message "withdraw wallet 1a multisig" \ - --out /tmp/tui-zone/artifacts/withdraw-2of3.intent.json -``` - -Sign the same intent with two different authorized signer keys: - -```sh -cargo run -p logos-blockchain-tui-zone -- withdraw sign \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --in /tmp/tui-zone/artifacts/withdraw-2of3.intent.json \ - --out /tmp/tui-zone/artifacts/sig-a.json - -cargo run -p logos-blockchain-tui-zone -- withdraw sign \ - --key-path /tmp/tui-zone/keys/seq-b.key \ - --in /tmp/tui-zone/artifacts/withdraw-2of3.intent.json \ - --out /tmp/tui-zone/artifacts/sig-b.json -``` - -Combine both signatures: - -```sh -cargo run -p logos-blockchain-tui-zone -- withdraw combine \ - --in /tmp/tui-zone/artifacts/withdraw-2of3.intent.json \ - --sig /tmp/tui-zone/artifacts/sig-a.json \ - --sig /tmp/tui-zone/artifacts/sig-b.json \ - --out /tmp/tui-zone/artifacts/withdraw-2of3.signed.json -``` - -Submit the signed withdrawal: - -```sh -cargo run -p logos-blockchain-tui-zone -- withdraw submit \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --in /tmp/tui-zone/artifacts/withdraw-2of3.signed.json -``` - -Each signer keeps its private key local. The only exchanged files are the intent -JSON and signature JSON files. - -## [Cucumber] Check multi-signer withdrawal balance - -Append: - -```text -BALANCE, wallet 'WALLET_1A' -``` - -The withdrawn funds are normal chain notes addressed to the exported cucumber -wallet public key, so they are observed through the existing wallet scan path. - -## [Sequencer] Multi-signer config update - -Prepare a 2-of-3 configuration update that adds the fourth key and raises both -thresholds to `3`: - -```sh -cargo run -p logos-blockchain-tui-zone -- config prepare \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --authorized-key-path /tmp/tui-zone/keys/seq-a.key \ - --authorized-key-path /tmp/tui-zone/keys/seq-b.key \ - --authorized-key-path /tmp/tui-zone/keys/seq-c.key \ - --authorized-key-path /tmp/tui-zone/keys/seq-d.key \ - --configuration-threshold 3 \ - --withdraw-threshold 3 \ - --posting-timeframe 30 \ - --posting-timeout 30 \ - --out /tmp/tui-zone/artifacts/config-3of4.intent.json -``` - -Sign the config intent with two currently authorized keys: - -```sh -cargo run -p logos-blockchain-tui-zone -- config sign \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --in /tmp/tui-zone/artifacts/config-3of4.intent.json \ - --out /tmp/tui-zone/artifacts/config-sig-a.json - -cargo run -p logos-blockchain-tui-zone -- config sign \ - --key-path /tmp/tui-zone/keys/seq-b.key \ - --in /tmp/tui-zone/artifacts/config-3of4.intent.json \ - --out /tmp/tui-zone/artifacts/config-sig-b.json -``` - -Combine the signatures: - -```sh -cargo run -p logos-blockchain-tui-zone -- config combine \ - --in /tmp/tui-zone/artifacts/config-3of4.intent.json \ - --sig /tmp/tui-zone/artifacts/config-sig-a.json \ - --sig /tmp/tui-zone/artifacts/config-sig-b.json \ - --out /tmp/tui-zone/artifacts/config-3of4.signed.json -``` - -Submit the signed config update: - -```sh -cargo run -p logos-blockchain-tui-zone -- config submit \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key \ - --in /tmp/tui-zone/artifacts/config-3of4.signed.json -``` - -Check the full channel state: - -```sh -cargo run -p logos-blockchain-tui-zone -- state full \ - --node-url http://localhost: \ - --key-path /tmp/tui-zone/keys/seq-a.key -``` - -## [Cucumber] Stop cucumber scenario - -Append: - -```text -STOP -``` diff --git a/deployment/tui-zone/src/cli.rs b/deployment/tui-zone/src/cli.rs deleted file mode 100644 index 5f1c2b399..000000000 --- a/deployment/tui-zone/src/cli.rs +++ /dev/null @@ -1,364 +0,0 @@ -use std::{error::Error, path::PathBuf}; - -use clap::{Args, Parser, Subcommand}; - -use crate::run_commands::{ - run_balance::run_state_full, - run_config::{ - run_config, run_config_combine, run_config_prepare, run_config_sign, run_config_submit, - }, - run_deposit::run_deposit, - run_keygen::run_keygen, - run_withdraw::{ - run_withdraw_combine, run_withdraw_prepare, run_withdraw_sign, run_withdraw_submit, - }, -}; - -pub(crate) type RunResult = Result>; - -#[derive(Parser, Debug)] -#[command(about = "Terminal UI zone sequencer", version)] -/// Top-level command-line parser for the TUI zone sequencer. -pub struct Cli { - #[command(subcommand)] - command: Option, - - #[command(flatten)] - run_args: NodeKeyArgs, -} - -#[derive(Subcommand, Debug)] -enum Command { - /// Run the interactive inscription TUI. - Run(NodeKeyArgs), - /// Apply, prepare, sign, combine, or submit zone channel configuration - /// updates. - Config { - #[command(subcommand)] - command: ConfigCommand, - }, - /// Print zone channel state. - State { - #[command(subcommand)] - command: StateCommand, - }, - /// Build and optionally submit a zone deposit. - Deposit(DepositArgs), - /// Create or inspect a local sequencer signing key. - Keygen(KeygenArgs), - /// Prepare, sign, combine, or submit zone withdrawals. - Withdraw { - #[command(subcommand)] - command: WithdrawCommand, - }, -} - -#[derive(Subcommand, Debug)] -enum ConfigCommand { - /// Apply a single-signer channel configuration update. - Apply(ConfigArgs), - /// Prepare an unsigned channel configuration intent file. - Prepare(ConfigPrepareArgs), - /// Sign a channel configuration intent with one authorized key. - Sign(ConfigSignArgs), - /// Combine configuration signature files into a signed transaction file. - Combine(ConfigCombineArgs), - /// Submit a signed channel configuration transaction file. - Submit(ConfigSubmitArgs), -} - -#[derive(Subcommand, Debug)] -enum StateCommand { - /// Print the channel configuration state. - Full(StateArgs), - // TODO: support channel note tracking, which restores the - // channel balance command. - // /// Print the channel balance. - // Balance(StateArgs), -} - -#[derive(Parser, Debug)] -#[command(about = "Terminal UI zone sequencer - publish text inscriptions")] -/// Shared node endpoint and channel signing key arguments. -pub struct NodeKeyArgs { - /// Logos blockchain node HTTP endpoint - #[arg(long, default_value = "http://localhost:8080", env = "NODE_URL")] - pub node_url: String, - - /// Zone channel ID to use instead of deriving one from the signing key. - #[arg(long, env = "CHANNEL_ID")] - pub channel_id: Option, - - /// Path to the signing key file (created if it doesn't exist) - #[arg(long, default_value = "sequencer.key", env = "KEY_PATH")] - pub key_path: String, - - /// Node wallet public key (hex, 32 bytes) used to pay transaction fees. - /// The value comes from the node's own configuration. - #[arg(long, env = "FUNDING_PK")] - pub funding_pk: String, - - /// Cap on a single transaction's fee (in gas units) when funding via - /// `--funding-pk`. - #[arg(long, default_value_t = 1_000_000, env = "MAX_TX_FEE")] - pub max_tx_fee: u64, - - /// Percentage of the final mandatory fee reserved when funding via - /// `--funding-pk`. The percentage covers execution plus storage cost; only - /// unused reserve becomes an effective priority tip. The 12% default is a - /// practical reserve for normal fee movement, including approximately one - /// storage-market epoch at normal price levels, not a protocol guarantee - /// at very low prices or when execution fees also rise materially. Storage - /// prices use integer arithmetic, so 1 can become 2. Capped in total by - /// `--max-tx-fee`. - #[arg( - long, - default_value_t = lb_zone_sdk::sequencer::FundingConfig::DEFAULT_PRIORITY_FEE_PERCENT, - env = "PRIORITY_FEE_PERCENT" - )] - pub priority_fee_percent: u64, -} - -#[derive(Args, Debug)] -/// Arguments for building and optionally submitting a zone deposit. -pub struct DepositArgs { - #[command(flatten)] - /// Node endpoint and channel signing key used by the sequencer. - pub node_key: NodeKeyArgs, - #[arg(long)] - /// Path to a cucumber `EXPORT_FUNDS` wallet funds JSON file. - pub funds: PathBuf, - #[arg(long)] - /// Amount to deposit into the zone channel. - pub amount: u64, - #[arg(long)] - /// Deposit metadata stored in the channel deposit op. - pub metadata: String, - #[arg(long)] - /// Inscription message paired with the deposit transaction. - pub message: String, - #[arg(long)] - /// Wait for finality instead of returning once the tx is observed on chain. - pub wait_finalized: bool, -} - -#[derive(Args, Debug)] -/// Arguments for updating zone channel configuration. -pub struct ConfigArgs { - #[command(flatten)] - /// Node endpoint and channel admin signing key. - pub node_key: NodeKeyArgs, - #[arg(long = "authorized-key-path", required = true)] - /// Paths to signing keys that should be accredited for the channel. - pub authorized_key_paths: Vec, - #[arg(long, default_value_t = 1)] - /// Number of accredited signatures required for future config updates. - pub configuration_threshold: u16, - #[arg(long)] - /// Number of accredited signatures required to transfer or withdraw the - /// channel's notes. - pub transfer_threshold: u16, - #[arg(long, default_value_t = 0)] - /// Number of slots assigned to an accredited poster. - pub posting_timeframe: u32, - #[arg(long, default_value_t = 0)] - /// Number of slots after which a poster is considered timed out. - pub posting_timeout: u32, - #[arg(long)] - /// Wait for finality instead of returning once the tx is observed on chain. - pub wait_finalized: bool, -} - -#[derive(Args, Debug)] -/// Arguments for printing zone channel state. -pub struct StateArgs { - #[command(flatten)] - /// Node endpoint and channel key used to resolve the channel. - pub node_key: NodeKeyArgs, -} - -#[derive(Args, Debug)] -/// Arguments for preparing an unsigned channel configuration intent file. -pub struct ConfigPrepareArgs { - #[command(flatten)] - /// Node endpoint and channel signing key. - pub node_key: NodeKeyArgs, - #[arg(long = "authorized-key-path", required = true)] - /// Paths to signing keys that should be accredited for the channel. - pub authorized_key_paths: Vec, - #[arg(long)] - /// Number of accredited signatures required for future config updates. - pub configuration_threshold: u16, - #[arg(long)] - /// Number of accredited signatures required to transfer or withdraw the - /// channel's notes. - pub transfer_threshold: u16, - #[arg(long, default_value_t = 0)] - /// Number of slots assigned to an accredited poster. - pub posting_timeframe: u32, - #[arg(long, default_value_t = 0)] - /// Number of slots after which a poster is considered timed out. - pub posting_timeout: u32, - #[arg(long)] - /// Path where the configuration intent JSON is written. - pub out: PathBuf, -} - -#[derive(Args, Debug)] -/// Arguments for signing a channel configuration intent with one key. -pub struct ConfigSignArgs { - #[arg(long)] - /// Path to the signer key file. - pub key_path: String, - #[arg(long = "in")] - /// Path to the configuration intent JSON file. - pub input: PathBuf, - #[arg(long)] - /// Path where the signature JSON is written. - pub out: PathBuf, -} - -#[derive(Args, Debug)] -/// Arguments for combining configuration signature files into a signed tx file. -pub struct ConfigCombineArgs { - #[arg(long = "in")] - /// Path to the configuration intent JSON file. - pub input: PathBuf, - #[arg(long)] - /// Signature JSON file paths to include. - pub sig: Vec, - #[arg(long)] - /// Path where the signed configuration transaction JSON is written. - pub out: PathBuf, -} - -#[derive(Args, Debug)] -/// Arguments for submitting a signed channel configuration transaction file. -pub struct ConfigSubmitArgs { - #[command(flatten)] - /// Node endpoint and channel signing key used to submit the transaction. - pub node_key: NodeKeyArgs, - #[arg(long = "in")] - /// Path to the signed configuration transaction JSON file. - pub input: PathBuf, - #[arg(long)] - /// Wait for finality instead of returning once the tx is observed on chain. - pub wait_finalized: bool, -} - -#[derive(Args, Debug)] -/// Arguments for creating or inspecting a local sequencer signing key. -pub struct KeygenArgs { - #[arg(long, default_value = "sequencer.key", env = "KEY_PATH")] - /// Path to the signing key file to create or inspect. - pub key_path: String, -} - -#[derive(Subcommand, Debug)] -enum WithdrawCommand { - /// Prepare an unsigned withdrawal intent file. - Prepare(WithdrawPrepareArgs), - /// Sign a withdrawal intent with one authorized key. - Sign(WithdrawSignArgs), - /// Combine withdrawal signature files into a signed transaction file. - Combine(WithdrawCombineArgs), - /// Submit a signed withdrawal transaction file. - Submit(WithdrawSubmitArgs), -} - -#[derive(Args, Debug)] -/// Arguments for preparing an unsigned withdrawal intent file. -pub struct WithdrawPrepareArgs { - #[command(flatten)] - /// Node endpoint and channel signing key used to prepare the intent. - pub node_key: NodeKeyArgs, - #[arg(long)] - /// Amount to withdraw from the zone channel. - pub amount: u64, - #[arg(long)] - /// Path to a recipient cucumber `EXPORT_FUNDS` JSON file. - pub recipient_funds: PathBuf, - #[arg(long)] - /// Inscription message paired with the withdrawal transaction. - pub message: String, - #[arg(long)] - /// Path where the withdrawal intent JSON is written. - pub out: PathBuf, -} - -#[derive(Args, Debug)] -/// Arguments for signing a withdrawal intent with one authorized key. -pub struct WithdrawSignArgs { - #[arg(long)] - /// Path to the signer key file. - pub key_path: String, - #[arg(long = "in")] - /// Path to the withdrawal intent JSON file. - pub input: PathBuf, - #[arg(long)] - /// Path where the signature JSON is written. - pub out: PathBuf, -} - -#[derive(Args, Debug)] -/// Arguments for combining withdrawal signature files into a signed tx file. -pub struct WithdrawCombineArgs { - #[arg(long = "in")] - /// Path to the withdrawal intent JSON file. - pub input: PathBuf, - #[arg(long)] - /// Signature JSON file paths to include. - pub sig: Vec, - #[arg(long)] - /// Path where the signed withdrawal transaction JSON is written. - pub out: PathBuf, -} - -#[derive(Args, Debug)] -/// Arguments for submitting a signed withdrawal transaction file. -pub struct WithdrawSubmitArgs { - #[command(flatten)] - /// Node endpoint and channel signing key used to submit the transaction. - pub node_key: NodeKeyArgs, - #[arg(long = "in")] - /// Path to the signed withdrawal transaction JSON file. - pub input: PathBuf, - #[arg(long)] - /// Wait for finality instead of returning once the tx is observed on chain. - pub wait_finalized: bool, -} - -/// Dispatch a parsed CLI command to the matching command runner. -pub async fn run_cli(cli: Cli) -> RunResult<()> { - match cli.command { - Some(Command::Run(args)) => { - crate::run_commands::run_inscribe::run_inscribe(args).await; - Ok(()) - } - Some(Command::Config { command }) => match command { - ConfigCommand::Apply(args) => run_config(args).await, - ConfigCommand::Prepare(args) => run_config_prepare(args).await, - ConfigCommand::Sign(args) => run_config_sign(&args), - ConfigCommand::Combine(args) => run_config_combine(args), - ConfigCommand::Submit(args) => run_config_submit(args).await, - }, - Some(Command::State { command }) => match command { - StateCommand::Full(args) => run_state_full(args).await, - }, - Some(Command::Deposit(args)) => run_deposit(args).await, - Some(Command::Keygen(args)) => { - run_keygen(&args); - Ok(()) - } - Some(Command::Withdraw { command }) => match command { - WithdrawCommand::Prepare(args) => run_withdraw_prepare(args).await, - WithdrawCommand::Sign(args) => run_withdraw_sign(&args), - WithdrawCommand::Combine(args) => run_withdraw_combine(args), - WithdrawCommand::Submit(args) => run_withdraw_submit(args).await, - }, - None => { - crate::run_commands::run_inscribe::run_inscribe(cli.run_args).await; - Ok(()) - } - } -} diff --git a/deployment/tui-zone/src/lib.rs b/deployment/tui-zone/src/lib.rs deleted file mode 100644 index 8979676a5..000000000 --- a/deployment/tui-zone/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Command-line argument parsing and dispatch. -pub mod cli; -/// Message payload types rendered by the TUI. -pub mod message; -/// Manual command implementations used by the CLI. -pub mod run_commands; -/// In-memory TUI state. -pub mod state; -/// Terminal rendering helpers. -pub mod ui; diff --git a/deployment/tui-zone/src/main.rs b/deployment/tui-zone/src/main.rs deleted file mode 100644 index 0014a823e..000000000 --- a/deployment/tui-zone/src/main.rs +++ /dev/null @@ -1,32 +0,0 @@ -use clap::Parser as _; -use logos_blockchain_tui_zone::cli::{Cli, run_cli}; -use tracing_subscriber::{ - Layer as _, filter::LevelFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _, -}; - -#[tokio::main] -async fn main() { - let file_appender = tracing_appender::rolling::daily("logs", "tui-zone.log"); - let (log_writer, _log_guard) = tracing_appender::non_blocking(file_appender); - - let console_layer = tracing_subscriber::fmt::layer() - .with_writer(std::io::stdout) - .with_ansi(true) - .with_filter(LevelFilter::WARN); - - let file_layer = tracing_subscriber::fmt::layer() - .with_writer(log_writer) - .with_ansi(false) - .with_filter(LevelFilter::DEBUG); - - tracing_subscriber::registry() - .with(console_layer) - .with(file_layer) - .init(); - - let args = Cli::parse(); - if let Err(error) = run_cli(args).await { - eprintln!("tui-zone command failed: {error}"); - std::process::exit(1); - } -} diff --git a/deployment/tui-zone/src/message.rs b/deployment/tui-zone/src/message.rs deleted file mode 100644 index 3f8fcb949..000000000 --- a/deployment/tui-zone/src/message.rs +++ /dev/null @@ -1,61 +0,0 @@ -use lb_core::mantle::ops::channel::MsgId; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// Application-level message wrapper. `tx_uuid` ensures unique payload to -/// avoid mempool deduplication even with same signing keys in decentralized -/// scenarios. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AppMessage { - /// Random transaction UUID used to make otherwise identical payloads - /// unique. - pub tx_uuid: Uuid, - /// User-entered message text. - pub text: String, -} - -impl AppMessage { - /// Create a new application message with a fresh UUID. - #[must_use] - pub fn new(text: String) -> Self { - Self { - tx_uuid: Uuid::new_v4(), - text, - } - } - - /// Serialize the message to bytes for inscription payloads. - #[must_use] - pub fn to_bytes(&self) -> Vec { - serde_json::to_vec(self).expect("AppMessage serialization should not fail") - } - - /// Deserialize an application message from inscription payload bytes. - #[must_use] - pub fn from_bytes(bytes: &[u8]) -> Option { - serde_json::from_slice(bytes).ok() - } -} - -/// A single message tracked by the TUI. -/// -/// `msg_id` is SDK-provided lineage anchor; `text` is the app-level text -/// extracted from the JSON-encoded payload (falls back to raw UTF-8 for -/// payloads that didn't come from this TUI). -#[derive(Debug, Clone)] -pub struct Msg { - /// SDK message lineage identifier. - pub msg_id: MsgId, - /// Text rendered in the TUI. - pub text: String, -} - -impl Msg { - /// Build a rendered message from an inscription payload. - #[must_use] - pub fn from_payload(msg_id: MsgId, payload: &[u8]) -> Self { - let text = AppMessage::from_bytes(payload) - .map_or_else(|| String::from_utf8_lossy(payload).into_owned(), |m| m.text); - Self { msg_id, text } - } -} diff --git a/deployment/tui-zone/src/run_commands/driver.rs b/deployment/tui-zone/src/run_commands/driver.rs deleted file mode 100644 index 2c7996c8b..000000000 --- a/deployment/tui-zone/src/run_commands/driver.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::time::Duration; - -use lb_core::mantle::{ - Value, - ledger::Inputs, - ops::channel::{ChannelId, deposit::Metadata, withdraw::ChannelWithdrawOp}, - transactions::hash::TxHash, -}; -use lb_zone_sdk::{ - adapter::NodeHttpClient, - sequencer::{Event, FinalizedOp, FinalizedTx, TxStatus, TxStatusUpdate, ZoneSequencer}, -}; -use tokio::{select, sync::broadcast, time::sleep}; - -use crate::{ - cli::RunResult, - run_commands::utils::{save_cli_checkpoint, timestamp}, -}; - -const COMMAND_FINALITY_TIMEOUT: Duration = Duration::from_mins(5); - -/// Command verification depth. -#[derive(Clone, Copy)] -pub enum WaitFor { - /// Return once the transaction is observed on the local canonical chain. - OnChain, - /// Return only once the command goal is finalized. - Finalized, -} - -/// Finalization target for a non-interactive sequencer command. -pub enum CommandGoal { - /// Wait until the transaction hash appears in finalized chain history. - Tx { tx_hash: TxHash }, - /// Wait until the expected deposit op appears in finalized channel ops. - Deposit { - /// Signed transaction hash that carried the deposit. - tx_hash: TxHash, - /// Deposit note inputs. - inputs: Inputs, - /// Deposited amount. - amount: Value, - /// Deposit metadata. - metadata: Metadata, - }, - /// Wait until all expected withdraw ops appear in the finalized tx. - Withdraw { - /// Signed transaction hash that carried the withdraw ops. - tx_hash: TxHash, - /// Expected withdraw ops. - withdraws: Vec, - }, -} - -impl CommandGoal { - const fn tx_hash(&self) -> TxHash { - match self { - Self::Tx { tx_hash } - | Self::Deposit { tx_hash, .. } - | Self::Withdraw { tx_hash, .. } => *tx_hash, - } - } -} - -/// Drive a sequencer until the requested command goal reaches the wait target. -pub async fn drive_until_observed( - channel_id: &ChannelId, - sequencer: &mut ZoneSequencer, - mut status_rx: broadcast::Receiver, - goal: CommandGoal, - wait_for: WaitFor, - label: &str, -) -> RunResult<()> { - let tx_hash = goal.tx_hash(); - let timeout = sleep(COMMAND_FINALITY_TIMEOUT); - tokio::pin!(timeout); - let mut observed_on_chain = false; - - loop { - select! { - () = &mut timeout => { - return Err(format!( - "{} {label}: verification timeout tx_hash={}", - timestamp(), - hex::encode(tx_hash.as_ref()) - ).into()); - } - update = status_rx.recv() => match update { - Ok(update) if update.tx_hash == tx_hash => { - print_status(label, update); - if matches!(update.status, TxStatus::Orphaned(_)) { - return Err(format!( - "{} {label}: orphaned tx_hash={}", - timestamp(), - hex::encode(tx_hash.as_ref()) - ).into()); - } - if matches!(update.status, TxStatus::OnChain(_)) - && matches!(wait_for, WaitFor::OnChain) - { - observed_on_chain = true; - } - if matches!(update.status, TxStatus::Finalized(_)) - && matches!(goal, CommandGoal::Tx { .. }) - { - return Ok(()); - } - } - Ok(_) => {} - Err(broadcast::error::RecvError::Lagged(skipped)) => { - println!( - "{} {label}: verification lagged skipped_updates={skipped}", - timestamp() - ); - } - Err(broadcast::error::RecvError::Closed) => { - return Err("sequencer tx-status stream closed".into()); - } - }, - event = sequencer.next_event() => { - if let Event::BlocksProcessed { checkpoint, finalized, .. } = event { - save_cli_checkpoint(channel_id, &checkpoint)?; - if observed_on_chain { - return Ok(()); - } - if finalized_goal_matches(&goal, &finalized) { - println!( - "{} {label}: finalized tx_hash={}", - timestamp(), - hex::encode(tx_hash.as_ref()) - ); - return Ok(()); - } - } - } - } - } -} - -fn print_status(label: &str, update: TxStatusUpdate) { - println!( - "{} {label}: verification tx_hash={} status={:?}", - timestamp(), - hex::encode(update.tx_hash.as_ref()), - update.status - ); -} - -fn finalized_goal_matches(goal: &CommandGoal, finalized: &[FinalizedTx]) -> bool { - finalized.iter().any(|tx| match goal { - CommandGoal::Tx { tx_hash } => tx.tx_hash == *tx_hash, - CommandGoal::Deposit { - tx_hash, - inputs, - amount, - metadata, - } => { - tx.tx_hash == *tx_hash - && tx.ops.iter().any(|op| { - matches!(op, FinalizedOp::Deposit(deposit) - if deposit.inputs == *inputs - && deposit.amount == *amount - && deposit.metadata == *metadata) - }) - } - CommandGoal::Withdraw { tx_hash, withdraws } => { - tx.tx_hash == *tx_hash - && withdraws.iter().all(|expected| { - tx.ops.iter().any(|op| { - matches!(op, FinalizedOp::Withdraw(withdraw) - if withdraw.op.channel_id == expected.channel_id - && withdraw.op.inputs == expected.inputs) - }) - }) - } - }) -} diff --git a/deployment/tui-zone/src/run_commands/mod.rs b/deployment/tui-zone/src/run_commands/mod.rs deleted file mode 100644 index 7d4157dfa..000000000 --- a/deployment/tui-zone/src/run_commands/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -/// The TUI sequencer command runner module. -mod driver; -/// Channel balance query command runner. -pub mod run_balance; -/// Channel configuration command runner. -pub mod run_config; -/// Deposit command runner. -pub mod run_deposit; -/// Interactive inscription command runner. -pub mod run_inscribe; -/// Local sequencer key generation command runner. -pub mod run_keygen; -/// Withdrawal command runners. -pub mod run_withdraw; -/// Transaction signing command runner. -pub(crate) mod types; -/// Utility functions for the TUI sequencer command runners. -mod utils; - -#[cfg(test)] -mod unit_tests; - -/// The TUI sequencer prefix for all config intent files to ensure they are -/// easily identifiable. -pub const ZONE_CONFIG_INTENT: &str = "zone_config_intent"; -/// The TUI sequencer prefix for all config signature files to ensure they are -/// easily identifiable. -pub const ZONE_CONFIG_SIGNATURE: &str = "zone_config_signature"; -/// The TUI sequencer prefix for all signed config transaction files to ensure -/// they are easily identifiable. -pub const ZONE_SIGNED_CONFIG: &str = "zone_signed_config"; - -/// The TUI sequencer prefix for all withdraw intent files to ensure they are -/// easily identifiable. -pub const ZONE_WITHDRAW_INTENT: &str = "zone_withdraw_intent"; - -/// The TUI sequencer prefix for all withdraw signature files to ensure they are -/// easily identifiable. -pub const ZONE_WITHDRAW_SIGNATURE: &str = "zone_withdraw_signature"; -/// The TUI sequencer prefix for all signed transaction files to ensure they are -/// easily identifiable. -pub const ZONE_SIGNED_TRANSACTION: &str = "zone_signed_transaction"; -/// The TUI sequencer prefix for all funds export files to ensure they are -/// easily identifiable. -pub const ZONE_WALLET_FUNDS_EXPORT: &str = "zone_wallet_funds_export"; -/// The TUI sequencer file transfer version to ensure compatibility of transfer -/// files. -pub const ZONE_FILE_TRANSFER_VERSION: u8 = 1; diff --git a/deployment/tui-zone/src/run_commands/run_balance.rs b/deployment/tui-zone/src/run_commands/run_balance.rs deleted file mode 100644 index d0baad11f..000000000 --- a/deployment/tui-zone/src/run_commands/run_balance.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{ - cli::{RunResult, StateArgs}, - run_commands::utils::{ - node_client, print_channel_state, query_channel_state, resolve_channel_id, - }, -}; - -// TODO: support channel note tracking in the TUI. A channel's balance is now -// the sum of its channel notes rather than a field on `ChannelState`. -// pub(crate) async fn run_state_balance(args: StateArgs) -> RunResult<()> { -// let channel_id = resolve_channel_id(&args.node_key)?; -// let node = node_client(&args.node_key.node_url)?; -// let channel_state = query_channel_state(&node, channel_id).await; -// print_channel_balance("balance", &channel_id, channel_state.as_ref()); -// Ok(()) -// } - -pub(crate) async fn run_state_full(args: StateArgs) -> RunResult<()> { - let channel_id = resolve_channel_id(&args.node_key)?; - let node = node_client(&args.node_key.node_url)?; - let channel_state = query_channel_state(&node, channel_id).await; - print_channel_state("state", &channel_id, channel_state.as_ref()); - Ok(()) -} diff --git a/deployment/tui-zone/src/run_commands/run_config.rs b/deployment/tui-zone/src/run_commands/run_config.rs deleted file mode 100644 index fdc229a2c..000000000 --- a/deployment/tui-zone/src/run_commands/run_config.rs +++ /dev/null @@ -1,409 +0,0 @@ -use std::path::PathBuf; - -use lb_codec::BinaryEncode as _; -use lb_core::{ - mantle::{ - Op, OpProof, SignedMantleTx, - ops::channel::{ - ChannelId, ChannelKeyIndex, MsgId, - config::{ChannelConfigOp, Keys}, - }, - traits::Hashable as _, - transactions::{codec::encode_signed_mantle_tx, mantle_tx::MantleTx as _}, - }, - proofs::channel_multi_sig_proof::{ChannelMultiSigProof, IndexedSignature}, -}; -use lb_key_management_system_service::keys::{Ed25519PublicKey, Ed25519Signature}; - -use crate::{ - cli::{ - ConfigArgs, ConfigCombineArgs, ConfigPrepareArgs, ConfigSignArgs, ConfigSubmitArgs, - RunResult, - }, - run_commands::{ - ZONE_CONFIG_INTENT, ZONE_CONFIG_SIGNATURE, ZONE_FILE_TRANSFER_VERSION, ZONE_SIGNED_CONFIG, - driver::{CommandGoal, WaitFor, drive_until_observed}, - types::{ - AuthorizedSigner, ConfigIntent, ConfigSignatureFile, SignedConfigFile, - WithdrawSignatureEntry, - }, - utils::{ - decode_ed25519_public_key_hex, decode_hex_bincode, decode_mantle_tx_hex, - decode_signed_mantle_tx_hex, encode_hex_bincode, ensure_tx_hash, fixed_bytes, - load_or_create_signing_key, node_client, print_channel_state, query_channel_state, - read_json, resolve_channel_id, start_cli_sequencer_with_channel_state, timestamp, - validate_kind, write_json, - }, - }, -}; - -pub(crate) async fn run_config(args: ConfigArgs) -> RunResult<()> { - let authorized_keys = - authorized_keys_for_paths(&args.node_key.key_path, &args.authorized_key_paths); - validate_config_thresholds( - args.configuration_threshold, - args.transfer_threshold, - authorized_keys.len(), - )?; - - let channel_id = resolve_channel_id(&args.node_key)?; - let (mut sequencer, channel_state) = - start_cli_sequencer_with_channel_state(&args.node_key).await?; - print_channel_state("zone_config before", &channel_id, channel_state.as_ref()); - let status_rx = sequencer.subscribe_tx_status(); - let (_receipt, signed_tx) = sequencer - .handle() - .channel_config( - Keys::try_from(authorized_keys)?, - args.posting_timeframe.into(), - args.posting_timeout.into(), - args.configuration_threshold, - args.transfer_threshold, - ) - .await?; - let tx_hash = signed_tx.hash(); - let goal = CommandGoal::Tx { tx_hash }; - let wait_for = if args.wait_finalized { - WaitFor::Finalized - } else { - WaitFor::OnChain - }; - drive_until_observed( - &channel_id, - &mut sequencer, - status_rx, - goal, - wait_for, - "zone_config", - ) - .await?; - let node = node_client(&args.node_key.node_url)?; - let channel_state = query_channel_state(&node, channel_id).await; - print_channel_state("zone_config after", &channel_id, channel_state.as_ref()); - Ok(()) -} - -pub(crate) async fn run_config_prepare(args: ConfigPrepareArgs) -> RunResult<()> { - let authorized_keys = - authorized_keys_for_paths(&args.node_key.key_path, &args.authorized_key_paths); - validate_config_thresholds( - args.configuration_threshold, - args.transfer_threshold, - authorized_keys.len(), - )?; - let channel_id = resolve_channel_id(&args.node_key)?; - let (_sequencer, channel_state) = - start_cli_sequencer_with_channel_state(&args.node_key).await?; - let channel_state = - channel_state.ok_or_else(|| format!("channel state not found for {channel_id}"))?; - let config_op = build_config_op( - channel_id, - channel_state.config_tip_hash, - authorized_keys.clone(), - args.posting_timeframe, - args.posting_timeout, - args.configuration_threshold, - args.transfer_threshold, - )?; - let config_id = config_op.id(); - let tx = lb_core::mantle::RawMantleTx([Op::ChannelConfig(config_op)].into()); - let tx_hash = tx.hash(); - let intent = ConfigIntent { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_CONFIG_INTENT.to_owned(), - channel_id: hex::encode(channel_id.as_ref()), - tx_hash: hex::encode(tx_hash.as_ref()), - config_id: hex::encode(config_id.as_ref()), - required_threshold: channel_state.configuration_threshold, - mantle_tx: hex::encode(tx.encode()), - new_authorized_keys: authorized_keys - .iter() - .map(|key| hex::encode(key.to_bytes())) - .collect(), - configuration_threshold: args.configuration_threshold, - transfer_threshold: args.transfer_threshold, - posting_timeframe: args.posting_timeframe, - posting_timeout: args.posting_timeout, - authorized_signers: channel_state - .accredited_keys - .iter() - .enumerate() - .map(|(index, key)| AuthorizedSigner { - key_index: index as ChannelKeyIndex, - public_key: hex::encode(key.to_bytes()), - }) - .collect(), - signatures: Vec::new(), - }; - write_json(&args.out, &intent)?; - println!( - "{} zone_config: intent tx_hash={} config_id={} required_threshold={}", - timestamp(), - intent.tx_hash, - intent.config_id, - intent.required_threshold - ); - Ok(()) -} - -pub(crate) fn run_config_sign(args: &ConfigSignArgs) -> RunResult<()> { - let intent = read_json::(&args.input)?; - validate_kind(&intent.kind, ZONE_CONFIG_INTENT, intent.version)?; - let tx = decode_mantle_tx_hex(&intent.mantle_tx)?; - let tx_hash = tx.hash(); - ensure_tx_hash(&intent.tx_hash, tx_hash)?; - let signing_key = load_or_create_signing_key(PathBuf::from(&args.key_path).as_path()); - let public_key = signing_key.public_key(); - let signer = intent - .authorized_signers - .iter() - .find(|signer| signer.public_key == hex::encode(public_key.to_bytes())) - .ok_or_else(|| { - "signing key is not listed in config intent authorized_signers".to_owned() - })?; - let signature = signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()); - let signature_file = ConfigSignatureFile { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_CONFIG_SIGNATURE.to_owned(), - channel_id: intent.channel_id, - tx_hash: intent.tx_hash, - signer_public_key: signer.public_key.clone(), - signer_key_index: signer.key_index, - signature: encode_hex_bincode(&signature)?, - }; - write_json(&args.out, &signature_file)?; - println!( - "{} zone_config: signature tx_hash={} signer_key_index={}", - timestamp(), - signature_file.tx_hash, - signature_file.signer_key_index - ); - Ok(()) -} - -pub(crate) fn run_config_combine(args: ConfigCombineArgs) -> RunResult<()> { - let intent = read_json::(&args.input)?; - validate_kind(&intent.kind, ZONE_CONFIG_INTENT, intent.version)?; - let tx = decode_mantle_tx_hex(&intent.mantle_tx)?; - let tx_hash = tx.hash(); - ensure_tx_hash(&intent.tx_hash, tx_hash)?; - validate_config_tx(&tx, &intent)?; - let mut signature_entries = intent.signatures.clone(); - for path in args.sig { - let sig = read_json::(&path)?; - validate_kind(&sig.kind, ZONE_CONFIG_SIGNATURE, sig.version)?; - if sig.channel_id != intent.channel_id { - return Err(format!( - "signature '{}' channel_id {} does not match intent {}", - path.display(), - sig.channel_id, - intent.channel_id - ) - .into()); - } - if sig.tx_hash != intent.tx_hash { - return Err(format!( - "signature '{}' tx_hash {} does not match intent {}", - path.display(), - sig.tx_hash, - intent.tx_hash - ) - .into()); - } - let public_key = decode_ed25519_public_key_hex(&sig.signer_public_key)?; - validate_authorized_signer(&intent, sig.signer_key_index, &sig.signer_public_key)?; - let signature = decode_hex_bincode::(&sig.signature)?; - public_key.verify(tx_hash.as_signing_bytes().as_ref(), &signature)?; - signature_entries.push(WithdrawSignatureEntry { - signer_key_index: sig.signer_key_index, - signer_public_key: sig.signer_public_key, - signature: sig.signature, - }); - } - let proof = ChannelMultiSigProof::try_new( - signature_entries - .iter() - .map(|sig| { - validate_authorized_signer(&intent, sig.signer_key_index, &sig.signer_public_key)?; - decode_hex_bincode::(&sig.signature) - .map(|signature| IndexedSignature::new(sig.signer_key_index, signature)) - }) - .collect::>>()? - .try_into()?, - )?; - if proof.signatures().len() != intent.required_threshold as usize { - return Err(format!( - "config combine requires exactly {} unique authorized signature(s), got {}", - intent.required_threshold, - proof.signatures().len() - ) - .into()); - } - let signed_tx = SignedMantleTx::new(tx, [OpProof::ChannelMultiSigProof(proof)].into()); - let signed = SignedConfigFile { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_SIGNED_CONFIG.to_owned(), - channel_id: intent.channel_id, - tx_hash: intent.tx_hash, - config_id: intent.config_id, - signed_mantle_tx: hex::encode(encode_signed_mantle_tx(&signed_tx)), - signatures: signature_entries, - }; - write_json(&args.out, &signed)?; - println!( - "{} zone_config: signed tx_hash={} config_id={}", - timestamp(), - signed.tx_hash, - signed.config_id - ); - Ok(()) -} - -pub(crate) async fn run_config_submit(args: ConfigSubmitArgs) -> RunResult<()> { - let signed = read_json::(&args.input)?; - validate_kind(&signed.kind, ZONE_SIGNED_CONFIG, signed.version)?; - let signed_tx = decode_signed_mantle_tx_hex(&signed.signed_mantle_tx)?; - let tx_hash = signed_tx.hash(); - ensure_tx_hash(&signed.tx_hash, tx_hash)?; - let channel_id = ChannelId::from(fixed_bytes::<32>(&signed.channel_id)?); - if let Some(requested_channel_id) = args.node_key.channel_id.as_deref() - && signed.channel_id != requested_channel_id - { - return Err(format!( - "signed config channel_id {} does not match requested channel_id {}", - signed.channel_id, requested_channel_id - ) - .into()); - } - let mut node_key = args.node_key; - node_key.channel_id = Some(signed.channel_id.clone()); - let (mut sequencer, channel_state) = start_cli_sequencer_with_channel_state(&node_key).await?; - print_channel_state("zone_config before", &channel_id, channel_state.as_ref()); - let status_rx = sequencer.subscribe_tx_status(); - let goal = CommandGoal::Tx { tx_hash }; - let tip_message = sequencer - .checkpoint() - .map_or_else(MsgId::root, |checkpoint| checkpoint.last_msg_id); - let (_result, _checkpoint) = sequencer - .handle() - .submit_signed_tx(signed_tx, tip_message)?; - println!( - "{} zone_config: submitted tx_hash={} config_id={}", - timestamp(), - signed.tx_hash, - signed.config_id - ); - let wait_for = if args.wait_finalized { - WaitFor::Finalized - } else { - WaitFor::OnChain - }; - drive_until_observed( - &channel_id, - &mut sequencer, - status_rx, - goal, - wait_for, - "zone_config", - ) - .await?; - let node = node_client(&node_key.node_url)?; - let channel_state = query_channel_state(&node, channel_id).await; - print_channel_state("zone_config after", &channel_id, channel_state.as_ref()); - Ok(()) -} - -fn validate_config_thresholds( - configuration_threshold: u16, - transfer_threshold: u16, - authorized_key_count: usize, -) -> RunResult<()> { - if transfer_threshold == 0 { - return Err("transfer_threshold must be greater than 0".into()); - } - if configuration_threshold == 0 { - return Err("configuration_threshold must be greater than 0".into()); - } - if transfer_threshold as usize > authorized_key_count { - return Err(format!( - "transfer_threshold {transfer_threshold} exceeds authorized key count {authorized_key_count}" - ) - .into()); - } - if configuration_threshold as usize > authorized_key_count { - return Err(format!( - "configuration_threshold {configuration_threshold} exceeds authorized key count {authorized_key_count}" - ) - .into()); - } - Ok(()) -} - -fn authorized_keys_for_paths( - admin_key_path: &str, - authorized_key_paths: &[String], -) -> Vec { - let admin_key = load_or_create_signing_key(PathBuf::from(admin_key_path).as_path()); - let mut authorized_keys = vec![admin_key.public_key()]; - for key_path in authorized_key_paths { - let public_key = load_or_create_signing_key(PathBuf::from(key_path).as_path()).public_key(); - if !authorized_keys.contains(&public_key) { - authorized_keys.push(public_key); - } - } - authorized_keys -} - -fn build_config_op( - channel_id: ChannelId, - parent: MsgId, - authorized_keys: Vec, - posting_timeframe: u32, - posting_timeout: u32, - configuration_threshold: u16, - transfer_threshold: u16, -) -> RunResult { - Ok(ChannelConfigOp { - channel: channel_id, - parent, - keys: Keys::try_from(authorized_keys)?, - posting_timeframe: posting_timeframe.into(), - posting_timeout: posting_timeout.into(), - configuration_threshold, - transfer_threshold, - }) -} - -fn validate_authorized_signer( - intent: &ConfigIntent, - signer_key_index: ChannelKeyIndex, - signer_public_key: &str, -) -> RunResult<()> { - if intent.authorized_signers.iter().any(|signer| { - signer.key_index == signer_key_index && signer.public_key == signer_public_key - }) { - return Ok(()); - } - Err(format!( - "signature signer key_index={signer_key_index} public_key={signer_public_key} is not listed in config intent authorized_signers" - ) - .into()) -} - -fn validate_config_tx(tx: &lb_core::mantle::RawMantleTx, intent: &ConfigIntent) -> RunResult<()> { - let config = tx - .ops() - .iter() - .find_map(|op| match op { - Op::ChannelConfig(config) => Some(config), - _ => None, - }) - .ok_or("config intent transaction has no ChannelConfig op")?; - if hex::encode(config.id().as_ref()) != intent.config_id { - return Err("config intent config_id does not match ChannelConfig op id".into()); - } - if tx.ops().len() != 1 { - return Err("config intent transaction must contain exactly one op".into()); - } - Ok(()) -} diff --git a/deployment/tui-zone/src/run_commands/run_deposit.rs b/deployment/tui-zone/src/run_commands/run_deposit.rs deleted file mode 100644 index e087c3b8a..000000000 --- a/deployment/tui-zone/src/run_commands/run_deposit.rs +++ /dev/null @@ -1,94 +0,0 @@ -use lb_core::mantle::{ - Op, OpProof, SignedMantleTx, ops::channel::inscribe::Inscription, traits::Hashable as _, -}; -use lb_key_management_system_service::keys::ZkKey; - -use crate::{ - cli::{DepositArgs, RunResult}, - run_commands::{ - ZONE_WALLET_FUNDS_EXPORT, - driver::{CommandGoal, WaitFor, drive_until_observed}, - types::WalletFundsExport, - utils::{ - build_deposit_op, build_deposit_transfer, decode_exported_utxos, - decode_required_hex_bincode, read_json, resolve_channel_id, - start_cli_sequencer_with_channel_state, timestamp, validate_kind, - }, - }, -}; - -pub(crate) async fn run_deposit(args: DepositArgs) -> RunResult<()> { - let funds = read_json::(&args.funds)?; - validate_kind(&funds.kind, ZONE_WALLET_FUNDS_EXPORT, funds.version)?; - let wallet_key = decode_required_hex_bincode::( - funds.secret_key.as_deref(), - "funds JSON is missing secret_key; rerun EXPORT_FUNDS with include_secret true", - )?; - let funding_public_key = wallet_key.to_public_key(); - let available_utxos = decode_exported_utxos(&funds)?; - let (transfer, _reserved_inputs) = - build_deposit_transfer(available_utxos, funding_public_key, args.amount)?; - let channel_id = resolve_channel_id(&args.node_key)?; - let deposit = build_deposit_op(channel_id, &transfer, &args.metadata)?; - let inscription = Inscription::try_from(args.message.into_bytes())?; - // TODO: report the channel balance again once channel notes are tracked. - let (mut sequencer, _channel_state) = - start_cli_sequencer_with_channel_state(&args.node_key).await?; - let status_rx = sequencer.subscribe_tx_status(); - let goal_inputs = deposit.inputs.clone(); - let goal_metadata = deposit.metadata.clone(); - let (tx, msg_id, sequencer_sig) = sequencer.handle().prepare_tx( - [Op::Transfer(transfer), Op::ChannelDeposit(deposit)].into(), - inscription, - )?; - let user_sig = ZkKey::multi_sign(&[wallet_key], &tx.hash().to_fr())?; - let signed_tx = SignedMantleTx::new( - tx, - [ - OpProof::ZkSig(user_sig.clone()), - OpProof::ZkSig(user_sig), - OpProof::Ed25519Sig(sequencer_sig), - ] - .into(), - ); - let tx_hash = signed_tx.hash(); - - let goal = CommandGoal::Deposit { - tx_hash, - inputs: goal_inputs, - amount: args.amount, - metadata: goal_metadata, - }; - let _receipt = sequencer.handle().submit_signed_tx(signed_tx, msg_id)?; - println!( - "{} deposit: submitted tx_hash={} msg_id={}", - timestamp(), - hex::encode(tx_hash.as_ref()), - hex::encode(msg_id.as_ref()) - ); - let wait_for = if args.wait_finalized { - WaitFor::Finalized - } else { - WaitFor::OnChain - }; - drive_until_observed( - &channel_id, - &mut sequencer, - status_rx, - goal, - wait_for, - "deposit", - ) - .await?; - - // TODO: report the channel balance again once channel notes are tracked. It - // is now the sum of the channel's notes rather than a field on - // `ChannelState`. - println!( - "{} deposit: tx_hash={} msg_id={}", - timestamp(), - hex::encode(tx_hash.as_ref()), - hex::encode(msg_id.as_ref()) - ); - Ok(()) -} diff --git a/deployment/tui-zone/src/run_commands/run_inscribe.rs b/deployment/tui-zone/src/run_commands/run_inscribe.rs deleted file mode 100644 index cfb370ebb..000000000 --- a/deployment/tui-zone/src/run_commands/run_inscribe.rs +++ /dev/null @@ -1,253 +0,0 @@ -use std::{collections::HashSet, path::Path}; - -use lb_core::mantle::ops::channel::inscribe::Inscription; -use lb_zone_sdk::{ - CommonHttpClient, - adapter::NodeHttpClient, - sequencer::{ - ChannelUpdate, ChannelUpdateTx, Event, FinalizedOp, FinalizedTx, InscriptionInfo, - ZoneSequencer, - }, -}; -use reqwest::Url; -use tokio::sync::mpsc; -use tracing::{debug, error, info, warn}; - -use crate::{ - cli::NodeKeyArgs, - message::AppMessage, - run_commands::utils, - state::{InMemoryZoneState, ZoneState as _}, - ui, -}; - -#[expect( - clippy::cognitive_complexity, - reason = "TODO: address this in a dedicated refactor" -)] -/// Run the interactive inscription TUI. -pub async fn run_inscribe(args: NodeKeyArgs) { - let node_url: Url = args.node_url.parse().expect("invalid node URL"); - let signing_key = utils::load_or_create_signing_key(Path::new(&args.key_path)); - let channel_id = utils::resolve_channel_id(&args).expect("invalid channel ID"); - - println!("TUI Zone Sequencer"); - println!(" Node: {node_url}"); - println!(" Key: {}", args.key_path); - println!(" Channel ID: {}", hex::encode(channel_id.as_ref())); - println!(); - - let node = NodeHttpClient::new(CommonHttpClient::new(None), node_url); - let channel_exists = utils::query_channel_exists(&node, channel_id).await; - let mut state = - InMemoryZoneState::for_channel(channel_id, channel_exists).expect("invalid checkpoint"); - let checkpoint = state.load_checkpoint().cloned(); - - let sequencer_config = utils::cli_sequencer_config(&args).expect("invalid funding pk"); - let mut sequencer = ZoneSequencer::init_with_config( - channel_id, - signing_key, - node, - sequencer_config, - checkpoint, - ); - let view_rx = sequencer.subscribe_channel_view(); - - let (ready_tx, ready_rx) = tokio::sync::oneshot::channel(); - let mut stdin_rx = spawn_stdin_reader(ready_rx); - let mut ready_tx = Some(ready_tx); - - println!("Bootstrapping sequencer..."); - - loop { - tokio::select! { - event = sequencer.next_event() => { - state.set_channel_view(view_rx.borrow().clone()); - handle_event(event, &mut state, &mut sequencer, &mut ready_tx).await; - } - - input = stdin_rx.recv() => { - let Some(text) = input else { - println!(); - break; - }; - - let msg = AppMessage::new(text); - debug!(tx_uuid = %msg.tx_uuid, text = %msg.text, "Publishing message"); - let Ok(inscription) = Inscription::try_from(msg.to_bytes()) else { - error!("Message is too large to fit in an inscription"); - continue; - }; - match sequencer.handle().publish(inscription).await { - Ok((result, checkpoint)) => { - let info = result.tx.inscription(); - debug!(msg_id = %hex::encode(info.this_msg.as_ref()), "Published"); - state.on_published(info); - state.save_checkpoint(checkpoint); - ui::render_state(&state); - eprintln!(" \x1b[90mpending...\x1b[0m"); - ui::prompt(); - } - Err(lb_zone_sdk::sequencer::Error::Unavailable { reason }) => { - warn!("publish rejected: {reason}"); - eprintln!( - " \x1b[33msequencer is still starting up, try again in a moment\x1b[0m" - ); - ui::prompt(); - } - Err(e) => { - error!("failed to publish: {e}"); - break; - } - } - } - - _ = tokio::signal::ctrl_c() => { - println!(); - break; - } - } - } - - println!("Goodbye!"); -} - -async fn handle_event( - event: Event, - state: &mut InMemoryZoneState, - sequencer: &mut ZoneSequencer, - ready_tx: &mut Option>, -) { - match event { - Event::Ready => handle_ready(state, ready_tx), - Event::BlocksProcessed { - checkpoint, - channel_update, - finalized, - } => { - // Pin finalized payloads before the re-publish dedup below. - apply_finalized(&finalized, state); - apply_channel_update(channel_update, state, sequencer).await; - state.save_checkpoint(checkpoint); - } - Event::MempoolPending(..) | Event::TurnNotification { .. } => {} - } -} - -fn apply_finalized(items: &[FinalizedTx], state: &mut InMemoryZoneState) { - // TUI only cares about inscriptions for rendering; deposit / withdraw - // ops have no inscription payload. - let inscriptions: Vec = items - .iter() - .flat_map(|t| t.ops.iter()) - .filter_map(|op| match op { - FinalizedOp::Inscription(i) => Some(i.clone()), - FinalizedOp::Deposit(_) - | FinalizedOp::Withdraw(_) - | FinalizedOp::Config(_) - | FinalizedOp::ChannelTransfer(_) => None, - }) - .collect(); - if inscriptions.is_empty() { - return; - } - state.on_finalized(&inscriptions); - ui::render_state(state); - ui::prompt(); -} - -async fn apply_channel_update( - update: ChannelUpdate, - state: &mut InMemoryZoneState, - sequencer: &mut ZoneSequencer, -) { - let ChannelUpdate { orphaned, adopted } = update; - if orphaned.is_empty() { - return; - } - // Dedup by payload (carries a unique tx_uuid): an orphan already back on - // the channel reappears in `adopted`, so don't republish it. - let adopted_payloads: HashSet<&[u8]> = adopted - .iter() - .filter_map(|tx| tx.inscription().map(|i| i.payload.as_slice())) - .collect(); - for entry in &orphaned { - handle_orphan(state, sequencer, entry, &adopted_payloads).await; - } -} - -async fn handle_orphan( - state: &mut InMemoryZoneState, - sequencer: &mut ZoneSequencer, - entry: &ChannelUpdateTx, - adopted_payloads: &HashSet<&[u8]>, -) { - let ChannelUpdateTx::Inscription(info) = entry else { - debug!("ignoring atomic-withdraw orphan; TUI does not publish bundles"); - return; - }; - if adopted_payloads.contains(info.payload.as_slice()) { - debug!(msg_id = %hex::encode(info.this_msg.as_ref()), "orphan already on channel; not republishing"); - return; - } - if state.is_finalized(info.payload.as_slice()) { - debug!(msg_id = %hex::encode(info.this_msg.as_ref()), "orphan already finalized; not republishing"); - return; - } - republish_orphan(state, sequencer, info).await; -} - -async fn republish_orphan( - state: &mut InMemoryZoneState, - sequencer: &mut ZoneSequencer, - info: &InscriptionInfo, -) { - debug!(msg_id = %hex::encode(info.this_msg.as_ref()), "Auto-republishing orphan"); - match sequencer.handle().publish(info.payload.clone()).await { - Ok((_, checkpoint)) => state.save_checkpoint(checkpoint), - Err(e) => error!("failed to auto-republish: {e}"), - } -} - -fn handle_ready( - state: &InMemoryZoneState, - ready_tx: &mut Option>, -) { - info!("Sequencer ready"); - if let Some(tx) = ready_tx.take() { - let _ = tx.send(()); - } - println!("Ready."); - println!(); - println!("Type a message and press Enter to publish."); - println!("Press Ctrl-D or type an empty line to exit."); - println!(); - ui::render_state(state); - ui::prompt(); -} - -fn spawn_stdin_reader(ready: tokio::sync::oneshot::Receiver<()>) -> mpsc::Receiver { - let (tx, rx) = mpsc::channel(16); - std::thread::spawn(move || { - // Wait until the sequencer is ready before accepting input - if ready.blocking_recv().is_err() { - return; - } - - let stdin = std::io::stdin(); - let mut line = String::new(); - loop { - line.clear(); - match stdin.read_line(&mut line) { - Ok(0) | Err(_) => break, - Ok(_) => { - let text = line.trim_end().to_owned(); - if text.is_empty() || tx.blocking_send(text).is_err() { - break; - } - } - } - } - }); - rx -} diff --git a/deployment/tui-zone/src/run_commands/run_keygen.rs b/deployment/tui-zone/src/run_commands/run_keygen.rs deleted file mode 100644 index 0a4f131fe..000000000 --- a/deployment/tui-zone/src/run_commands/run_keygen.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::path::Path; - -use crate::{cli::KeygenArgs, run_commands::utils::load_or_create_signing_key}; - -/// Create or load a local sequencer signing key and print its public key. -pub(crate) fn run_keygen(args: &KeygenArgs) { - let key = load_or_create_signing_key(Path::new(&args.key_path)); - println!("key_path={}", args.key_path); - println!("public_key={}", hex::encode(key.public_key().to_bytes())); -} diff --git a/deployment/tui-zone/src/run_commands/run_withdraw.rs b/deployment/tui-zone/src/run_commands/run_withdraw.rs deleted file mode 100644 index 310930e4c..000000000 --- a/deployment/tui-zone/src/run_commands/run_withdraw.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::path::PathBuf; - -use lb_core::{ - mantle::{ - Op, OpProof, SignedMantleTx, - ops::channel::{ChannelId, ChannelKeyIndex}, - traits::Hashable as _, - transactions::{codec::encode_signed_mantle_tx, mantle_tx::MantleTx as _}, - }, - proofs::channel_multi_sig_proof::{ChannelMultiSigProof, IndexedSignature}, -}; -use lb_key_management_system_service::keys::Ed25519Signature; - -pub(crate) use crate::{ - cli::{ - RunResult, WithdrawCombineArgs, WithdrawPrepareArgs, WithdrawSignArgs, WithdrawSubmitArgs, - }, - run_commands::{ - ZONE_FILE_TRANSFER_VERSION, ZONE_SIGNED_TRANSACTION, ZONE_WITHDRAW_INTENT, - ZONE_WITHDRAW_SIGNATURE, - driver::{CommandGoal, WaitFor, drive_until_observed}, - types::{ - SignedWithdrawFile, WithdrawIntent, WithdrawSignatureEntry, WithdrawSignatureFile, - }, - utils::{ - decode_ed25519_public_key_hex, decode_hex_bincode, decode_mantle_tx_hex, - decode_msg_id_hex, decode_signed_mantle_tx_hex, encode_hex_bincode, ensure_tx_hash, - fixed_bytes, load_or_create_signing_key, read_json, start_cli_sequencer, timestamp, - validate_kind, write_json, - }, - }, -}; - -// TODO: rebuild withdraw prepare on CHANNEL_TRANSFER + CHANNEL_WITHDRAW. A -// withdraw now only releases an existing channel note to the key it already -// carries, so paying a recipient an arbitrary amount first requires -// transferring a channel note to their key. That needs channel note tracking. -pub(crate) async fn run_withdraw_prepare(_args: WithdrawPrepareArgs) -> RunResult<()> { - Err("withdraw prepare is unsupported until channel notes are tracked".into()) -} - -// pub(crate) async fn run_withdraw_prepare(args: WithdrawPrepareArgs) -> -// RunResult<()> { let funds = -// read_json::(&args.recipient_funds)?; validate_kind(& -// funds.kind, ZONE_WALLET_FUNDS_EXPORT, funds.version)?; let recipient = -// decode_zk_public_key_hex(&funds.public_key)?; let channel_id = -// resolve_channel_id(&args.node_key)?; let (mut sequencer, channel_state) = -// start_cli_sequencer_with_channel_state(&args.node_key).await?; -// let channel_state = -// channel_state.ok_or_else(|| format!("channel state not found for -// {channel_id}"))?; print_channel_balance("withdraw before", &channel_id, -// Some(&channel_state)); if args.amount > channel_state.balance { -// return Err(format!( -// "insufficient channel balance for withdraw: requested {}, -// available {}", args.amount, channel_state.balance -// ) -// .into()); -// } -// let withdraw_nonce = channel_state.withdrawal_nonce; -// let withdraw = ChannelWithdrawOp { -// channel_id, -// outputs: Outputs::new([Note::new(args.amount, recipient)]), -// withdraw_nonce, -// }; -// let inscription = Inscription::try_from(args.message.into_bytes())?; -// let (tx, msg_id, inscription_signature) = sequencer -// .handle() -// .prepare_tx([Op::ChannelWithdraw(withdraw)].into(), inscription)?; -// let tx_hash = tx.hash(); -// let intent = WithdrawIntent { -// version: ZONE_FILE_TRANSFER_VERSION, -// kind: ZONE_WITHDRAW_INTENT.to_owned(), -// channel_id: hex::encode(channel_id.as_ref()), -// tx_hash: hex::encode(tx_hash.as_ref()), -// msg_id: hex::encode(msg_id.as_ref()), -// required_threshold: channel_state.transfer_threshold, -// mantle_tx: hex::encode(tx.encode()), -// inscription_signature: encode_hex_bincode(&inscription_signature)?, -// withdraws: vec![WithdrawFileEntry { -// amount: args.amount, -// recipient_public_key: funds.public_key, -// withdraw_nonce, -// }], -// authorized_signers: channel_state -// .accredited_keys -// .iter() -// .enumerate() -// .map(|(index, key)| AuthorizedSigner { -// key_index: index as ChannelKeyIndex, -// public_key: hex::encode(key.to_bytes()), -// }) -// .collect(), -// signatures: Vec::new(), -// }; -// write_json(&args.out, &intent)?; -// println!( -// "{} withdraw: intent tx_hash={} msg_id={}", -// timestamp(), -// intent.tx_hash, -// intent.msg_id -// ); -// Ok(()) -// } - -pub(crate) fn run_withdraw_sign(args: &WithdrawSignArgs) -> RunResult<()> { - let intent = read_json::(&args.input)?; - validate_kind(&intent.kind, ZONE_WITHDRAW_INTENT, intent.version)?; - let tx = decode_mantle_tx_hex(&intent.mantle_tx)?; - let tx_hash = tx.hash(); - ensure_tx_hash(&intent.tx_hash, tx_hash)?; - let signing_key = load_or_create_signing_key(PathBuf::from(&args.key_path).as_path()); - let public_key = signing_key.public_key(); - let signer = intent - .authorized_signers - .iter() - .find(|signer| signer.public_key == hex::encode(public_key.to_bytes())) - .ok_or_else(|| { - "signing key is not listed in withdraw intent authorized_signers".to_owned() - })?; - let signature = signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()); - let signature_file = WithdrawSignatureFile { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WITHDRAW_SIGNATURE.to_owned(), - channel_id: intent.channel_id, - tx_hash: intent.tx_hash, - signer_public_key: signer.public_key.clone(), - signer_key_index: signer.key_index, - signature: encode_hex_bincode(&signature)?, - }; - write_json(&args.out, &signature_file)?; - println!( - "{} withdraw: signature tx_hash={} signer_key_index={}", - timestamp(), - signature_file.tx_hash, - signature_file.signer_key_index - ); - Ok(()) -} - -pub(crate) fn run_withdraw_combine(args: WithdrawCombineArgs) -> RunResult<()> { - let intent = read_json::(&args.input)?; - validate_kind(&intent.kind, ZONE_WITHDRAW_INTENT, intent.version)?; - let tx = decode_mantle_tx_hex(&intent.mantle_tx)?; - let tx_hash = tx.hash(); - ensure_tx_hash(&intent.tx_hash, tx_hash)?; - let mut signature_entries = intent.signatures.clone(); - for path in args.sig { - let sig = read_json::(&path)?; - validate_kind(&sig.kind, ZONE_WITHDRAW_SIGNATURE, sig.version)?; - if sig.channel_id != intent.channel_id { - return Err(format!( - "signature '{}' channel_id {} does not match intent {}", - path.display(), - sig.channel_id, - intent.channel_id - ) - .into()); - } - if sig.tx_hash != intent.tx_hash { - return Err(format!( - "signature '{}' tx_hash {} does not match intent {}", - path.display(), - sig.tx_hash, - intent.tx_hash - ) - .into()); - } - let public_key = decode_ed25519_public_key_hex(&sig.signer_public_key)?; - validate_authorized_signer(&intent, sig.signer_key_index, &sig.signer_public_key)?; - let signature = decode_hex_bincode::(&sig.signature)?; - public_key.verify(tx_hash.as_signing_bytes().as_ref(), &signature)?; - signature_entries.push(WithdrawSignatureEntry { - signer_key_index: sig.signer_key_index, - signer_public_key: sig.signer_public_key, - signature: sig.signature, - }); - } - let proof = ChannelMultiSigProof::try_new( - signature_entries - .iter() - .map(|sig| { - validate_authorized_signer(&intent, sig.signer_key_index, &sig.signer_public_key)?; - decode_hex_bincode::(&sig.signature) - .map(|signature| IndexedSignature::new(sig.signer_key_index, signature)) - }) - .collect::>>()? - .try_into()?, - )?; - if proof.signatures().len() < intent.required_threshold as usize { - return Err(format!( - "withdraw combine requires {} unique authorized signature(s), got {}", - intent.required_threshold, - proof.signatures().len() - ) - .into()); - } - let inscription_signature = - decode_hex_bincode::(&intent.inscription_signature)?; - let op_proofs = tx - .ops() - .iter() - .map(|op| match op { - Op::ChannelWithdraw(_) => Ok(OpProof::ChannelMultiSigProof(proof.clone())), - Op::ChannelInscribe(_) => Ok(OpProof::Ed25519Sig(inscription_signature)), - other => Err(format!("unexpected op in withdraw intent: {other:?}").into()), - }) - .collect::>>()? - .try_into()?; - let signed_tx = SignedMantleTx::new(tx, op_proofs); - let signed = SignedWithdrawFile { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_SIGNED_TRANSACTION.to_owned(), - channel_id: intent.channel_id, - tx_hash: intent.tx_hash, - msg_id: intent.msg_id, - signed_mantle_tx: hex::encode(encode_signed_mantle_tx(&signed_tx)), - signatures: signature_entries, - }; - write_json(&args.out, &signed)?; - println!( - "{} withdraw: signed tx_hash={} msg_id={}", - timestamp(), - signed.tx_hash, - signed.msg_id - ); - Ok(()) -} - -fn validate_authorized_signer( - intent: &WithdrawIntent, - signer_key_index: ChannelKeyIndex, - signer_public_key: &str, -) -> RunResult<()> { - if intent.authorized_signers.iter().any(|signer| { - signer.key_index == signer_key_index && signer.public_key == signer_public_key - }) { - return Ok(()); - } - Err(format!( - "signature signer key_index={signer_key_index} public_key={signer_public_key} is not listed in withdraw intent authorized_signers" - ) - .into()) -} - -fn decode_channel_id_hex(channel_id: &str) -> RunResult { - Ok(ChannelId::from(fixed_bytes::<32>(channel_id)?)) -} - -pub(crate) async fn run_withdraw_submit(args: WithdrawSubmitArgs) -> RunResult<()> { - let signed = read_json::(&args.input)?; - validate_kind(&signed.kind, ZONE_SIGNED_TRANSACTION, signed.version)?; - let signed_tx = decode_signed_mantle_tx_hex(&signed.signed_mantle_tx)?; - let tx_hash = signed_tx.hash(); - ensure_tx_hash(&signed.tx_hash, tx_hash)?; - let channel_id = decode_channel_id_hex(&signed.channel_id)?; - if let Some(requested_channel_id) = args.node_key.channel_id.as_deref() - && signed.channel_id != requested_channel_id - { - return Err(format!( - "signed withdraw channel_id {} does not match requested channel_id {}", - signed.channel_id, requested_channel_id - ) - .into()); - } - let mut node_key = args.node_key; - node_key.channel_id = Some(signed.channel_id.clone()); - let withdraws = signed_tx - .mantle_tx() - .ops() - .iter() - .filter_map(|op| match op { - Op::ChannelWithdraw(withdraw) => Some(withdraw.clone()), - _ => None, - }) - .collect::>(); - if withdraws.is_empty() { - return Err("signed withdraw transaction has no withdraw ops".into()); - } - if withdraws - .iter() - .any(|withdraw| withdraw.channel_id != channel_id) - { - return Err( - "signed withdraw transaction channel_id does not match requested channel".into(), - ); - } - let goal = CommandGoal::Withdraw { tx_hash, withdraws }; - let mut sequencer = start_cli_sequencer(&node_key).await?; - let status_rx = sequencer.subscribe_tx_status(); - let (_result, _checkpoint) = sequencer - .handle() - .submit_signed_tx(signed_tx, decode_msg_id_hex(&signed.msg_id)?)?; - println!( - "{} withdraw: submitted tx_hash={} msg_id={}", - timestamp(), - signed.tx_hash, - signed.msg_id - ); - let wait_for = if args.wait_finalized { - WaitFor::Finalized - } else { - WaitFor::OnChain - }; - drive_until_observed( - &channel_id, - &mut sequencer, - status_rx, - goal, - wait_for, - "withdraw", - ) - .await?; - // TODO: report the channel balance again once channel notes are tracked. - // let node = node_client(&node_key.node_url)?; - // let channel_state = query_channel_state(&node, channel_id).await; - // print_channel_balance("withdraw after", &channel_id, channel_state.as_ref()); - Ok(()) -} diff --git a/deployment/tui-zone/src/run_commands/types.rs b/deployment/tui-zone/src/run_commands/types.rs deleted file mode 100644 index 8e494fd62..000000000 --- a/deployment/tui-zone/src/run_commands/types.rs +++ /dev/null @@ -1,205 +0,0 @@ -use lb_core::mantle::{Value, ops::channel::ChannelKeyIndex}; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -/// Wallet funds exported by the cucumber manual `EXPORT_FUNDS` command. -pub struct WalletFundsExport { - /// File format version. - pub version: u8, - /// File kind discriminator. - pub kind: String, - /// Name of the cucumber wallet that exported the funds. - pub wallet: String, - /// HTTP endpoint of the node associated with the exporting wallet. - #[serde(alias = "node")] - pub node_url: String, - /// Hex-encoded recipient wallet public key. - pub public_key: String, - /// Optional hex-encoded wallet secret key for spending exported funds. - pub secret_key: Option, - /// Amount requested by the export command. - pub requested_value: u64, - /// Total value selected by the export command. - pub selected_value: u64, - /// Exported spendable UTXOs. - pub utxos: Vec, -} - -#[derive(Serialize, Deserialize)] -/// One UTXO entry in a wallet funds export. -pub struct ExportedUtxo { - /// Hex-encoded UTXO identifier. - pub utxo_id: String, - /// Value carried by the UTXO. - pub value: u64, - /// Hex-encoded bincode representation of the UTXO. - pub encoded_utxo: String, -} - -#[derive(Serialize, Deserialize)] -/// Unsigned withdrawal transaction plus metadata required for offline signing. -pub struct WithdrawIntent { - /// File format version. - pub version: u8, - /// File kind discriminator. - pub kind: String, - /// Hex-encoded zone channel identifier. - pub channel_id: String, - /// Hex-encoded transaction hash that signers must sign. - pub tx_hash: String, - /// Hex-encoded inscription message identifier. - pub msg_id: String, - /// Number of unique authorized signatures required. - pub required_threshold: u16, - /// Hex-encoded mantle transaction bytes. - pub mantle_tx: String, - /// Hex-encoded sequencer signature for the inscription op. - pub inscription_signature: String, - /// Withdrawal output metadata included for operators. - pub withdraws: Vec, - /// Authorized channel signers indexed by channel key index. - pub authorized_signers: Vec, - /// Signatures already attached to the intent. - pub signatures: Vec, -} - -#[derive(Serialize, Deserialize)] -/// Human-readable withdrawal output metadata. -pub struct WithdrawFileEntry { - /// Withdrawn value. - pub amount: Value, - /// Hex-encoded recipient public key. - pub recipient_public_key: String, - /// Channel withdrawal nonce used by the transaction. - pub withdraw_nonce: u32, -} - -#[derive(Serialize, Deserialize, Clone)] -/// Authorized channel signer metadata. -pub struct AuthorizedSigner { - /// Index of the key in the channel's accredited key list. - pub key_index: ChannelKeyIndex, - /// Hex-encoded Ed25519 public key. - pub public_key: String, -} - -#[derive(Serialize, Deserialize, Clone)] -/// Signature entry embedded in a withdrawal intent or signed withdrawal file. -pub struct WithdrawSignatureEntry { - /// Index of the signer in the channel's accredited key list. - pub signer_key_index: ChannelKeyIndex, - /// Hex-encoded Ed25519 public key for the signer. - pub signer_public_key: String, - /// Hex-encoded bincode Ed25519 signature over the transaction hash. - pub signature: String, -} - -#[derive(Serialize, Deserialize)] -/// Standalone signature file produced by `withdraw sign`. -pub struct WithdrawSignatureFile { - /// File format version. - pub version: u8, - /// File kind discriminator. - pub kind: String, - /// Hex-encoded zone channel identifier. - pub channel_id: String, - /// Hex-encoded transaction hash this signature covers. - pub tx_hash: String, - /// Hex-encoded Ed25519 public key for the signer. - pub signer_public_key: String, - /// Index of the signer in the channel's accredited key list. - pub signer_key_index: ChannelKeyIndex, - /// Hex-encoded bincode Ed25519 signature over the transaction hash. - pub signature: String, -} - -#[derive(Serialize, Deserialize)] -/// Signed withdrawal transaction file ready for submission. -pub struct SignedWithdrawFile { - /// File format version. - pub version: u8, - /// File kind discriminator. - pub kind: String, - /// Hex-encoded zone channel identifier. - pub channel_id: String, - /// Hex-encoded signed transaction hash. - pub tx_hash: String, - /// Hex-encoded inscription message identifier. - pub msg_id: String, - /// Hex-encoded signed mantle transaction bytes. - pub signed_mantle_tx: String, - /// Signature entries used to build the signed transaction. - pub signatures: Vec, -} - -#[derive(Serialize, Deserialize)] -/// Unsigned channel configuration transaction plus metadata for offline -/// signing. -pub struct ConfigIntent { - /// File format version. - pub version: u8, - /// File kind discriminator. - pub kind: String, - /// Hex-encoded zone channel identifier. - pub channel_id: String, - /// Hex-encoded transaction hash that signers must sign. - pub tx_hash: String, - /// Hex-encoded channel configuration identifier (config lineage). - pub config_id: String, - /// Number of current authorized signatures required. - pub required_threshold: u16, - /// Hex-encoded mantle transaction bytes. - pub mantle_tx: String, - /// New authorized signer public keys in channel order. - pub new_authorized_keys: Vec, - /// New configuration threshold. - pub configuration_threshold: u16, - /// New transfer threshold, gating transfers and withdrawals. - pub transfer_threshold: u16, - /// New posting timeframe in slots. - pub posting_timeframe: u32, - /// New posting timeout in slots. - pub posting_timeout: u32, - /// Current authorized channel signers indexed by channel key index. - pub authorized_signers: Vec, - /// Signatures already attached to the intent. - pub signatures: Vec, -} - -#[derive(Serialize, Deserialize)] -/// Standalone signature file produced by `config sign`. -pub struct ConfigSignatureFile { - /// File format version. - pub version: u8, - /// File kind discriminator. - pub kind: String, - /// Hex-encoded zone channel identifier. - pub channel_id: String, - /// Hex-encoded transaction hash this signature covers. - pub tx_hash: String, - /// Hex-encoded Ed25519 public key for the signer. - pub signer_public_key: String, - /// Index of the signer in the channel's current accredited key list. - pub signer_key_index: ChannelKeyIndex, - /// Hex-encoded bincode Ed25519 signature over the transaction hash. - pub signature: String, -} - -#[derive(Serialize, Deserialize)] -/// Signed channel configuration transaction file ready for submission. -pub struct SignedConfigFile { - /// File format version. - pub version: u8, - /// File kind discriminator. - pub kind: String, - /// Hex-encoded zone channel identifier. - pub channel_id: String, - /// Hex-encoded signed transaction hash. - pub tx_hash: String, - /// Hex-encoded channel configuration identifier (config lineage). - pub config_id: String, - /// Hex-encoded signed mantle transaction bytes. - pub signed_mantle_tx: String, - /// Signature entries used to build the signed transaction. - pub signatures: Vec, -} diff --git a/deployment/tui-zone/src/run_commands/unit_tests.rs b/deployment/tui-zone/src/run_commands/unit_tests.rs deleted file mode 100644 index 3f8180451..000000000 --- a/deployment/tui-zone/src/run_commands/unit_tests.rs +++ /dev/null @@ -1,491 +0,0 @@ -#[cfg(test)] -mod tests { - use std::{ - fs, - path::PathBuf, - time::{SystemTime, UNIX_EPOCH}, - }; - - use lb_codec::BinaryEncode as _; - use lb_core::mantle::{ - Note, NoteId, Op, RawMantleTx, SignedMantleTx, Utxo, Value, - ledger::Inputs, - ops::channel::{ - ChannelId, MsgId, - inscribe::{Inscription, InscriptionOp}, - withdraw::ChannelWithdrawOp, - }, - traits::Hashable as _, - transactions::{Ops, OpsProofs, codec::encode_signed_mantle_tx}, - }; - use lb_groth16::{Fr, fr_to_bytes}; - use lb_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key, ZkKey}; - - use crate::{ - cli::{WithdrawCombineArgs, WithdrawSignArgs}, - run_commands::{ - ZONE_FILE_TRANSFER_VERSION, ZONE_WALLET_FUNDS_EXPORT, ZONE_WITHDRAW_INTENT, - ZONE_WITHDRAW_SIGNATURE, - run_withdraw::{run_withdraw_combine, run_withdraw_sign}, - types::{ - AuthorizedSigner, ExportedUtxo, SignedWithdrawFile, WalletFundsExport, - WithdrawFileEntry, WithdrawIntent, WithdrawSignatureFile, - }, - utils::{ - build_deposit_op, build_deposit_transfer, decode_ed25519_public_key_hex, - decode_exported_utxos, decode_hex, decode_hex_bincode, decode_mantle_tx_hex, - decode_signed_mantle_tx_hex, decode_zk_public_key_hex, encode_hex_bincode, - ensure_tx_hash, fixed_bytes, read_json, validate_kind, write_json, - }, - }, - }; - - fn test_path(name: &str) -> PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time must be after UNIX_EPOCH") - .as_nanos(); - std::env::temp_dir().join(format!( - "tui-zone-manual-demo-test-{}-{nanos}-{name}", - std::process::id() - )) - } - - fn test_zk_key() -> ZkKey { - ZkKey::new(Fr::from(1u64)) - } - - fn test_utxo(value: Value, output_index: usize) -> Utxo { - Utxo::new( - [output_index as u8; 32], - output_index, - Note::new(value, test_zk_key().to_public_key()), - ) - } - - fn test_signing_key(byte: u8) -> Ed25519Key { - Ed25519Key::from_bytes(&[byte; ED25519_SECRET_KEY_SIZE]) - } - - fn empty_mantle_tx() -> RawMantleTx { - RawMantleTx(Ops::try_from(Vec::new()).expect("empty ops must be valid")) - } - - #[test] - fn validate_kind_accepts_expected_versioned_kind() { - validate_kind(ZONE_WALLET_FUNDS_EXPORT, ZONE_WALLET_FUNDS_EXPORT, 1).unwrap(); - } - - #[test] - fn validate_kind_rejects_wrong_version_or_kind() { - assert!(validate_kind(ZONE_WALLET_FUNDS_EXPORT, ZONE_WALLET_FUNDS_EXPORT, 2).is_err()); - assert!(validate_kind("other", ZONE_WALLET_FUNDS_EXPORT, 1).is_err()); - } - - #[test] - fn hex_helpers_accept_prefixed_and_plain_values() { - assert_eq!(decode_hex("0x0a0b").unwrap(), vec![10, 11]); - assert_eq!(decode_hex("0a0b").unwrap(), vec![10, 11]); - assert_eq!(fixed_bytes::<2>("0x0a0b").unwrap(), [10, 11]); - assert!(fixed_bytes::<3>("0x0a0b").is_err()); - } - - #[test] - fn bincode_hex_roundtrips_exported_utxo() { - let utxo = test_utxo(42, 0); - let encoded = encode_hex_bincode(&utxo).unwrap(); - let decoded = decode_hex_bincode::(&encoded).unwrap(); - assert_eq!(decoded, utxo); - } - - #[test] - fn public_key_decoders_accept_cli_json_hex_shapes() { - let zk_public_key = test_zk_key().to_public_key(); - let zk_hex = hex::encode(fr_to_bytes(zk_public_key.as_fr())); - assert_eq!(decode_zk_public_key_hex(&zk_hex).unwrap(), zk_public_key); - - let ed25519_public_key = test_signing_key(9).public_key(); - let ed25519_hex = hex::encode(ed25519_public_key.to_bytes()); - assert_eq!( - decode_ed25519_public_key_hex(&ed25519_hex).unwrap(), - ed25519_public_key - ); - } - - #[test] - fn mantle_tx_decoders_reject_trailing_bytes_and_hash_mismatches() { - let tx = empty_mantle_tx(); - let encoded = hex::encode(tx.encode()); - assert_eq!(decode_mantle_tx_hex(&encoded).unwrap(), tx); - assert!(decode_mantle_tx_hex(&format!("{encoded}00")).is_err()); - - ensure_tx_hash(&hex::encode(tx.hash().as_ref()), tx.hash()).unwrap(); - assert!(ensure_tx_hash(&hex::encode([1u8; 32]), tx.hash()).is_err()); - - let signed_tx = SignedMantleTx::new(tx, OpsProofs::empty()); - let signed_encoded = hex::encode(encode_signed_mantle_tx(&signed_tx)); - assert_eq!( - decode_signed_mantle_tx_hex(&signed_encoded).unwrap(), - signed_tx - ); - assert!(decode_signed_mantle_tx_hex(&format!("{signed_encoded}00")).is_err()); - } - - #[test] - fn decode_exported_utxos_reads_cucumber_wallet_funds_json_entries() { - let utxos = vec![test_utxo(100, 0), test_utxo(200, 1)]; - let funds = WalletFundsExport { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WALLET_FUNDS_EXPORT.to_owned(), - wallet: "WALLET_1A".to_owned(), - node_url: "http://localhost:8080".to_owned(), - public_key: hex::encode(fr_to_bytes(test_zk_key().to_public_key().as_fr())), - secret_key: Some(encode_hex_bincode(&test_zk_key()).unwrap()), - requested_value: 100, - selected_value: 300, - utxos: utxos - .iter() - .map(|utxo| ExportedUtxo { - utxo_id: hex::encode(utxo.id().as_bytes()), - value: utxo.note.value, - encoded_utxo: encode_hex_bincode(utxo).unwrap(), - }) - .collect(), - }; - - assert_eq!(decode_exported_utxos(&funds).unwrap(), utxos); - } - - #[test] - fn build_deposit_transfer_selects_largest_inputs_and_returns_change() { - let public_key = test_zk_key().to_public_key(); - let (transfer, selected) = - build_deposit_transfer(vec![test_utxo(4, 0), test_utxo(10, 1)], public_key, 7).unwrap(); - - assert_eq!(selected, vec![test_utxo(10, 1)]); - let outputs = transfer.utxos().collect::>(); - assert_eq!(outputs.len(), 2); - assert_eq!(outputs[0].note.value, 7); - assert_eq!(outputs[1].note.value, 3); - } - - #[test] - fn build_deposit_transfer_rejects_insufficient_funds() { - assert!( - build_deposit_transfer(vec![test_utxo(4, 0)], test_zk_key().to_public_key(), 7) - .is_err() - ); - } - - #[test] - fn build_deposit_op_consumes_first_transfer_output() { - let (transfer, _) = - build_deposit_transfer(vec![test_utxo(10, 1)], test_zk_key().to_public_key(), 7) - .unwrap(); - let channel_id = ChannelId::from([5; 32]); - let deposit = build_deposit_op(channel_id, &transfer, "demo metadata").unwrap(); - - assert_eq!(deposit.channel_id, channel_id); - assert_eq!( - deposit.inputs, - Inputs::new([transfer.outputs.utxo_by_index(0, &transfer).unwrap().id()]) - ); - } - - #[test] - fn json_helpers_roundtrip_and_create_parent_directories() { - let path = test_path("nested/funds.json"); - let funds = WalletFundsExport { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WALLET_FUNDS_EXPORT.to_owned(), - wallet: "WALLET_1A".to_owned(), - node_url: "http://localhost:8080".to_owned(), - public_key: hex::encode(fr_to_bytes(test_zk_key().to_public_key().as_fr())), - secret_key: None, - requested_value: 0, - selected_value: 0, - utxos: Vec::new(), - }; - - write_json(&path, &funds).unwrap(); - let decoded = read_json::(&path).unwrap(); - assert_eq!(decoded.kind, funds.kind); - assert_eq!(decoded.wallet, funds.wallet); - - drop(fs::remove_file(&path)); - if let Some(parent) = path.parent() { - drop(fs::remove_dir_all(parent)); - } - } - - #[test] - fn withdraw_sign_and_combine_build_signed_withdraw_file_offline() { - let channel_id = ChannelId::from([7; 32]); - let signer = test_signing_key(2); - let inscriber = test_signing_key(3); - let recipient = test_zk_key().to_public_key(); - let withdraw = ChannelWithdrawOp { - channel_id, - inputs: Inputs::new([NoteId::from(Fr::from(500u64))]), - }; - let inscribe = InscriptionOp { - channel_id, - inscription: Inscription::try_from(b"withdraw wallet 1a".to_vec()).unwrap(), - parent: MsgId::root(), - signer: inscriber.public_key(), - }; - let tx = RawMantleTx( - Ops::try_from(vec![ - Op::ChannelWithdraw(withdraw), - Op::ChannelInscribe(inscribe), - ]) - .unwrap(), - ); - let tx_hash = tx.hash(); - let msg_id = MsgId::from([8; 32]); - let intent = WithdrawIntent { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WITHDRAW_INTENT.to_owned(), - channel_id: hex::encode(channel_id.as_ref()), - tx_hash: hex::encode(tx_hash.as_ref()), - msg_id: hex::encode(msg_id.as_ref()), - required_threshold: 1, - mantle_tx: hex::encode(tx.encode()), - inscription_signature: encode_hex_bincode( - &inscriber.sign_payload(tx_hash.as_signing_bytes().as_ref()), - ) - .unwrap(), - withdraws: vec![WithdrawFileEntry { - amount: 500, - recipient_public_key: hex::encode(fr_to_bytes(recipient.as_fr())), - withdraw_nonce: 0, - }], - authorized_signers: vec![AuthorizedSigner { - key_index: 0, - public_key: hex::encode(signer.public_key().to_bytes()), - }], - signatures: Vec::new(), - }; - let key_path = test_path("signer.key"); - let intent_path = test_path("withdraw.intent.json"); - let sig_path = test_path("sig-a.json"); - let signed_path = test_path("withdraw.signed.json"); - - fs::write(&key_path, [2u8; ED25519_SECRET_KEY_SIZE]).unwrap(); - write_json(&intent_path, &intent).unwrap(); - run_withdraw_sign(&WithdrawSignArgs { - key_path: key_path.to_string_lossy().to_string(), - input: intent_path.clone(), - out: sig_path.clone(), - }) - .unwrap(); - run_withdraw_combine(WithdrawCombineArgs { - input: intent_path.clone(), - sig: vec![sig_path.clone()], - out: signed_path.clone(), - }) - .unwrap(); - - let sig_file = read_json::(&sig_path).unwrap(); - assert_eq!(sig_file.signer_key_index, 0); - assert_eq!(sig_file.tx_hash, intent.tx_hash); - - let signed_file = read_json::(&signed_path).unwrap(); - let signed_tx = decode_signed_mantle_tx_hex(&signed_file.signed_mantle_tx).unwrap(); - assert_eq!(signed_tx.hash(), tx_hash); - assert_eq!(signed_file.signatures.len(), 1); - - drop(fs::remove_file(key_path)); - drop(fs::remove_file(intent_path)); - drop(fs::remove_file(sig_path)); - drop(fs::remove_file(signed_path)); - } - - #[test] - fn withdraw_combine_rejects_too_few_signatures() { - let tx = RawMantleTx( - Ops::try_from(vec![Op::ChannelWithdraw(ChannelWithdrawOp { - channel_id: ChannelId::from([9; 32]), - inputs: Inputs::new([NoteId::from(Fr::from(1u64))]), - })]) - .unwrap(), - ); - let intent = WithdrawIntent { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WITHDRAW_INTENT.to_owned(), - channel_id: hex::encode([9; 32]), - tx_hash: hex::encode(tx.hash().as_ref()), - msg_id: hex::encode(MsgId::root().as_ref()), - required_threshold: 1, - mantle_tx: hex::encode(tx.encode()), - inscription_signature: encode_hex_bincode( - &test_signing_key(4).sign_payload(tx.hash().as_signing_bytes().as_ref()), - ) - .unwrap(), - withdraws: Vec::new(), - authorized_signers: Vec::new(), - signatures: Vec::new(), - }; - let intent_path = test_path("withdraw-too-few.intent.json"); - let signed_path = test_path("withdraw-too-few.signed.json"); - - write_json(&intent_path, &intent).unwrap(); - let error = run_withdraw_combine(WithdrawCombineArgs { - input: intent_path.clone(), - sig: Vec::new(), - out: signed_path.clone(), - }) - .unwrap_err() - .to_string(); - - assert!(error.contains("requires 1 unique authorized signature(s), got 0")); - - drop(fs::remove_file(intent_path)); - drop(fs::remove_file(signed_path)); - } - - #[test] - fn withdraw_combine_counts_unique_signatures_after_proof_normalization() { - let channel_id = ChannelId::from([10; 32]); - let signer = test_signing_key(2); - let second_signer = test_signing_key(3); - let tx = RawMantleTx( - Ops::try_from(vec![Op::ChannelWithdraw(ChannelWithdrawOp { - channel_id, - inputs: Inputs::new([NoteId::from(Fr::from(1u64))]), - })]) - .unwrap(), - ); - let tx_hash = tx.hash(); - let intent = WithdrawIntent { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WITHDRAW_INTENT.to_owned(), - channel_id: hex::encode(channel_id.as_ref()), - tx_hash: hex::encode(tx_hash.as_ref()), - msg_id: hex::encode(MsgId::root().as_ref()), - required_threshold: 2, - mantle_tx: hex::encode(tx.encode()), - inscription_signature: encode_hex_bincode( - &signer.sign_payload(tx_hash.as_signing_bytes().as_ref()), - ) - .unwrap(), - withdraws: Vec::new(), - authorized_signers: vec![ - AuthorizedSigner { - key_index: 0, - public_key: hex::encode(signer.public_key().to_bytes()), - }, - AuthorizedSigner { - key_index: 1, - public_key: hex::encode(second_signer.public_key().to_bytes()), - }, - ], - signatures: Vec::new(), - }; - let sig = WithdrawSignatureFile { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WITHDRAW_SIGNATURE.to_owned(), - channel_id: intent.channel_id.clone(), - tx_hash: intent.tx_hash.clone(), - signer_public_key: hex::encode(signer.public_key().to_bytes()), - signer_key_index: 0, - signature: encode_hex_bincode( - &signer.sign_payload(tx_hash.as_signing_bytes().as_ref()), - ) - .unwrap(), - }; - let intent_path = test_path("withdraw-duplicate.intent.json"); - let sig_path = test_path("withdraw-duplicate.sig.json"); - let signed_path = test_path("withdraw-duplicate.signed.json"); - - write_json(&intent_path, &intent).unwrap(); - write_json(&sig_path, &sig).unwrap(); - let error = run_withdraw_combine(WithdrawCombineArgs { - input: intent_path.clone(), - sig: vec![sig_path.clone(), sig_path.clone()], - out: signed_path.clone(), - }) - .unwrap_err() - .to_string(); - - assert!( - error.contains("Signature indices are not strictly increasing"), - "Error is: {error:?}", - ); - - drop(fs::remove_file(intent_path)); - drop(fs::remove_file(sig_path)); - drop(fs::remove_file(signed_path)); - } - - #[test] - fn withdraw_combine_rejects_signature_with_mismatched_authorized_index() { - let channel_id = ChannelId::from([11; 32]); - let signer = test_signing_key(2); - let second_signer = test_signing_key(3); - let tx = RawMantleTx( - Ops::try_from(vec![Op::ChannelWithdraw(ChannelWithdrawOp { - channel_id, - inputs: Inputs::new([NoteId::from(Fr::from(1u64))]), - })]) - .unwrap(), - ); - let tx_hash = tx.hash(); - let intent = WithdrawIntent { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WITHDRAW_INTENT.to_owned(), - channel_id: hex::encode(channel_id.as_ref()), - tx_hash: hex::encode(tx_hash.as_ref()), - msg_id: hex::encode(MsgId::root().as_ref()), - required_threshold: 1, - mantle_tx: hex::encode(tx.encode()), - inscription_signature: encode_hex_bincode( - &signer.sign_payload(tx_hash.as_signing_bytes().as_ref()), - ) - .unwrap(), - withdraws: Vec::new(), - authorized_signers: vec![ - AuthorizedSigner { - key_index: 0, - public_key: hex::encode(signer.public_key().to_bytes()), - }, - AuthorizedSigner { - key_index: 1, - public_key: hex::encode(second_signer.public_key().to_bytes()), - }, - ], - signatures: Vec::new(), - }; - let sig = WithdrawSignatureFile { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WITHDRAW_SIGNATURE.to_owned(), - channel_id: intent.channel_id.clone(), - tx_hash: intent.tx_hash.clone(), - signer_public_key: hex::encode(signer.public_key().to_bytes()), - signer_key_index: 1, - signature: encode_hex_bincode( - &signer.sign_payload(tx_hash.as_signing_bytes().as_ref()), - ) - .unwrap(), - }; - let intent_path = test_path("withdraw-mismatched-index.intent.json"); - let sig_path = test_path("withdraw-mismatched-index.sig.json"); - let signed_path = test_path("withdraw-mismatched-index.signed.json"); - - write_json(&intent_path, &intent).unwrap(); - write_json(&sig_path, &sig).unwrap(); - let error = run_withdraw_combine(WithdrawCombineArgs { - input: intent_path.clone(), - sig: vec![sig_path.clone()], - out: signed_path.clone(), - }) - .unwrap_err() - .to_string(); - - assert!(error.contains("is not listed in withdraw intent authorized_signers")); - - drop(fs::remove_file(intent_path)); - drop(fs::remove_file(sig_path)); - drop(fs::remove_file(signed_path)); - } -} diff --git a/deployment/tui-zone/src/run_commands/utils.rs b/deployment/tui-zone/src/run_commands/utils.rs deleted file mode 100644 index a8596020a..000000000 --- a/deployment/tui-zone/src/run_commands/utils.rs +++ /dev/null @@ -1,417 +0,0 @@ -use std::{ - fs, - path::{Path, PathBuf}, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -use chrono::{DateTime, Utc}; -use lb_codec::BinaryDecodeExt as _; -use lb_core::mantle::{ - Note, SignedMantleTx, Utxo, Value, - channel::ChannelState, - ledger::{Inputs, Outputs}, - ops::{ - channel::{ - ChannelId, MsgId, - deposit::{DepositOp, Metadata}, - }, - transfer::TransferOp, - }, - transactions::{ - codec::decode_signed_mantle_tx, hash::TxHash, mantle_tx::RawMantleTx, states::Unverified, - }, -}; -use lb_key_management_system_service::keys::{ - ED25519_SECRET_KEY_SIZE, Ed25519Key, Ed25519PublicKey, ZkPublicKey, -}; -use lb_zone_sdk::{ - CommonHttpClient, - adapter::{Node as _, NodeHttpClient}, - sequencer::{Event, FundingConfig, SequencerCheckpoint, SequencerConfig, ZoneSequencer}, -}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use tokio::time::sleep; -use tracing::warn; - -use crate::{ - cli::{NodeKeyArgs, RunResult}, - run_commands::types::WalletFundsExport, - state::{load_or_discard_persisted_checkpoint_for_channel, save_persisted_checkpoint}, -}; - -const CHANNEL_STATE_QUERY_RETRY: Duration = Duration::from_secs(5); - -/// Format the current wall-clock time as RFC 3339 / ISO-8601 UTC timestamp with -/// microseconds, example `2026-06-19T05:43:07.036408Z`. -pub fn timestamp() -> String { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); - - let secs = now.as_secs(); - let micros = now.subsec_micros(); - - let datetime = DateTime::::from_timestamp(secs as i64, micros * 1_000) - .unwrap_or_else(|| DateTime::::from_timestamp(0, 0).unwrap()); - - datetime.format("%Y-%m-%dT%H:%M:%S.%6fZ").to_string() -} - -/// Load an Ed25519 signing key from disk or create a new one at `path`. -pub fn load_or_create_signing_key(path: &Path) -> Ed25519Key { - if path.exists() { - let key_bytes = fs::read(path).expect("failed to read key file"); - assert_eq!( - key_bytes.len(), - ED25519_SECRET_KEY_SIZE, - "invalid key file: expected {} bytes, got {}", - ED25519_SECRET_KEY_SIZE, - key_bytes.len() - ); - let key_array: [u8; ED25519_SECRET_KEY_SIZE] = - key_bytes.try_into().expect("length already checked"); - Ed25519Key::from_bytes(&key_array) - } else { - if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() - { - fs::create_dir_all(parent).expect("failed to create key file parent directory"); - } - let mut key_bytes = [0u8; ED25519_SECRET_KEY_SIZE]; - rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut key_bytes); - fs::write(path, key_bytes).expect("failed to write key file"); - Ed25519Key::from_bytes(&key_bytes) - } -} - -/// Build a node HTTP client from a URL string. -pub fn node_client(node_url: &str) -> RunResult { - Ok(NodeHttpClient::new( - CommonHttpClient::new(None), - Url::parse(node_url)?, - )) -} - -/// Validate a versioned JSON file kind discriminator. -pub fn validate_kind(actual: &str, expected: &str, version: u8) -> RunResult<()> { - if version != 1 { - return Err(format!("unsupported {actual} version {version}; expected version 1").into()); - } - if actual != expected { - return Err(format!("unsupported JSON kind '{actual}'; expected '{expected}'").into()); - } - Ok(()) -} - -/// Read and deserialize a JSON file. -pub fn read_json Deserialize<'de>>(path: &PathBuf) -> RunResult { - Ok(serde_json::from_slice(&fs::read(path)?)?) -} - -/// Serialize and write a JSON file, creating parent directories when needed. -pub fn write_json(path: &PathBuf, value: &T) -> RunResult<()> { - if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() - { - fs::create_dir_all(parent)?; - } - fs::write(path, serde_json::to_vec_pretty(value)?)?; - Ok(()) -} - -/// Decode a required hex-encoded bincode value. -pub fn decode_required_hex_bincode Deserialize<'de>>( - value: Option<&str>, - missing: &str, -) -> RunResult { - let value = value.ok_or_else(|| missing.to_owned())?; - decode_hex_bincode(value) -} - -/// Encode a serializable value as hex-encoded bincode. -pub fn encode_hex_bincode(value: &T) -> RunResult { - Ok(hex::encode(bincode::serialize(value)?)) -} - -/// Decode a hex-encoded bincode value. -pub fn decode_hex_bincode Deserialize<'de>>(value: &str) -> RunResult { - Ok(bincode::deserialize(&decode_hex(value)?)?) -} - -/// Decode a plain or `0x`-prefixed hex string. -pub fn decode_hex(value: &str) -> RunResult> { - Ok(hex::decode(value.strip_prefix("0x").unwrap_or(value))?) -} - -/// Derive the zone channel ID from a channel signing key path. -pub fn channel_id_for_key_path(path: &str) -> ChannelId { - let signing_key = load_or_create_signing_key(PathBuf::from(path).as_path()); - ChannelId::from(signing_key.public_key().to_bytes()) -} - -/// Resolve the target channel ID from CLI args or the signing key path. -pub fn resolve_channel_id(args: &NodeKeyArgs) -> RunResult { - args.channel_id.as_ref().map_or_else( - || Ok(channel_id_for_key_path(&args.key_path)), - |channel_id| Ok(ChannelId::from(fixed_bytes::<32>(channel_id)?)), - ) -} - -/// Query whether the node has channel state, retrying until the node responds. -pub async fn query_channel_exists(node: &NodeHttpClient, channel_id: ChannelId) -> bool { - query_channel_state(node, channel_id).await.is_some() -} - -/// Query channel state, retrying until the node responds. -pub async fn query_channel_state( - node: &NodeHttpClient, - channel_id: ChannelId, -) -> Option { - loop { - match node.channel_state(channel_id).await { - Ok(channel_state) => return channel_state, - Err(error) => { - warn!("failed to query channel state before sequencer init: {error}"); - sleep(CHANNEL_STATE_QUERY_RETRY).await; - } - } - } -} - -// TODO: support channel note tracking in the TUI. A channel's balance is now -// the sum of its channel notes rather than a field on `ChannelState`. -// /// Print the channel's current balance. -// pub fn print_channel_balance(label: &str, channel_id: &ChannelId, state: -// Option<&ChannelState>) { match state { -// Some(state) => println!( -// "{} {label}: channel_id={} balance={}", -// timestamp(), -// hex::encode(channel_id.as_ref()), -// state.balance, -// ), -// None => println!( -// "{} {label}: channel_id={} balance=unknown -// channel_state=missing", timestamp(), -// hex::encode(channel_id.as_ref()) -// ), -// } -// } - -/// Print the channel's current configuration state. -pub fn print_channel_state(label: &str, channel_id: &ChannelId, state: Option<&ChannelState>) { - match state { - Some(state) => println!( - "{} {label}: channel_id={} accredited_keys={} configuration_threshold={} transfer_threshold={} tip_message={} config_tip_hash={}", - timestamp(), - hex::encode(channel_id.as_ref()), - state.accredited_keys.len(), - state.configuration_threshold, - state.transfer_threshold, - hex::encode(state.tip_message.as_ref()), - hex::encode(state.config_tip_hash.as_ref()) - ), - None => println!( - "{} {label}: channel_id={} channel_state=missing", - timestamp(), - hex::encode(channel_id.as_ref()) - ), - } -} - -/// Decode all UTXOs embedded in a wallet funds export. -pub fn decode_exported_utxos(funds: &WalletFundsExport) -> RunResult> { - funds - .utxos - .iter() - .map(|utxo| decode_hex_bincode::(&utxo.encoded_utxo)) - .collect() -} - -/// Decode a hex-encoded mantle transaction and reject trailing bytes. -pub fn decode_mantle_tx_hex(value: &str) -> RunResult { - let bytes = decode_hex(value)?; - let (remaining, tx) = RawMantleTx::decode(&bytes).map_err(|error| format!("{error:?}"))?; - if !remaining.is_empty() { - return Err("mantle tx has trailing bytes".into()); - } - Ok(tx) -} - -/// Decode a hex-encoded signed mantle transaction and reject trailing bytes. -pub fn decode_signed_mantle_tx_hex(value: &str) -> RunResult> { - let bytes = decode_hex(value)?; - let (remaining, tx) = decode_signed_mantle_tx(&bytes).map_err(|error| format!("{error:?}"))?; - if !remaining.is_empty() { - return Err("signed mantle tx has trailing bytes".into()); - } - Ok(tx) -} - -/// Decode a hex-encoded ZK public key. -pub fn decode_zk_public_key_hex(value: &str) -> RunResult { - let bytes = fixed_bytes::<32>(value)?; - Ok(ZkPublicKey::new(lb_groth16::fr_from_bytes(&bytes)?)) -} - -/// Decode a hex-encoded Ed25519 public key. -pub fn decode_ed25519_public_key_hex(value: &str) -> RunResult { - let bytes = fixed_bytes::<32>(value)?; - Ok(Ed25519PublicKey::from_bytes(&bytes)?) -} - -/// Decode a hex-encoded zone message ID. -pub fn decode_msg_id_hex(value: &str) -> RunResult { - Ok(MsgId::from(fixed_bytes(value)?)) -} - -/// Decode a hex string into exactly `N` bytes. -pub fn fixed_bytes(value: &str) -> RunResult<[u8; N]> { - let bytes = decode_hex(value)?; - bytes - .try_into() - .map_err(|bytes: Vec| format!("expected {N} bytes, got {}", bytes.len()).into()) -} - -/// Ensure a decoded transaction hash matches the expected hex string. -pub fn ensure_tx_hash(expected_hex: &str, actual: TxHash) -> RunResult<()> { - let actual_hex = hex::encode(actual.as_ref()); - if expected_hex != actual_hex { - return Err(format!( - "tx_hash mismatch: JSON has {expected_hex}, decoded tx has {actual_hex}" - ) - .into()); - } - Ok(()) -} - -/// Build a transfer op that selects exported UTXOs and returns change. -pub fn build_deposit_transfer( - mut available_utxos: Vec, - funding_public_key: ZkPublicKey, - amount: Value, -) -> RunResult<(TransferOp, Vec)> { - available_utxos.sort_by_key(|utxo| std::cmp::Reverse(utxo.note.value)); - let mut selected = Vec::new(); - let mut selected_value = 0u64; - for utxo in available_utxos { - selected_value = selected_value.saturating_add(utxo.note.value); - selected.push(utxo); - if selected_value >= amount { - break; - } - } - if selected_value < amount { - return Err(format!( - "insufficient exported funds: requested {amount}, selected {selected_value}" - ) - .into()); - } - let mut outputs = vec![Note::new(amount, funding_public_key)]; - let change = selected_value - amount; - if change > 0 { - outputs.push(Note::new(change, funding_public_key)); - } - let transfer = TransferOp { - inputs: Inputs::try_new(selected.iter().map(Utxo::id).collect::>())?, - outputs: Outputs::try_new(outputs)?, - }; - Ok((transfer, selected)) -} - -/// Build a channel deposit op that consumes the first transfer output. -pub fn build_deposit_op( - channel_id: ChannelId, - transfer: &TransferOp, - metadata: &str, -) -> RunResult { - let deposit_note_id = transfer - .outputs - .utxo_by_index(0, transfer) - .expect("deposit transfer always has an output") - .id(); - Ok(DepositOp { - channel_id, - inputs: Inputs::new([deposit_note_id]), - metadata: Metadata::try_from(metadata.as_bytes().to_vec())?, - }) -} - -/// Build the sequencer funding config from CLI args. -pub fn funding_config(args: &NodeKeyArgs) -> RunResult { - Ok(FundingConfig { - funding_pk: decode_zk_public_key_hex(&args.funding_pk)?, - change_pk: None, - max_tx_fee: args.max_tx_fee.into(), - priority_fee_percent: args.priority_fee_percent, - }) -} - -/// Sequencer config for CLI commands, with funding taken from the args. -pub fn cli_sequencer_config(args: &NodeKeyArgs) -> RunResult { - Ok(SequencerConfig::new(funding_config(args)?)) -} - -/// Start a zone sequencer for non-interactive CLI commands and wait for -/// readiness. -pub async fn start_cli_sequencer(args: &NodeKeyArgs) -> RunResult> { - let (sequencer, _channel_state) = start_cli_sequencer_with_channel_state(args).await?; - Ok(sequencer) -} - -/// Start a zone sequencer for non-interactive CLI commands and wait until the -/// post-ready channel view is backed by freshly queried node channel state. -pub async fn start_cli_sequencer_with_channel_state( - args: &NodeKeyArgs, -) -> RunResult<(ZoneSequencer, Option)> { - let signing_key = load_or_create_signing_key(PathBuf::from(&args.key_path).as_path()); - let channel_id = resolve_channel_id(args)?; - let node = node_client(&args.node_url)?; - let channel_exists = query_channel_exists(&node, channel_id).await; - let checkpoint = load_cli_checkpoint(&channel_id, channel_exists)?; - let mut sequencer = ZoneSequencer::init_with_config( - channel_id, - signing_key, - node.clone(), - cli_sequencer_config(args)?, - checkpoint, - ); - while !sequencer.is_ready() { - drop(sequencer.next_event().await); - } - let channel_state = wait_for_fresh_channel_state(&mut sequencer, &node, channel_id).await?; - Ok((sequencer, channel_state)) -} - -async fn wait_for_fresh_channel_state( - sequencer: &mut ZoneSequencer, - node: &NodeHttpClient, - channel_id: ChannelId, -) -> RunResult> { - let fresh_channel_state = query_channel_state(node, channel_id).await; - let view_rx = sequencer.subscribe_channel_view(); - loop { - if view_rx.borrow().channel == fresh_channel_state { - return Ok(fresh_channel_state); - } - if let Event::BlocksProcessed { checkpoint, .. } = sequencer.next_event().await { - save_cli_checkpoint(&channel_id, &checkpoint)?; - } - } -} - -/// Load the persisted non-interactive sequencer checkpoint, if present. -pub fn load_cli_checkpoint( - channel_id: &ChannelId, - channel_exists: bool, -) -> RunResult> { - load_or_discard_persisted_checkpoint_for_channel(channel_id, channel_exists) -} - -/// Persist a non-interactive sequencer checkpoint in the runtime directory. -pub fn save_cli_checkpoint( - channel_id: &ChannelId, - checkpoint: &SequencerCheckpoint, -) -> RunResult<()> { - save_persisted_checkpoint(channel_id, checkpoint) -} diff --git a/deployment/tui-zone/src/state.rs b/deployment/tui-zone/src/state.rs deleted file mode 100644 index 2597bb1d9..000000000 --- a/deployment/tui-zone/src/state.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::{collections::HashSet, error::Error, fs}; - -use lb_core::mantle::ops::channel::ChannelId; -use lb_zone_sdk::sequencer::{InscriptionInfo, SequencerChannelView, SequencerCheckpoint}; -use serde::{Deserialize, Serialize}; -use tracing::{error, warn}; - -use crate::message::Msg; - -const CHECKPOINT_FILE: &str = "sequencer.checkpoint.json"; -const CHECKPOINT_KIND: &str = "zone_sequencer_checkpoint"; -const CHECKPOINT_VERSION: u8 = 1; - -#[derive(Serialize, Deserialize)] -struct CheckpointFile { - version: u8, - kind: String, - channel_id: Option, - checkpoint: String, -} - -/// Trait for the TUI's view of zone state. -/// -/// The TUI feeds SDK events into this trait; the trait owns persistence. -/// `InMemoryZoneState` is the demo implementation. -/// -/// Tracks two lists: -/// - `pending`: messages we published that haven't finalized yet. -/// - `finalized`: inscriptions below LIB, delivered on `BlocksProcessed`. -/// -/// The SDK manages the outbox (resubmit and shed across reorgs); this state -/// renders published and finalized messages and does not consume the channel -/// delta. -pub trait ZoneState: Send { - /// Record a message we just published as pending. - fn on_published(&mut self, info: &InscriptionInfo); - /// Move finalized inscriptions from `pending` into `finalized`. - fn on_finalized(&mut self, inscriptions: &[InscriptionInfo]); - - /// Locally published inscriptions that are not finalized yet. - fn pending(&self) -> &[Msg]; - /// Finalized inscriptions below LIB. - fn finalized(&self) -> &[Msg]; - - /// Persist the sequencer checkpoint for later resume. - fn save_checkpoint(&mut self, checkpoint: SequencerCheckpoint); - /// Load the last persisted sequencer checkpoint, if any. - fn load_checkpoint(&self) -> Option<&SequencerCheckpoint>; -} - -/// In-memory implementation of [`ZoneState`]. -#[derive(Default)] -pub struct InMemoryZoneState { - pending: Vec, - finalized: Vec, - finalized_payloads: HashSet>, - checkpoint: Option, - channel_id: Option, - channel_view: Option, -} - -impl ZoneState for InMemoryZoneState { - fn on_published(&mut self, info: &InscriptionInfo) { - if !self.pending.iter().any(|m| m.msg_id == info.this_msg) { - self.pending - .push(Msg::from_payload(info.this_msg, &info.payload)); - } - } - - fn on_finalized(&mut self, inscriptions: &[InscriptionInfo]) { - for info in inscriptions { - if let Some(i) = self.pending.iter().position(|m| m.msg_id == info.this_msg) { - self.pending.remove(i); - } - if !self.finalized.iter().any(|m| m.msg_id == info.this_msg) { - self.finalized - .push(Msg::from_payload(info.this_msg, &info.payload)); - } - self.finalized_payloads - .insert(info.payload.as_slice().to_vec()); - } - } - - fn pending(&self) -> &[Msg] { - &self.pending - } - - fn finalized(&self) -> &[Msg] { - &self.finalized - } - - fn save_checkpoint(&mut self, checkpoint: SequencerCheckpoint) { - self.checkpoint = Some(checkpoint.clone()); - if let Some(channel_id) = self.channel_id - && let Err(error) = save_persisted_checkpoint(&channel_id, &checkpoint) - { - error!("failed to save sequencer checkpoint: {error}"); - } - } - - fn load_checkpoint(&self) -> Option<&SequencerCheckpoint> { - self.checkpoint.as_ref() - } -} - -/// Load the persisted sequencer checkpoint after validating the target channel. -pub fn load_or_discard_persisted_checkpoint_for_channel( - channel_id: &ChannelId, - channel_exists: bool, -) -> Result, Box> { - let Ok(bytes) = fs::read(CHECKPOINT_FILE) else { - return Ok(None); - }; - let file = serde_json::from_slice::(&bytes)?; - if file.version != CHECKPOINT_VERSION { - return Err(format!( - "unsupported {CHECKPOINT_KIND} version {}; expected {CHECKPOINT_VERSION}", - file.version - ) - .into()); - } - if file.kind != CHECKPOINT_KIND { - return Err(format!( - "unsupported checkpoint kind '{}'; expected '{CHECKPOINT_KIND}'", - file.kind - ) - .into()); - } - let expected_channel_id = hex::encode(channel_id.as_ref()); - let checkpoint_channel_id = file.channel_id.ok_or_else(|| { - format!("checkpoint '{CHECKPOINT_FILE}' is missing channel_id; remove it or recreate it") - })?; - if checkpoint_channel_id != expected_channel_id { - discard_stale_checkpoint(&format!( - "checkpoint channel_id {checkpoint_channel_id} does not match requested channel_id {expected_channel_id}" - ))?; - return Ok(None); - } - if !channel_exists { - discard_stale_checkpoint(&format!( - "checkpoint channel_id {expected_channel_id} was accepted by file validation, but the node has no channel state for it" - ))?; - return Ok(None); - } - Ok(Some(bincode::deserialize(&hex::decode(file.checkpoint)?)?)) -} - -fn discard_stale_checkpoint(reason: &str) -> Result<(), Box> { - warn!("discarding stale sequencer checkpoint '{CHECKPOINT_FILE}': {reason}"); - fs::remove_file(CHECKPOINT_FILE)?; - Ok(()) -} - -/// Persist the sequencer checkpoint shared by all TUI zone commands. -pub fn save_persisted_checkpoint( - channel_id: &ChannelId, - checkpoint: &SequencerCheckpoint, -) -> Result<(), Box> { - let file = CheckpointFile { - version: CHECKPOINT_VERSION, - kind: CHECKPOINT_KIND.to_owned(), - channel_id: Some(hex::encode(channel_id.as_ref())), - checkpoint: hex::encode(bincode::serialize(checkpoint)?), - }; - fs::write(CHECKPOINT_FILE, serde_json::to_vec_pretty(&file)?)?; - Ok(()) -} - -impl InMemoryZoneState { - /// Create in-memory TUI state with a channel-validated runtime checkpoint. - pub fn for_channel( - channel_id: ChannelId, - channel_exists: bool, - ) -> Result> { - Ok(Self { - pending: Vec::new(), - finalized: Vec::new(), - finalized_payloads: HashSet::new(), - checkpoint: load_or_discard_persisted_checkpoint_for_channel( - &channel_id, - channel_exists, - )?, - channel_id: Some(channel_id), - channel_view: None, - }) - } - - /// Store the latest sequencer channel view for rendering. - pub fn set_channel_view(&mut self, channel_view: SequencerChannelView) { - self.channel_view = Some(channel_view); - } - - /// Return the latest sequencer channel view, if one has been observed. - #[must_use] - pub const fn channel_view(&self) -> Option<&SequencerChannelView> { - self.channel_view.as_ref() - } - - /// True if this exact payload has finalized — used to skip re-publishing an - /// orphan whose payload is already permanently on chain. - #[must_use] - pub fn is_finalized(&self, payload: &[u8]) -> bool { - self.finalized_payloads.contains(payload) - } -} diff --git a/deployment/tui-zone/src/ui.rs b/deployment/tui-zone/src/ui.rs deleted file mode 100644 index 75ee57078..000000000 --- a/deployment/tui-zone/src/ui.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::io::Write as _; - -use crate::{ - message::Msg, - state::{InMemoryZoneState, ZoneState as _}, -}; - -/// Print current state as two sections: Pending, Finalized. -pub fn render_state(state: &InMemoryZoneState) { - eprintln!(); - if let Some(view) = state.channel_view() { - eprintln!("=== Sequencer ==="); - eprintln!(" Channel: {}", hex::encode(view.channel_id.as_ref())); - eprintln!(" Slot: {}", view.current_slot.into_inner()); - eprintln!( - " Accredited keys: {}", - view.accredited_key_count.unwrap_or_default() - ); - eprintln!( - " This sequencer: {}", - view.own_key_index - .map_or_else(|| "not accredited".to_owned(), |idx| format!("index {idx}")) - ); - eprintln!( - " Authorized sequencer: {}", - view.authorized_key_index - .map_or_else(|| "unknown".to_owned(), |idx| format!("index {idx}")) - ); - eprintln!( - " Status: {}", - if view.our_turn_to_write { - "our turn" - } else { - "waiting for turn" - } - ); - eprintln!( - " Posting timeframe: {}", - view.turn_to_write_slots - .map_or_else(|| "unknown".to_owned(), |slots| format!("{slots} slots")) - ); - eprintln!( - " Posting timeout: {}", - view.posting_timeout_slots - .map_or_else(|| "unknown".to_owned(), |slots| format!("{slots} slots")) - ); - eprintln!(" Queued messages: {}", view.queued_messages); - eprintln!(" Tip message: {}", hex::encode(view.tip_message.as_ref())); - eprintln!(); - } - print_section("Pending", state.pending()); - print_section("Finalized", state.finalized()); -} - -fn print_section(label: &str, msgs: &[Msg]) { - if msgs.is_empty() { - return; - } - eprintln!("=== {label} ==="); - for m in msgs { - eprintln!(" {}", m.text); - } - eprintln!(); -} - -/// Print the prompt character. -pub fn prompt() { - eprint!("> "); - std::io::stderr().flush().expect("flush stderr"); -} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index e00539184..d95c93eda 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -43,7 +43,6 @@ lb-mmr = { workspace = true } lb-node = { features = ["testing"], workspace = true } lb-storage-service = { features = ["rocksdb-backend"], workspace = true } lb-testing-framework = { workspace = true } -lb-tui-zone = { workspace = true } lb-tx-service = { workspace = true } lb-utils = { workspace = true } lb-wallet = { workspace = true } diff --git a/tests/src/cucumber/steps/manual_transactions/command_file_utils.rs b/tests/src/cucumber/steps/manual_transactions/command_file_utils.rs index d4b337b4b..eb124d6c7 100644 --- a/tests/src/cucumber/steps/manual_transactions/command_file_utils.rs +++ b/tests/src/cucumber/steps/manual_transactions/command_file_utils.rs @@ -51,7 +51,6 @@ use lb_core::mantle::{ transactions::{GasPrices, hash::TxHash}, }; use lb_key_management_system_service::keys::ZkPublicKey; -use lb_tui_zone::run_commands::{ZONE_FILE_TRANSFER_VERSION, ZONE_WALLET_FUNDS_EXPORT}; use lb_wallet::WalletError; use serde::Serialize; use tokio::time::{Instant, sleep}; @@ -759,8 +758,6 @@ fn log_wallet_state_balance(wallet_name: &str, public_key_hex: &str, state: &Wal #[derive(Serialize)] struct WalletFundsExport { - version: u8, - kind: &'static str, wallet: String, node_url: String, public_key: String, @@ -799,8 +796,6 @@ async fn export_funds( let selected = select_utxos_covering(available_utxos.clone(), value)?; let selected_value = selected.iter().map(|utxo| utxo.note.value).sum(); let export = WalletFundsExport { - version: ZONE_FILE_TRANSFER_VERSION, - kind: ZONE_WALLET_FUNDS_EXPORT, wallet: wallet.wallet_name.clone(), node_url: format!( "{}",