Skip to content

Commit f222325

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): improve code organization and add transaction parsing
Reorganize code structure with better modularization and add transaction parsing capability. This includes: - Create a shared input handling module - Support for transaction parsing via new `tx parse` command - Move parsers into organized submodules - Improve file reading with better error handling - Add support for decoding different input formats (hex/base64/raw) - Fix clippy warnings in existing code Issue: BTC-2652 Co-authored-by: llm-git <[email protected]>
1 parent 85f472c commit f222325

File tree

12 files changed

+326
-95
lines changed

12 files changed

+326
-95
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use anyhow::{Context, Result};
2+
use base64::Engine;
3+
use std::fs;
4+
use std::io::{self, Read};
5+
use std::path::PathBuf;
6+
7+
/// Decode input bytes, attempting to interpret as base64, hex, or raw bytes
8+
pub fn decode_input(raw_bytes: &[u8]) -> Result<Vec<u8>> {
9+
// Try to interpret as text first (for base64/hex encoded input)
10+
if let Ok(text) = std::str::from_utf8(raw_bytes) {
11+
let trimmed = text.trim();
12+
13+
// Try hex first (more common format)
14+
if let Ok(decoded) = hex::decode(trimmed) {
15+
return Ok(decoded);
16+
}
17+
18+
// Try base64
19+
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(trimmed) {
20+
return Ok(decoded);
21+
}
22+
}
23+
24+
// Fall back to raw bytes
25+
Ok(raw_bytes.to_vec())
26+
}
27+
28+
/// Read bytes from a file path or stdin (if path is "-")
29+
pub fn read_input_bytes(path: &PathBuf, file_type: &str) -> Result<Vec<u8>> {
30+
if path.to_str() == Some("-") {
31+
// Read from stdin
32+
let mut buffer = Vec::new();
33+
io::stdin()
34+
.read_to_end(&mut buffer)
35+
.context("Failed to read from stdin")?;
36+
Ok(buffer)
37+
} else {
38+
// Read from file
39+
fs::read(path)
40+
.with_context(|| format!("Failed to read {} file: {}", file_type, path.display()))
41+
}
42+
}

packages/wasm-utxo/cli/src/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ use clap::{Parser, Subcommand};
33

44
mod address;
55
mod format;
6+
mod input;
67
mod node;
7-
mod parse_node;
8-
mod parse_node_raw;
8+
mod parse;
99
mod psbt;
10+
mod tx;
1011

1112
#[cfg(test)]
1213
pub mod test_utils;
@@ -32,6 +33,11 @@ enum Commands {
3233
#[command(subcommand)]
3334
command: psbt::PsbtCommand,
3435
},
36+
/// Transaction parsing and inspection operations
37+
Tx {
38+
#[command(subcommand)]
39+
command: tx::TxCommand,
40+
},
3541
}
3642

