Skip to content

Commit 870af7e

Browse files
OttoAllmendingerllm-git
andcommitted
feat(webui): improve parser to handle multiple encodings
Improve the PSBT/transaction parser to try multiple decoding methods (hex, base64) when parsing input. The parser now attempts all possible decodings and selects the first one that successfully parses, making it more robust against different input formats. Issue: BTC-0 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 0369874 commit 870af7e

1 file changed

Lines changed: 105 additions & 64 deletions

File tree

  • packages/webui/src/wasm-utxo/parser

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

Lines changed: 105 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -55,27 +55,50 @@ const networkLabels: Record<CoinName, string> = {
5555
};
5656

5757
/**
58-
* Decode hex or base64 input to bytes.
58+
* Try to decode input as hex.
5959
*/
60-
function decodeInput(input: string): Uint8Array {
60+
function tryDecodeHex(input: string): Uint8Array | null {
6161
const trimmed = input.trim();
62+
if (!/^[0-9a-fA-F]*$/.test(trimmed) || trimmed.length % 2 !== 0) {
63+
return null;
64+
}
65+
const bytes = new Uint8Array(trimmed.length / 2);
66+
for (let i = 0; i < bytes.length; i++) {
67+
bytes[i] = parseInt(trimmed.slice(i * 2, i * 2 + 2), 16);
68+
}
69+
return bytes;
70+
}
6271

63-
// Try hex first
64-
if (/^[0-9a-fA-F]+$/.test(trimmed)) {
65-
const bytes = new Uint8Array(trimmed.length / 2);
66-
for (let i = 0; i < bytes.length; i++) {
67-
bytes[i] = parseInt(trimmed.slice(i * 2, i * 2 + 2), 16);
72+
/**
73+
* Try to decode input as base64.
74+
*/
75+
function tryDecodeBase64(input: string): Uint8Array | null {
76+
const trimmed = input.trim();
77+
try {
78+
const binary = atob(trimmed);
79+
const bytes = new Uint8Array(binary.length);
80+
for (let i = 0; i < binary.length; i++) {
81+
bytes[i] = binary.charCodeAt(i);
6882
}
6983
return bytes;
84+
} catch {
85+
return null;
7086
}
87+
}
7188

72-
// Try base64
73-
const binary = atob(trimmed);
74-
const bytes = new Uint8Array(binary.length);
75-
for (let i = 0; i < binary.length; i++) {
76-
bytes[i] = binary.charCodeAt(i);
77-
}
78-
return bytes;
89+
/**
90+
* Get all possible decodings of the input (hex, base64).
91+
*/
92+
function possibleDecodings(input: string): Uint8Array[] {
93+
const decodings: Uint8Array[] = [];
94+
95+
const hex = tryDecodeHex(input);
96+
if (hex) decodings.push(hex);
97+
98+
const base64 = tryDecodeBase64(input);
99+
if (base64) decodings.push(base64);
100+
101+
return decodings;
79102
}
80103

81104
/**
@@ -1076,77 +1099,95 @@ class PsbtTxParser extends BaseComponent {
10761099
this.expandedPaths = new Set(["root"]);
10771100
this.expandedValues = new Set();
10781101

1079-
let bytes: Uint8Array;
1080-
try {
1081-
bytes = decodeInput(input);
1082-
} catch (e) {
1083-
errorEl.replaceChildren(h("div", { class: "error-message" }, `Failed to decode input: ${e}`));
1102+
// Get all possible decodings (hex, base64)
1103+
const decodings = possibleDecodings(input);
1104+
if (decodings.length === 0) {
1105+
errorEl.replaceChildren(
1106+
h("div", { class: "error-message" }, "Failed to decode input: not valid hex or base64"),
1107+
);
10841108
resultsEl.replaceChildren(
10851109
h("div", { class: "empty-state" }, h("p", {}, "Enter valid hex or base64 data")),
10861110
);
10871111
return;
10881112
}
10891113

1090-
// Auto-detect type (PSBT vs TX)
1091-
const detectedPsbt = isPsbt(bytes);
1092-
10931114
// Parse based on mode with network handling
1094-
let node: Node;
1115+
// Try all decodings and pick the first one that parses successfully
1116+
let node: Node | null = null;
10951117
let detectedNetwork: CoinName | null = null;
1096-
1097-
try {
1098-
if (this.autoDetectNetwork) {
1099-
// Try all networks and pick the first one that works
1100-
if (this.currentMode === "psbt") {
1101-
const result = tryParsePsbt(bytes);
1102-
if (result) {
1103-
detectedNetwork = result.network;
1104-
this.currentNetwork = result.network;
1105-
node = result.node;
1118+
let successBytes: Uint8Array | null = null;
1119+
let lastError: string | null = null;
1120+
1121+
for (const bytes of decodings) {
1122+
try {
1123+
if (this.autoDetectNetwork) {
1124+
// Try all networks and pick the first one that works
1125+
if (this.currentMode === "psbt") {
1126+
const result = tryParsePsbt(bytes);
1127+
if (result) {
1128+
detectedNetwork = result.network;
1129+
this.currentNetwork = result.network;
1130+
node = result.node;
1131+
successBytes = bytes;
1132+
break;
1133+
}
1134+
} else if (this.currentMode === "psbt-raw") {
1135+
const result = tryParsePsbtRaw(bytes);
1136+
if (result) {
1137+
detectedNetwork = result.network;
1138+
this.currentNetwork = result.network;
1139+
node = result.node;
1140+
successBytes = bytes;
1141+
break;
1142+
}
11061143
} else {
1107-
throw new Error("Failed to parse PSBT with any known network");
1108-
}
1109-
} else if (this.currentMode === "psbt-raw") {
1110-
const result = tryParsePsbtRaw(bytes);
1111-
if (result) {
1112-
detectedNetwork = result.network;
1113-
this.currentNetwork = result.network;
1114-
node = result.node;
1115-
} else {
1116-
throw new Error("Failed to parse raw PSBT with any known network");
1144+
const result = tryParseTx(bytes);
1145+
if (result) {
1146+
detectedNetwork = result.network;
1147+
this.currentNetwork = result.network;
1148+
node = result.node;
1149+
successBytes = bytes;
1150+
break;
1151+
}
11171152
}
11181153
} else {
1119-
const result = tryParseTx(bytes);
1120-
if (result) {
1121-
detectedNetwork = result.network;
1122-
this.currentNetwork = result.network;
1123-
node = result.node;
1124-
} else {
1125-
throw new Error("Failed to parse transaction with any known network");
1154+
// Use the specified network
1155+
switch (this.currentMode) {
1156+
case "psbt":
1157+
node = parsePsbtToNode(bytes, this.currentNetwork);
1158+
break;
1159+
case "psbt-raw":
1160+
node = parsePsbtRawToNode(bytes, this.currentNetwork);
1161+
break;
1162+
case "tx":
1163+
node = parseTxToNode(bytes, this.currentNetwork);
1164+
break;
11261165
}
1127-
}
1128-
} else {
1129-
// Use the specified network
1130-
switch (this.currentMode) {
1131-
case "psbt":
1132-
node = parsePsbtToNode(bytes, this.currentNetwork);
1133-
break;
1134-
case "psbt-raw":
1135-
node = parsePsbtRawToNode(bytes, this.currentNetwork);
1136-
break;
1137-
case "tx":
1138-
node = parseTxToNode(bytes, this.currentNetwork);
1166+
if (node) {
1167+
successBytes = bytes;
11391168
break;
1169+
}
11401170
}
1171+
} catch (e) {
1172+
lastError = String(e);
1173+
// Continue trying other decodings
11411174
}
1142-
} catch (e) {
1143-
errorEl.replaceChildren(h("div", { class: "error-message" }, `Parse error: ${e}`));
1175+
}
1176+
1177+
if (!node) {
1178+
const errorMsg = lastError
1179+
? `Parse error (tried hex and base64): ${lastError}`
1180+
: "Failed to parse with any encoding";
1181+
errorEl.replaceChildren(h("div", { class: "error-message" }, errorMsg));
11441182
resultsEl.replaceChildren(
11451183
h("div", { class: "empty-state" }, h("p", {}, "Failed to parse data")),
11461184
);
11471185
return;
11481186
}
11491187

1188+
// Auto-detect type (PSBT vs TX) based on successfully decoded bytes
1189+
const detectedPsbt = successBytes ? isPsbt(successBytes) : false;
1190+
11501191
// Update detected type display
11511192
const typeStr = detectedPsbt ? "PSBT" : "Transaction";
11521193
const networkStr = detectedNetwork

0 commit comments

Comments
 (0)