Skip to content

Commit b747706

Browse files
OttoAllmendingerllm-git
andcommitted
feat(webui): add PSBT/TX parser feature with sample data
Add PSBT/TX parser component that allows inspecting PSBTs and transactions with a collapsible tree view. This new tool helps users and developers to analyze UTXO transaction structures. Key additions: - Parse and display Bitcoin format PSBTs and raw transactions - Extract sample data from test fixtures for quick testing - Support auto-detection of transaction format - Collapsible tree view with search, copy and expand/collapse features - Share functionality to save and share parser state Issue: BTC-0 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 136e8f5 commit b747706

7 files changed

Lines changed: 1673 additions & 29 deletions

File tree

package-lock.json

Lines changed: 2 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webui/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
"main": "./dist/src/index.js",
1212
"private": true,
1313
"scripts": {
14-
"build": "webpack --mode production --progress --config ./webpack.config.js",
14+
"extract-samples": "node scripts/extract-samples.js",
15+
"build": "npm run extract-samples && webpack --mode production --progress --config ./webpack.config.js",
1516
"typecheck": "tsc --noEmit",
1617
"test": "echo \"Error: no test specified\"",
17-
"dev": "webpack serve --mode development --progress --hot --config ./webpack.config.js",
18+
"dev": "npm run extract-samples && webpack serve --mode development --progress --hot --config ./webpack.config.js",
1819
"fmt": "prettier --write .",
1920
"check-fmt": "prettier --check '{src,webpack}/**/*.{tsx,ts,js}'",
2021
"clean": "rm -r ./dist",
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Extract sample PSBT/TX data from fixtures for the parser UI.
4+
*
5+
* This script reads fixture files and extracts:
6+
* - psbtBase64: unsigned or partially signed PSBT
7+
* - psbtBase64Finalized: fully signed PSBT
8+
* - extractedTransaction: raw transaction hex
9+
*
10+
* Usage: node scripts/extract-samples.js
11+
*/
12+
13+
const fs = require("fs");
14+
const path = require("path");
15+
16+
const FIXTURES_DIR = path.resolve(__dirname, "../src/fixtures/fixed-script");
17+
const OUTPUT_FILE = path.resolve(__dirname, "../src/wasm-utxo/parser/samples.ts");
18+
19+
function extractSamples() {
20+
const samples = [];
21+
22+
// Read fixture files
23+
const files = fs.readdirSync(FIXTURES_DIR).filter((f) => f.endsWith(".json") && f.startsWith("psbt"));
24+
25+
for (const file of files) {
26+
const filePath = path.join(FIXTURES_DIR, file);
27+
const content = fs.readFileSync(filePath, "utf-8");
28+
29+
let fixture;
30+
try {
31+
fixture = JSON.parse(content);
32+
} catch {
33+
console.warn(`Skipping invalid JSON: ${file}`);
34+
continue;
35+
}
36+
37+
// Extract name from filename: psbt-lite.bitcoin.unsigned.json -> Bitcoin Lite (unsigned)
38+
const match = file.match(/^psbt(-lite)?\.(\w+)\.(\w+)\.json$/);
39+
if (!match) continue;
40+
41+
const [, lite, network, state] = match;
42+
const networkName = network.charAt(0).toUpperCase() + network.slice(1);
43+
const stateName = state.charAt(0).toUpperCase() + state.slice(1);
44+
const liteLabel = lite ? " Lite" : "";
45+
46+
// Add psbtBase64 (unsigned/halfsigned)
47+
if (fixture.psbtBase64 && state !== "fullsigned") {
48+
samples.push({
49+
name: `${networkName}${liteLabel} PSBT (${stateName})`,
50+
type: "psbt",
51+
data: fixture.psbtBase64,
52+
});
53+
}
54+
55+
// Add psbtBase64Finalized (fullsigned)
56+
if (fixture.psbtBase64Finalized) {
57+
samples.push({
58+
name: `${networkName}${liteLabel} PSBT (Finalized)`,
59+
type: "psbt",
60+
data: fixture.psbtBase64Finalized,
61+
});
62+
}
63+
64+
// Add extractedTransaction
65+
if (fixture.extractedTransaction) {
66+
samples.push({
67+
name: `${networkName}${liteLabel} TX (Extracted)`,
68+
type: "tx",
69+
data: fixture.extractedTransaction,
70+
});
71+
}
72+
}
73+
74+
// Sort by name
75+
samples.sort((a, b) => a.name.localeCompare(b.name));
76+
77+
return samples;
78+
}
79+
80+
function generateTypeScript(samples) {
81+
const lines = [
82+
"/**",
83+
" * Sample PSBT/TX data extracted from test fixtures.",
84+
" * Auto-generated by scripts/extract-samples.js",
85+
" * DO NOT EDIT MANUALLY",
86+
" */",
87+
"",
88+
"export interface Sample {",
89+
" name: string;",
90+
' type: "psbt" | "tx";',
91+
" data: string;",
92+
"}",
93+
"",
94+
"export const samples: Sample[] = [",
95+
];
96+
97+
for (const sample of samples) {
98+
lines.push(" {");
99+
lines.push(` name: ${JSON.stringify(sample.name)},`);
100+
lines.push(` type: ${JSON.stringify(sample.type)},`);
101+
lines.push(` data: ${JSON.stringify(sample.data)},`);
102+
lines.push(" },");
103+
}
104+
105+
lines.push("];");
106+
lines.push("");
107+
108+
return lines.join("\n");
109+
}
110+
111+
function main() {
112+
console.log("Extracting samples from fixtures...");
113+
114+
const samples = extractSamples();
115+
console.log(`Found ${samples.length} samples`);
116+
117+
const typescript = generateTypeScript(samples);
118+
119+
// Ensure output directory exists
120+
const outputDir = path.dirname(OUTPUT_FILE);
121+
if (!fs.existsSync(outputDir)) {
122+
fs.mkdirSync(outputDir, { recursive: true });
123+
}
124+
125+
fs.writeFileSync(OUTPUT_FILE, typescript);
126+
console.log(`Written to ${OUTPUT_FILE}`);
127+
}
128+
129+
main();
130+

packages/webui/src/fixtures

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/home/otto/src/BitGo/BitGoWASM/packages/wasm-utxo/test/fixtures

packages/webui/src/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { initRouter, type Route } from "./lib/router";
99

1010
// Import demo components (registers them as custom elements)
1111
import "./wasm-utxo/addresses";
12+
import "./wasm-utxo/parser";
1213

1314
// Common styles used across components
1415
export const commonStyles = `
@@ -122,7 +123,7 @@ class HomePage extends BaseComponent {
122123
"div",
123124
{ class: "home" },
124125
h("h1", {}, "BitGoWASM Demos"),
125-
h("p", { class: "subtitle" }, "Interactive demos for BitGo WASM libraries"),
126+
h("p", { class: "subtitle" }, "Developer tools for BitGoWASM libraries"),
126127
h(
127128
"ul",
128129
{ class: "demo-list" },
@@ -140,6 +141,20 @@ class HomePage extends BaseComponent {
140141
),
141142
),
142143
),
144+
h(
145+
"li",
146+
{},
147+
h(
148+
"a",
149+
{ class: "demo-link", href: "#/wasm-utxo/parser" },
150+
h("div", { class: "demo-title" }, "UTXO PSBT/TX Parser"),
151+
h(
152+
"div",
153+
{ class: "demo-desc" },
154+
"Parse and inspect PSBTs and transactions as collapsible trees",
155+
),
156+
),
157+
),
143158
),
144159
),
145160
);
@@ -152,6 +167,7 @@ defineComponent("home-page", HomePage);
152167
const routes: Route[] = [
153168
{ path: "/", component: "home-page" },
154169
{ path: "/wasm-utxo/addresses", component: "address-converter" },
170+
{ path: "/wasm-utxo/parser", component: "psbt-tx-parser" },
155171
];
156172

157173
// Initialize router when DOM is ready

0 commit comments

Comments
 (0)