Skip to content

Commit 822f3f1

Browse files
authored
Merge pull request #183 from BitGo/BTC-0.new-webui
feat(webui): add PSBT/TX parser and improve address converter
2 parents 32010d5 + ce42c88 commit 822f3f1

11 files changed

Lines changed: 1927 additions & 4 deletions

File tree

.github/workflows/deploy-pages.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: "Deploy to GitHub Pages"
33
on:
44
push:
55
branches:
6-
- gh-pages
6+
- master
77
workflow_dispatch:
88

99
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages

packages/webui/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
dist/
2+
wasm/

packages/webui/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
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:wasm": "bash scripts/build-wasm.sh",
16+
"build": "npm run build:wasm && npm run extract-samples && webpack --mode production --progress --config ./webpack.config.js",
1517
"typecheck": "tsc --noEmit",
1618
"test": "echo \"Error: no test specified\"",
17-
"dev": "webpack serve --mode development --progress --hot --config ./webpack.config.js",
19+
"dev": "npm run build:wasm && npm run extract-samples && webpack serve --mode development --progress --hot --config ./webpack.config.js",
1820
"fmt": "prettier --write .",
1921
"check-fmt": "prettier --check '{src,webpack}/**/*.{tsx,ts,js}'",
2022
"clean": "rm -r ./dist",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5+
WEBUI_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
6+
WASM_UTXO_DIR="$(cd "$WEBUI_DIR/../wasm-utxo" && pwd)"
7+
OUT_DIR="$WEBUI_DIR/wasm"
8+
9+
# Auto-detect Mac and use Homebrew LLVM for WASM compilation
10+
# Apple's Clang doesn't support wasm32-unknown-unknown target
11+
if [[ "$(uname -s)" == "Darwin" ]]; then
12+
HOMEBREW_LLVM="$(brew --prefix llvm 2>/dev/null || true)"
13+
if [[ -n "$HOMEBREW_LLVM" ]]; then
14+
export CC="$HOMEBREW_LLVM/bin/clang"
15+
export AR="$HOMEBREW_LLVM/bin/llvm-ar"
16+
echo "Using Homebrew LLVM: $HOMEBREW_LLVM"
17+
fi
18+
fi
19+
20+
echo "Building wasm-utxo with inspect feature..."
21+
rm -rf "$OUT_DIR"
22+
wasm-pack build \
23+
--no-opt \
24+
--no-pack \
25+
--weak-refs \
26+
"$WASM_UTXO_DIR" \
27+
--out-dir "$OUT_DIR" \
28+
--target bundler \
29+
--features inspect
30+
31+
echo "Optimizing wasm..."
32+
wasm-opt \
33+
--enable-bulk-memory \
34+
--enable-nontrapping-float-to-int \
35+
--enable-sign-ext \
36+
-Oz \
37+
"$OUT_DIR"/wasm_utxo_bg.wasm \
38+
-o "$OUT_DIR"/wasm_utxo_bg.wasm
39+
40+
# Remove .gitignore files that wasm-pack generates
41+
find "$OUT_DIR" -name .gitignore -delete
42+
43+
# Copy .d.ts to wasm-utxo/js/wasm/ so ts-loader (project references) sees inspect types.
44+
# This is safe: js/wasm/ is a build artifact, and the npm publish uses dist/ which is already built.
45+
cp "$OUT_DIR"/wasm_utxo.d.ts "$WASM_UTXO_DIR/js/wasm/"
46+
47+
echo "wasm build complete: $OUT_DIR"
48+
ls -lh "$OUT_DIR"/*.wasm
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/fullsigned)
47+
if (fixture.psbtBase64) {
48+
samples.push({
49+
name: `${networkName}${liteLabel} PSBT (${stateName})`,
50+
type: "psbt",
51+
data: fixture.psbtBase64,
52+
});
53+
}
54+
55+
// Add psbtBase64Finalized (from fullsigned files only)
56+
if (fixture.psbtBase64Finalized && state === "fullsigned") {
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+
../../wasm-utxo/test/fixtures

packages/webui/src/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { initRouter, type Route } from "./lib/router";
1010
// Import demo components (registers them as custom elements)
1111
import "./wasm-utxo/addresses";
1212
import "./wasm-solana/transaction";
13+
import "./wasm-utxo/parser";
1314

1415
// Common styles used across components
1516
export const commonStyles = `
@@ -33,6 +34,10 @@ export const commonStyles = `
3334
text-decoration: underline;
3435
}
3536
37+
button, input, textarea, select {
38+
font-family: inherit;
39+
}
40+
3641
h1, h2, h3 {
3742
margin: 0 0 1rem;
3843
font-weight: 500;
@@ -119,7 +124,7 @@ class HomePage extends BaseComponent {
119124
"div",
120125
{ class: "home" },
121126
h("h1", {}, "BitGoWASM Demos"),
122-
h("p", { class: "subtitle" }, "Interactive demos for BitGo WASM libraries"),
127+
h("p", { class: "subtitle" }, "Developer tools for BitGoWASM libraries"),
123128
h(
124129
"ul",
125130
{ class: "demo-list" },
@@ -137,6 +142,20 @@ class HomePage extends BaseComponent {
137142
),
138143
),
139144
),
145+
h(
146+
"li",
147+
{},
148+
h(
149+
"a",
150+
{ class: "demo-link", href: "#/wasm-utxo/parser" },
151+
h("div", { class: "demo-title" }, "UTXO PSBT/TX Parser"),
152+
h(
153+
"div",
154+
{ class: "demo-desc" },
155+
"Parse and inspect PSBTs and transactions as collapsible trees",
156+
),
157+
),
158+
),
140159
h(
141160
"li",
142161
{},
@@ -163,6 +182,7 @@ defineComponent("home-page", HomePage);
163182
const routes: Route[] = [
164183
{ path: "/", component: "home-page" },
165184
{ path: "/wasm-utxo/addresses", component: "address-converter" },
185+
{ path: "/wasm-utxo/parser", component: "psbt-tx-parser" },
166186
{ path: "/wasm-solana/transaction", component: "solana-transaction-parser" },
167187
];
168188

packages/webui/src/wasm-utxo/addresses/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,16 @@ class AddressConverter extends BaseComponent {
339339
margin: 0;
340340
}
341341
342+
.disclaimer {
343+
padding: 0.75rem 1rem;
344+
background: rgba(207, 172, 83, 0.1);
345+
border: 1px solid rgba(207, 172, 83, 0.4);
346+
border-radius: 6px;
347+
color: var(--warning, #CFAC53);
348+
margin-bottom: 1.5rem;
349+
font-size: 0.875rem;
350+
}
351+
342352
@media (max-width: 768px) {
343353
.result-row {
344354
grid-template-columns: 1fr auto;
@@ -370,6 +380,11 @@ class AddressConverter extends BaseComponent {
370380
h("span", {}, "UTXO Address Converter"),
371381
),
372382
h("h1", {}, "UTXO Address Converter"),
383+
h(
384+
"div",
385+
{ class: "disclaimer" },
386+
"⚠ Careful: funds sent to addresses of other networks may be lost forever!",
387+
),
373388
h(
374389
"section",
375390
{ class: "input-section" },
@@ -380,6 +395,9 @@ class AddressConverter extends BaseComponent {
380395
id: "address-input",
381396
placeholder: "Paste a utxo address...",
382397
rows: "2",
398+
spellcheck: "false",
399+
autocomplete: "off",
400+
style: "resize: none",
383401
oninput: (e: Event) => this.handleInput(e),
384402
}),
385403
h(

0 commit comments

Comments
 (0)