Skip to content

Commit d759aa3

Browse files
OttoAllmendingerllm-git
andcommitted
feat(webui): enhance PSBT parser UI with expandable values
Improve the extract-samples script to properly handle fullsigned PSBTs and enhance the PSBT parser UI with expandable buffer values for better readability. Long hex values are now truncated by default but can be expanded with a click, providing a more user-friendly experience. Issue: BTC-0 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 16ed31a commit d759aa3

3 files changed

Lines changed: 162 additions & 24 deletions

File tree

packages/webui/scripts/extract-samples.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,17 @@ function extractSamples() {
4343
const stateName = state.charAt(0).toUpperCase() + state.slice(1);
4444
const liteLabel = lite ? " Lite" : "";
4545

46-
// Add psbtBase64 (unsigned/halfsigned)
47-
if (fixture.psbtBase64 && state !== "fullsigned") {
46+
// Add psbtBase64 (unsigned/halfsigned/fullsigned)
47+
if (fixture.psbtBase64) {
4848
samples.push({
4949
name: `${networkName}${liteLabel} PSBT (${stateName})`,
5050
type: "psbt",
5151
data: fixture.psbtBase64,
5252
});
5353
}
5454

55-
// Add psbtBase64Finalized (fullsigned)
56-
if (fixture.psbtBase64Finalized) {
55+
// Add psbtBase64Finalized (from fullsigned files only)
56+
if (fixture.psbtBase64Finalized && state === "fullsigned") {
5757
samples.push({
5858
name: `${networkName}${liteLabel} PSBT (Finalized)`,
5959
type: "psbt",

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

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,12 @@ function isPsbt(bytes: Uint8Array): boolean {
9999
/**
100100
* Format a primitive value for display.
101101
*/
102-
function formatPrimitive(primitive: Primitive): string {
102+
function formatPrimitive(primitive: Primitive, expanded: boolean = false): string {
103103
if (primitive.type === "None") return "";
104104
if (primitive.type === "Buffer") {
105105
const hex = String(primitive.value ?? "");
106-
if (hex.length > 64) {
106+
// Only truncate when hiding more than 8 characters (64 shown + 8 hidden = 72)
107+
if (!expanded && hex.length > 72) {
107108
return hex.slice(0, 32) + "..." + hex.slice(-32);
108109
}
109110
return hex;
@@ -114,6 +115,16 @@ function formatPrimitive(primitive: Primitive): string {
114115
return String(primitive.value ?? "");
115116
}
116117

118+
/**
119+
* Check if a buffer value is truncatable.
120+
*/
121+
function isTruncatable(primitive: Primitive): boolean {
122+
if (primitive.type !== "Buffer") return false;
123+
const hex = String(primitive.value ?? "");
124+
// Only expandable when truncation would hide more than 8 characters
125+
return hex.length > 72;
126+
}
127+
117128
/**
118129
* Get full value for copying.
119130
*/
@@ -148,16 +159,25 @@ function renderTreeNode(
148159
node: Node,
149160
expandedPaths: Set<string>,
150161
onToggle: (path: string) => void,
162+
expandedValues: Set<string>,
163+
onToggleValue: (path: string) => void,
151164
path: string = "root",
152165
): HTMLElement {
153166
const hasChildren = node.children.length > 0;
154167
const isExpanded = expandedPaths.has(path);
155168
const hasValue = node.value.type !== "None";
169+
const isValueExpanded = expandedValues.has(path);
170+
const canExpandValue = isTruncatable(node.value);
156171

157172
const toggleExpand = () => {
158173
onToggle(path);
159174
};
160175

176+
const toggleValueExpand = (e: Event) => {
177+
e.stopPropagation();
178+
onToggleValue(path);
179+
};
180+
161181
// Leaf node - just show label: value
162182
if (!hasChildren) {
163183
return h(
@@ -170,10 +190,12 @@ function renderTreeNode(
170190
h(
171191
"span",
172192
{
173-
class: `tree-value tree-value-${node.value.type.toLowerCase()}`,
174-
title: getFullValue(node.value),
193+
class: `tree-value tree-value-${node.value.type.toLowerCase()}${canExpandValue ? " expandable" : ""}${isValueExpanded ? " expanded" : ""}`,
194+
title:
195+
canExpandValue && !isValueExpanded ? "Click to expand" : getFullValue(node.value),
196+
onclick: canExpandValue ? toggleValueExpand : undefined,
175197
},
176-
formatPrimitive(node.value),
198+
formatPrimitive(node.value, isValueExpanded),
177199
),
178200
h(
179201
"button",
@@ -194,7 +216,7 @@ function renderTreeNode(
194216
// Branch node - collapsible
195217
const childCount = node.children.length;
196218
const labelText = hasValue
197-
? `${node.label}: ${formatPrimitive(node.value)}`
219+
? `${node.label}: ${formatPrimitive(node.value, isValueExpanded)}`
198220
: `${node.label} [${childCount}]`;
199221

200222
return h(
@@ -214,7 +236,14 @@ function renderTreeNode(
214236
"div",
215237
{ class: "tree-children" },
216238
...node.children.map((child, i) =>
217-
renderTreeNode(child, expandedPaths, onToggle, `${path}.${i}`),
239+
renderTreeNode(
240+
child,
241+
expandedPaths,
242+
onToggle,
243+
expandedValues,
244+
onToggleValue,
245+
`${path}.${i}`,
246+
),
218247
),
219248
)
220249
: null,
@@ -227,6 +256,7 @@ function renderTreeNode(
227256
class PsbtTxParser extends BaseComponent {
228257
private debounceTimer: number | null = null;
229258
private expandedPaths: Set<string> = new Set(["root"]);
259+
private expandedValues: Set<string> = new Set();
230260
private currentMode: ParseMode = "psbt";
231261
private currentNode: Node | null = null;
232262
private currentNetwork: CoinName = "btc";
@@ -453,6 +483,23 @@ class PsbtTxParser extends BaseComponent {
453483
font-family: inherit;
454484
}
455485
486+
.tree-value-buffer.expandable {
487+
cursor: pointer;
488+
text-decoration: underline dotted;
489+
text-underline-offset: 2px;
490+
}
491+
492+
.tree-value-buffer.expandable:hover {
493+
background: rgba(122, 204, 143, 0.1);
494+
border-radius: 2px;
495+
}
496+
497+
.tree-value-buffer.expanded {
498+
text-decoration: none;
499+
word-break: break-all;
500+
max-width: none;
501+
}
502+
456503
.tree-value-string {
457504
color: var(--yellow, #EBC55E);
458505
}
@@ -862,11 +909,7 @@ class PsbtTxParser extends BaseComponent {
862909
onclick: () => this.selectSample(index),
863910
},
864911
sample.name,
865-
h(
866-
"span",
867-
{ class: "sample-type" },
868-
sample.type === "tx" ? "TX" : "PSBT",
869-
),
912+
h("span", { class: "sample-type" }, sample.type === "tx" ? "TX" : "PSBT"),
870913
),
871914
),
872915
),
@@ -1031,6 +1074,7 @@ class PsbtTxParser extends BaseComponent {
10311074
// Clear previous state
10321075
errorEl.innerHTML = "";
10331076
this.expandedPaths = new Set(["root"]);
1077+
this.expandedValues = new Set();
10341078

10351079
let bytes: Uint8Array;
10361080
try {
@@ -1118,14 +1162,27 @@ class PsbtTxParser extends BaseComponent {
11181162
const resultsEl = this.$("#results");
11191163
if (!resultsEl || !this.currentNode) return;
11201164

1121-
const treeEl = renderTreeNode(this.currentNode, this.expandedPaths, (path) => {
1122-
if (this.expandedPaths.has(path)) {
1123-
this.expandedPaths.delete(path);
1124-
} else {
1125-
this.expandedPaths.add(path);
1126-
}
1127-
this.renderTree();
1128-
});
1165+
const treeEl = renderTreeNode(
1166+
this.currentNode,
1167+
this.expandedPaths,
1168+
(path) => {
1169+
if (this.expandedPaths.has(path)) {
1170+
this.expandedPaths.delete(path);
1171+
} else {
1172+
this.expandedPaths.add(path);
1173+
}
1174+
this.renderTree();
1175+
},
1176+
this.expandedValues,
1177+
(path) => {
1178+
if (this.expandedValues.has(path)) {
1179+
this.expandedValues.delete(path);
1180+
} else {
1181+
this.expandedValues.add(path);
1182+
}
1183+
this.renderTree();
1184+
},
1185+
);
11291186

11301187
resultsEl.replaceChildren(h("div", { class: "tree-container" }, treeEl));
11311188
}
@@ -1147,6 +1204,7 @@ class PsbtTxParser extends BaseComponent {
11471204

11481205
private collapseAll(): void {
11491206
this.expandedPaths = new Set(["root"]);
1207+
this.expandedValues = new Set();
11501208
this.renderTree();
11511209
}
11521210

0 commit comments

Comments
 (0)