3743
fn main() -> Result<()> {
@@ -40,5 +46,6 @@ fn main() -> Result<()> {
4046
match cli.command {
4147
Commands::Address { command } => address::handle_command(command),
4248
Commands::Psbt { command } => psbt::handle_command(command),
49+
Commands::Tx { command } => tx::handle_command(command),
4350
}
4451
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub mod node;
2+
pub mod node_raw;
3+
4+
pub use node::{parse_psbt_bytes_internal, parse_tx_bytes_internal};
5+
pub use node_raw::parse_psbt_bytes_raw;

packages/wasm-utxo/cli/src/parse_node.rs renamed to packages/wasm-utxo/cli/src/parse/node.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ pub fn psbt_to_node(psbt: &Psbt, network: Network) -> Node {
103103

104104
psbt_node.add_child(xpubs_to_node(&psbt.xpub));
105105

106-
if psbt.proprietary.len() > 0 {
106+
if !psbt.proprietary.is_empty() {
107107
let mut proprietary_node =
108108
Node::new("proprietary", Primitive::U64(psbt.proprietary.len() as u64));
109109
proprietary_node.extend(proprietary_to_nodes(&psbt.proprietary));
@@ -177,7 +177,7 @@ pub fn psbt_to_node(psbt: &Psbt, network: Network) -> Node {
177177

178178
input_node.extend(bip32_derivations_to_nodes(&input.bip32_derivation));
179179

180-
if input.proprietary.len() > 0 {
180+
if !input.proprietary.is_empty() {
181181
let mut prop_node = Node::new(
182182
"proprietary",
183183
Primitive::U64(input.proprietary.len() as u64),
@@ -203,7 +203,7 @@ pub fn psbt_to_node(psbt: &Psbt, network: Network) -> Node {
203203
output_node.add_child(script_buf_to_node("witness_script", script));
204204
}
205205

206-
if output.proprietary.len() > 0 {
206+
if !output.proprietary.is_empty() {
207207
let mut prop_node = Node::new(
208208
"proprietary",
209209
Primitive::U64(output.proprietary.len() as u64),
@@ -339,4 +339,22 @@ mod tests {
339339
assert_tree_matches_fixture(&node, "psbt_bitcoin_fullsigned")?;
340340
Ok(())
341341
}
342+
343+
#[test]
344+
fn test_parse_tx_bitcoin_fullsigned() -> Result<(), Box<dyn std::error::Error>> {
345+
use crate::format::fixtures::assert_tree_matches_fixture;
346+
use crate::test_utils::{load_tx_bytes, SignatureState, TxFormat};
347+
use wasm_utxo::Network as WasmNetwork;
348+
349+
let tx_bytes = load_tx_bytes(
350+
WasmNetwork::Bitcoin,
351+
SignatureState::Fullsigned,
352+
TxFormat::PsbtLite,
353+
)?;
354+
355+
let node = parse_tx_bytes_internal(&tx_bytes)?;
356+
357+
assert_tree_matches_fixture(&node, "tx_bitcoin_fullsigned")?;
358+
Ok(())
359+
}
342360
}

packages/wasm-utxo/cli/src/parse_node_raw.rs renamed to packages/wasm-utxo/cli/src/parse/node_raw.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/// # Example
1515
///
1616
/// ```ignore
17-
/// use parse_node_raw::parse_psbt_bytes_raw;
17+
/// use parse::node_raw::parse_psbt_bytes_raw;
1818
///
1919
/// let psbt_bytes = /* your PSBT data */;
2020
/// let node = parse_psbt_bytes_raw(&psbt_bytes)?;

packages/wasm-utxo/cli/src/psbt.rs

Lines changed: 0 additions & 89 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use anyhow::Result;
2+
use clap::Subcommand;
3+
4+
mod parse;
5+
6+
#[derive(Subcommand)]
7+
pub enum PsbtCommand {
8+
/// Parse a PSBT file and display its contents
9+
Parse {
10+
/// Path to the PSBT file (use '-' to read from stdin)
11+
path: std::path::PathBuf,
12+
/// Disable colored output
13+
#[arg(long)]
14+
no_color: bool,
15+
/// Show raw key-value pairs instead of parsed structure
16+
#[arg(long)]
17+
raw: bool,
18+
},
19+
}
20+
21+
pub fn handle_command(command: PsbtCommand) -> Result<()> {
22+
match command {
23+
PsbtCommand::Parse {
24+
path,
25+
no_color,
26+
raw,
27+
} => parse::handle_parse_command(path, no_color, raw),
28+
}
29+
}
30+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use anyhow::Result;
2+
use std::path::PathBuf;
3+
4+
use crate::format::{render_tree_with_scheme, ColorScheme};
5+
use crate::input::{decode_input, read_input_bytes};
6+
use crate::parse::{parse_psbt_bytes_internal, parse_psbt_bytes_raw};
7+
8+
pub fn handle_parse_command(path: PathBuf, no_color: bool, raw: bool) -> Result<()> {
9+
// Read from file or stdin
10+
let raw_bytes = read_input_bytes(&path, "PSBT")?;
11+
12+
// Decode input (auto-detect hex, base64, or raw bytes)
13+
let bytes = decode_input(&raw_bytes)?;
14+
15+
let node = if raw {
16+
parse_psbt_bytes_raw(&bytes)
17+
.map_err(|e| anyhow::anyhow!("Failed to parse PSBT (raw): {}", e))?
18+
} else {
19+
parse_psbt_bytes_internal(&bytes)
20+
.map_err(|e| anyhow::anyhow!("Failed to parse PSBT: {}", e))?
21+
};
22+
23+
let color_scheme = if no_color {
24+
ColorScheme::no_color()
25+
} else {
26+
ColorScheme::default()
27+
};
28+
29+
render_tree_with_scheme(&node, &color_scheme)?;
30+
31+
Ok(())
32+
}
33+

packages/wasm-utxo/cli/src/test_utils.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ struct PsbtFixtureBase64 {
4444
psbt_base64: String,
4545
}
4646

47+
#[derive(Deserialize)]
48+
struct TxFixture {
49+
#[serde(rename = "extractedTransaction")]
50+
extracted_transaction: String,
51+
}
52+
4753
/// Load PSBT bytes from a fixture file
4854
///
4955
/// # Arguments
@@ -87,3 +93,47 @@ pub fn load_psbt_bytes(
8793
let psbt_bytes = general_purpose::STANDARD.decode(&fixture.psbt_base64)?;
8894
Ok(psbt_bytes)
8995
}
96+
97+
/// Load transaction bytes from a fixture file's extractedTransaction field
98+
///
99+
/// # Arguments
100+
/// * `network` - The network type
101+
/// * `signature_state` - The signature state of the transaction
102+
/// * `tx_format` - The transaction format (Psbt or PsbtLite)
103+
///
104+
/// # Example
105+
/// ```rust,no_run
106+
/// use cli::test_utils::*;
107+
/// use wasm_utxo::Network;
108+
///
109+
/// let tx_bytes = load_tx_bytes(
110+
/// Network::Bitcoin,
111+
/// SignatureState::Fullsigned,
112+
/// TxFormat::PsbtLite
113+
/// ).expect("Failed to load fixture");
114+
/// ```
115+
pub fn load_tx_bytes(
116+
network: Network,
117+
signature_state: SignatureState,
118+
tx_format: TxFormat,
119+
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
120+
let filename = format!(
121+
"{}.{}.{}.json",
122+
tx_format.as_str(),
123+
network.to_utxolib_name(),
124+
signature_state.as_str()
125+
);
126+
let path = format!(
127+
"{}/test/fixtures/fixed-script/{}",
128+
env!("CARGO_MANIFEST_DIR"),
129+
filename
130+
);
131+
132+
let contents = std::fs::read_to_string(&path)
133+
.unwrap_or_else(|_| panic!("Failed to load fixture: {}", path));
134+
135+
let fixture: TxFixture = serde_json::from_str(&contents)?;
136+
137+
let tx_bytes = hex::decode(&fixture.extracted_transaction)?;
138+
Ok(tx_bytes)
139+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use anyhow::Result;
2+
use clap::Subcommand;
3+
4+
mod parse;
5+
6+
#[derive(Subcommand)]
7+
pub enum TxCommand {
8+
/// Parse a transaction file and display its contents
9+
Parse {
10+
/// Path to the transaction file (use '-' to read from stdin)
11+
path: std::path::PathBuf,
12+
/// Disable colored output
13+
#[arg(long)]
14+
no_color: bool,
15+
},
16+
}
17+
18+
pub fn handle_command(command: TxCommand) -> Result<()> {
19+
match command {
20+
TxCommand::Parse { path, no_color } => parse::handle_parse_command(path, no_color),
21+
}
22+
}
23+

0 commit comments

Comments
 (0)