@lukso/transaction-view-headless 0.4.3-dev.a07a66f → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +29 -31
  2. package/CONTEXT_VARIANT_GUIDE.md +4 -4
  3. package/dist/composables/index.cjs +1 -1
  4. package/dist/composables/index.cjs.map +1 -1
  5. package/dist/composables/index.d.cts +3 -3
  6. package/dist/composables/index.d.ts +3 -3
  7. package/dist/composables/index.js +1 -1
  8. package/dist/composables/index.js.map +1 -1
  9. package/dist/{index-CNUHqRdk.d.ts → index-CMLU1hKL.d.ts} +2 -2
  10. package/dist/{index-BqL7xdDl.d.cts → index-CSDjhiKV.d.cts} +2 -2
  11. package/dist/index.cjs +1 -60
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +4 -5
  14. package/dist/index.d.ts +4 -5
  15. package/dist/index.js +1 -55
  16. package/dist/index.js.map +1 -1
  17. package/dist/registry/index.d.cts +2 -2
  18. package/dist/registry/index.d.ts +2 -2
  19. package/dist/types/index.cjs +0 -68
  20. package/dist/types/index.cjs.map +1 -1
  21. package/dist/types/index.d.cts +169 -229
  22. package/dist/types/index.d.ts +169 -229
  23. package/dist/types/index.js +0 -55
  24. package/dist/types/index.js.map +1 -1
  25. package/dist/utils/index.d.cts +1 -1
  26. package/dist/utils/index.d.ts +1 -1
  27. package/dist/{view-loader--mEEI8YY.d.ts → view-loader-dD86QAlQ.d.cts} +1 -1
  28. package/dist/{view-loader-D7ApWfdN.d.cts → view-loader-ii8AxiQR.d.ts} +1 -1
  29. package/package.json +3 -3
  30. package/src/composables/use-transaction-playback.ts +5 -8
  31. package/src/types/index.ts +0 -14
  32. package/ARG_ACCESS_PATTERN.md +0 -265
  33. package/EVENT_SYSTEM_SUMMARY.md +0 -601
  34. package/HEADER_EVENTS_GUIDE.md +0 -291
  35. package/RECOMPOSITION_REFERENCE.md +0 -303
  36. package/TRANSACTION_MODIFICATION_GUIDE.md +0 -927
  37. package/dist/view-matching-C9G3z8lQ.d.cts +0 -203
  38. package/dist/view-matching-C9G3z8lQ.d.ts +0 -203
  39. package/src/types/view-events.ts +0 -322
@@ -1,927 +0,0 @@
1
- # Transaction Modification Guide
2
-
3
- This guide explains how transaction views can modify transaction data through an event-based system, including handling of wrapped transactions and batches.
4
-
5
- ## Problem
6
-
7
- Transaction views often need to allow users to modify transaction parameters before submission:
8
-
9
- - **Force Transfer Checkbox**: Enable force flag for LSP7 transfers to EOA addresses
10
- - **Slippage Tolerance**: Adjust acceptable slippage for DEX swaps
11
- - **Allowance Amount**: Modify token approval amounts
12
- - **Gas Settings**: Change gas limits or priorities
13
-
14
- These modifications must work correctly even within wrapped transactions (Execute calls) and batch operations.
15
-
16
- ## Architecture Overview
17
-
18
- ```
19
- ┌─────────────────────────────────────────┐
20
- │ View Component (LSP7 Transfer) │
21
- │ - User clicks "Force Transfer" checkbox│
22
- │ - View re-encodes local transaction │
23
- │ - Emits modification event │
24
- │ { data: "0xnewdata...", index: 2 } │
25
- └──────────────┬──────────────────────────┘
26
- │ bubbles up
27
-
28
- ┌─────────────────────────────────────────┐
29
- │ <transaction-playback> (index 2) │
30
- │ - Validates modification │
31
- │ - Re-decodes to verify │
32
- │ - Adds transaction path info │
33
- │ - Re-emits with context │
34
- └──────────────┬──────────────────────────┘
35
- │ bubbles up
36
-
37
- ┌─────────────────────────────────────────┐
38
- │ Top-Level Container │
39
- │ - Navigates to correct transaction │
40
- │ - Applies modification │
41
- │ - Re-wraps if needed (Execute/Batch) │
42
- │ - Re-decodes entire tree │
43
- │ - Updates UI │
44
- └─────────────────────────────────────────┘
45
- ```
46
-
47
- ## The Wrapping Challenge
48
-
49
- Consider this transaction structure:
50
-
51
- ```
52
- ExecuteRelayCall (top level - signed by controller)
53
- └─ signature: 0x...
54
- └─ data: "0xaabbcc..." (encoded execute)
55
- └─ Decoded: Execute
56
- ├─ target: 0xProfile
57
- ├─ data: "0x112233..." (encoded executeBatch)
58
- └─ Decoded: ExecuteBatch
59
- ├─ Child[0]: Execute
60
- │ ├─ target: 0xTokenContract
61
- │ ├─ data: "0x445566..." (encoded transfer)
62
- │ └─ Decoded: transfer(from, to, amount, force=false, data)
63
-
64
- └─ Child[1]: SetData
65
- └─ setData(key, value)
66
- ```
67
-
68
- When the transfer view modifies `force` to `true`:
69
-
70
- 1. ✅ View encodes **local** transaction data: `transfer(..., force=true, ...)`
71
- 2. ✅ Parent **recomposes** the original wrappers:
72
- - Insert new transfer data into Execute wrapper (preserving target, value)
73
- - Recompose ExecuteBatch with updated Execute (preserving other children)
74
- - Recompose ExecuteRelayCall with updated ExecuteBatch (preserving signature, nonce)
75
- 3. ✅ Parent re-decodes entire transaction to verify and update UI
76
-
77
- **Key Insights**:
78
- - Views only handle their own level
79
- - Parents **recompose** original wrappers (don't create new ones)
80
- - All original wrapper arguments are preserved (signatures, nonces, targets, etc.)
81
-
82
- ## Type Definitions
83
-
84
- ```typescript
85
- interface TransactionModification {
86
- /**
87
- * The modified data for THIS transaction level
88
- * NOT the top-level transaction data
89
- *
90
- * This is the encoded function call for the current transaction,
91
- * not wrapped in Execute or Batch calls.
92
- */
93
- data: `0x${string}`
94
-
95
- /**
96
- * The playback index this modification applies to
97
- *
98
- * - 0 = batch summary (modifies batch itself)
99
- * - 1 = first transaction (or single tx)
100
- * - 2 = second child transaction
101
- * - etc.
102
- *
103
- * This matches the `index` prop of <transaction-playback>
104
- */
105
- transactionIndex: number
106
-
107
- /**
108
- * Human-readable explanation of why this modification occurred
109
- *
110
- * @example "User enabled force transfer to EOA"
111
- * @example "Slippage tolerance increased to 2%"
112
- */
113
- reason: string
114
-
115
- /**
116
- * Whether this modification was triggered by user interaction
117
- *
118
- * - true: User clicked checkbox, changed input, etc.
119
- * - false: Automatic adjustment by view logic
120
- */
121
- userInitiated: boolean
122
-
123
- /**
124
- * Optional structured summary for quick display/logging
125
- */
126
- summary?: {
127
- /** Name of the field that changed */
128
- field: string
129
-
130
- /** Human-readable old value */
131
- oldValue: string
132
-
133
- /** Human-readable new value */
134
- newValue: string
135
- }
136
-
137
- /**
138
- * Optional validation information
139
- */
140
- validation?: {
141
- isValid: boolean
142
- error?: string
143
- warnings?: string[]
144
- }
145
- }
146
-
147
- /**
148
- * Extended modification info added by transaction-playback
149
- * This is what the top-level container receives
150
- */
151
- interface TransactionModificationWithContext extends TransactionModification {
152
- /**
153
- * Path from root transaction to this transaction
154
- *
155
- * @example [1] - Simple transaction at index 1
156
- * @example [0, 2] - Batch at index 0, child at index 2
157
- * @example [0, 1, 0] - Nested batch: batch[0].children[1].children[0]
158
- */
159
- transactionPath: number[]
160
-
161
- /**
162
- * The full local transaction object with modified data
163
- */
164
- localTransaction: DecoderResult
165
-
166
- /**
167
- * Re-decoded result for verification
168
- */
169
- decodedResult: DecoderResult
170
- }
171
- ```
172
-
173
- ## View Implementation
174
-
175
- ### Basic Example: Force Transfer Checkbox
176
-
177
- ```typescript
178
- import { dispatchTransactionModification, getArgByName } from '@lukso/transaction-view-headless'
179
- import { encodeFunctionData } from 'viem'
180
-
181
- @customElement('lsp7-transfer-view')
182
- export class LSP7TransferView extends CoreLitElement {
183
- @property({ type: Object }) transaction!: DecoderResult
184
- @property({ type: String }) context?: ViewContext
185
- @property({ type: Number }) index!: number // From transaction-playback
186
-
187
- @state()
188
- private forceTransfer = false
189
-
190
- connectedCallback() {
191
- super.connectedCallback()
192
- // Initialize from transaction
193
- this.forceTransfer = (getArgByName(this.transaction.args, 'force') as boolean) ?? false
194
- }
195
-
196
- private handleForceChange(checked: boolean) {
197
- this.forceTransfer = checked
198
-
199
- // Re-encode with new force value
200
- const newData = encodeFunctionData({
201
- abi: LSP7_ABI,
202
- functionName: 'transfer',
203
- args: [
204
- getArgByName(this.transaction.args, 'from'),
205
- getArgByName(this.transaction.args, 'to'),
206
- getArgByName(this.transaction.args, 'amount'),
207
- checked, // <-- modified
208
- getArgByName(this.transaction.args, 'data')
209
- ]
210
- })
211
-
212
- // Emit modification event
213
- dispatchTransactionModification(this, {
214
- data: newData,
215
- transactionIndex: this.index,
216
- reason: checked
217
- ? 'User enabled force transfer to EOA'
218
- : 'User disabled force transfer',
219
- userInitiated: true,
220
- summary: {
221
- field: 'force',
222
- oldValue: String(!checked),
223
- newValue: String(checked)
224
- }
225
- })
226
- }
227
-
228
- render() {
229
- // Context-aware rendering
230
- if (this.context === 'approval') {
231
- // Show interactive checkbox
232
- return html`
233
- <div class="transfer-content">
234
- <!-- Transfer details -->
235
-
236
- <label class="flex items-center gap-2 mt-4">
237
- <input
238
- type="checkbox"
239
- .checked=${this.forceTransfer}
240
- @change=${(e) => this.handleForceChange(e.target.checked)}
241
- />
242
- <span>Force transfer (required for EOA recipients)</span>
243
- </label>
244
- </div>
245
- `
246
- } else if (this.context === 'feed') {
247
- // Show read-only status
248
- return html`
249
- <div class="transfer-summary">
250
- <!-- Compact transfer info -->
251
- ${this.forceTransfer ? html`
252
- <div class="text-12 text-orange-600">⚠️ Forced transfer</div>
253
- ` : ''}
254
- </div>
255
- `
256
- } else {
257
- // Detail view
258
- return html`
259
- <div class="transfer-details">
260
- <!-- Full transfer info -->
261
- <div class="mt-4">
262
- <strong>Force:</strong> ${this.forceTransfer ? 'Yes' : 'No'}
263
- ${this.forceTransfer ? html`
264
- <div class="text-12 text-neutral-60">
265
- This transfer will succeed even if recipient is an EOA
266
- </div>
267
- ` : ''}
268
- </div>
269
- </div>
270
- `
271
- }
272
- }
273
- }
274
- ```
275
-
276
- ### Advanced Example: Slippage Tolerance
277
-
278
- ```typescript
279
- @customElement('swap-view')
280
- export class SwapView extends CoreLitElement {
281
- @property({ type: Object }) transaction!: DecoderResult
282
- @property({ type: Number }) index!: number
283
-
284
- @state()
285
- private slippageBps = 50 // 0.5% default
286
-
287
- private handleSlippageChange(bps: number) {
288
- this.slippageBps = bps
289
-
290
- // Recalculate minimum output with new slippage
291
- const minOutput = this.calculateMinOutput(
292
- getArgByName(this.transaction.args, 'amountOut') as bigint,
293
- bps
294
- )
295
-
296
- // Re-encode swap with new minimum
297
- const newData = encodeFunctionData({
298
- abi: SWAP_ABI,
299
- functionName: 'swap',
300
- args: [
301
- getArgByName(this.transaction.args, 'tokenIn'),
302
- getArgByName(this.transaction.args, 'tokenOut'),
303
- getArgByName(this.transaction.args, 'amountIn'),
304
- minOutput, // <-- modified
305
- getArgByName(this.transaction.args, 'recipient')
306
- ]
307
- })
308
-
309
- dispatchTransactionModification(this, {
310
- data: newData,
311
- transactionIndex: this.index,
312
- reason: `User adjusted slippage tolerance to ${bps / 100}%`,
313
- userInitiated: true,
314
- summary: {
315
- field: 'slippage',
316
- oldValue: `${this.slippageBps / 100}%`,
317
- newValue: `${bps / 100}%`
318
- },
319
- validation: {
320
- isValid: bps <= 1000, // Max 10%
321
- warnings: bps > 500 ? ['High slippage may result in unfavorable trade'] : []
322
- }
323
- })
324
- }
325
-
326
- render() {
327
- return html`
328
- <div>
329
- <label>
330
- Slippage Tolerance: ${this.slippageBps / 100}%
331
- <input
332
- type="range"
333
- min="10"
334
- max="1000"
335
- step="10"
336
- .value=${String(this.slippageBps)}
337
- @input=${(e) => this.handleSlippageChange(Number(e.target.value))}
338
- />
339
- </label>
340
- </div>
341
- `
342
- }
343
- }
344
- ```
345
-
346
- ## Parent Container Implementation
347
-
348
- ### In transaction-playback Component
349
-
350
- ```typescript
351
- // Already implemented in transaction-playback.ts
352
- connectedCallback(): void {
353
- super.connectedCallback()
354
- this.addEventListener(
355
- TransactionViewEvents.TRANSACTION_MODIFICATION,
356
- this.handleTransactionModification as EventListener
357
- )
358
- }
359
-
360
- private handleTransactionModification = (event: Event): void => {
361
- if (!isTransactionModificationEvent(event)) return
362
-
363
- const mod = event.detail
364
-
365
- // Verify this modification is for our transaction
366
- if (mod.transactionIndex !== this.index) {
367
- console.warn(`Modification index mismatch: expected ${this.index}, got ${mod.transactionIndex}`)
368
- return
369
- }
370
-
371
- // Create modified local transaction
372
- const modifiedLocal: DecoderResult = {
373
- ...this.currentResult.transaction,
374
- data: mod.data
375
- }
376
-
377
- // Re-decode to verify
378
- const reDecoded = await decoder.decode(modifiedLocal)
379
-
380
- // Add context and re-emit for top-level container
381
- this.dispatchEvent(
382
- new CustomEvent(TransactionViewEvents.TRANSACTION_MODIFICATION, {
383
- bubbles: true,
384
- composed: true,
385
- detail: {
386
- ...mod,
387
- transactionPath: this.getTransactionPath(),
388
- localTransaction: modifiedLocal,
389
- decodedResult: reDecoded
390
- } as TransactionModificationWithContext
391
- })
392
- )
393
- }
394
-
395
- private getTransactionPath(): number[] {
396
- // Get path from root to current transaction
397
- return this.playbackState.getTransactionPath(this.index)
398
- }
399
- ```
400
-
401
- ### In Top-Level Container (Wallet/App)
402
-
403
- ```typescript
404
- class WalletApprovalScreen extends LitElement {
405
- @state()
406
- private originalTransaction: DecoderResult
407
-
408
- @state()
409
- private modifiedTransaction?: DecoderResult
410
-
411
- @state()
412
- private modifications: TransactionModificationWithContext[] = []
413
-
414
- render() {
415
- return html`
416
- <div class="approval-screen">
417
- <h2>Approve Transaction</h2>
418
-
419
- <!-- Show modification summary if any -->
420
- ${this.modifications.length > 0 ? html`
421
- <div class="modifications-summary">
422
- <h3>User Modifications:</h3>
423
- ${this.modifications.map(mod => html`
424
- <div class="modification-item">
425
- <span class="icon">✏️</span>
426
- <span>${mod.reason}</span>
427
- ${mod.summary ? html`
428
- <span class="change">
429
- ${mod.summary.field}:
430
- ${mod.summary.oldValue} → ${mod.summary.newValue}
431
- </span>
432
- ` : ''}
433
- </div>
434
- `)}
435
- </div>
436
- ` : ''}
437
-
438
- <!-- Transaction view (will re-render with modifications) -->
439
- <transaction-playback
440
- .transaction=${this.modifiedTransaction ?? this.originalTransaction}
441
- index="1"
442
- context="approval"
443
- @transaction-modification=${this.handleModification}
444
- ></transaction-playback>
445
-
446
- <div class="actions">
447
- <button @click=${this.reject}>Reject</button>
448
- <button @click=${this.approve}>Approve</button>
449
- </div>
450
- </div>
451
- `
452
- }
453
-
454
- private async handleModification(event: CustomEvent) {
455
- const mod: TransactionModificationWithContext = event.detail
456
-
457
- // Store modification
458
- this.modifications.push(mod)
459
-
460
- // Recompose the original transaction with the modification
461
- const recomposed = await this.recomposeTransaction(
462
- this.originalTransaction,
463
- mod
464
- )
465
-
466
- // Update state - this triggers re-render
467
- this.modifiedTransaction = recomposed
468
- }
469
-
470
- /**
471
- * Recompose the original transaction with a modification
472
- *
473
- * Takes the original transaction structure and inserts the modified data
474
- * at the correct location, preserving all original wrappers (Execute,
475
- * ExecuteBatch, ExecuteRelayCall, etc.)
476
- *
477
- * Example:
478
- * Original: executeRelayCall(execute(executeBatch([transfer, setData])))
479
- * Modified: executeRelayCall(execute(executeBatch([transfer*, setData])))
480
- * (* = transfer with force=true)
481
- */
482
- private async recomposeTransaction(
483
- originalTx: DecoderResult,
484
- mod: TransactionModificationWithContext
485
- ): Promise<DecoderResult> {
486
- const { transactionPath, data } = mod
487
-
488
- // Clone the original transaction to preserve structure
489
- const recomposed = structuredClone(originalTx)
490
-
491
- if (transactionPath.length === 1) {
492
- // Simple transaction - just replace data
493
- recomposed.data = data
494
- } else {
495
- // Complex transaction - navigate and recompose wrappers
496
- await this.recomposeAtPath(recomposed, transactionPath, data)
497
- }
498
-
499
- // Re-decode entire transaction tree to verify and get fresh DecoderResult
500
- const reDecoded = await decoder.decode(recomposed)
501
-
502
- return reDecoded
503
- }
504
-
505
- /**
506
- * Navigate to a transaction path and recompose wrappers bottom-up
507
- *
508
- * Example path [0, 1, 2] means:
509
- * - Navigate to batch[0].children[1].children[2]
510
- * - Replace that transaction's data
511
- * - Recompose batch[0].children[1] wrapper with updated child
512
- * - Recompose batch[0] wrapper with updated child
513
- * - Recompose root with updated batch
514
- */
515
- private async recomposeAtPath(
516
- root: any,
517
- path: number[],
518
- newData: string
519
- ): Promise<void> {
520
- // Navigate to the target transaction
521
- let current = root
522
- const parents: any[] = [root]
523
-
524
- for (let i = 0; i < path.length - 1; i++) {
525
- const index = path[i]
526
- if (current.children?.[index]) {
527
- current = current.children[index]
528
- parents.push(current)
529
- } else {
530
- throw new Error(`Invalid transaction path: ${path}`)
531
- }
532
- }
533
-
534
- // Apply modification to the target
535
- const targetIndex = path[path.length - 1]
536
- if (!current.children?.[targetIndex]) {
537
- throw new Error(`Invalid transaction path: ${path}`)
538
- }
539
-
540
- current.children[targetIndex].data = newData
541
-
542
- // Recompose wrappers bottom-up
543
- // Start from the deepest parent and work up to root
544
- for (let i = parents.length - 1; i >= 0; i--) {
545
- const parent = parents[i]
546
- if (parent.children && parent.functionName) {
547
- parent.data = await this.recomposeWrapper(parent)
548
- }
549
- }
550
- }
551
-
552
- /**
553
- * Recompose a wrapper transaction (Execute, ExecuteBatch, ExecuteRelayCall, etc.)
554
- *
555
- * Takes the original wrapper and re-encodes it with updated child data,
556
- * preserving all original arguments (operationType, target, value, signature, etc.)
557
- */
558
- private async recomposeWrapper(wrapper: any): Promise<string> {
559
- const { functionName, args, children } = wrapper
560
-
561
- // Recompose based on wrapper type
562
- switch (functionName) {
563
- case 'execute':
564
- return encodeFunctionData({
565
- abi: UP_ABI,
566
- functionName: 'execute',
567
- args: [
568
- getArgByName(args, 'operationType') as number,
569
- getArgByName(args, 'target') as string,
570
- getArgByName(args, 'value') as bigint,
571
- children[0].data // <-- updated child data
572
- ]
573
- })
574
-
575
- case 'executeBatch':
576
- return encodeFunctionData({
577
- abi: UP_ABI,
578
- functionName: 'executeBatch',
579
- args: [
580
- getArgByName(args, 'operationTypes') as number[],
581
- getArgByName(args, 'targets') as string[],
582
- getArgByName(args, 'values') as bigint[],
583
- children.map((child: any) => child.data) // <-- updated children data
584
- ]
585
- })
586
-
587
- case 'executeRelayCall':
588
- return encodeFunctionData({
589
- abi: KEY_MANAGER_ABI,
590
- functionName: 'executeRelayCall',
591
- args: [
592
- getArgByName(args, 'signature') as string,
593
- getArgByName(args, 'nonce') as bigint,
594
- getArgByName(args, 'validityTimestamps') as bigint,
595
- children[0].data // <-- updated execute/executeBatch data
596
- ]
597
- })
598
-
599
- default:
600
- throw new Error(`Unknown wrapper function: ${functionName}`)
601
- }
602
- }
603
-
604
- private async approve() {
605
- // Submit the recomposed transaction (or original if no modifications)
606
- const finalTx = this.modifiedTransaction ?? this.originalTransaction
607
-
608
- // Log modifications for audit trail
609
- if (this.modifications.length > 0) {
610
- console.log('Transaction modifications:', this.modifications)
611
- }
612
-
613
- // Send transaction
614
- await this.wallet.sendTransaction(finalTx)
615
- }
616
- }
617
- ```
618
-
619
- ## Helper Functions
620
-
621
- ### dispatchTransactionModification
622
-
623
- ```typescript
624
- /**
625
- * Helper function to dispatch transaction modification event
626
- * Use this in view components to emit modifications
627
- */
628
- export function dispatchTransactionModification(
629
- element: HTMLElement,
630
- modification: TransactionModification
631
- ): void {
632
- element.dispatchEvent(
633
- new CustomEvent(TransactionViewEvents.TRANSACTION_MODIFICATION, {
634
- bubbles: true,
635
- composed: true,
636
- detail: modification,
637
- })
638
- )
639
- }
640
- ```
641
-
642
- ### isTransactionModificationEvent
643
-
644
- ```typescript
645
- /**
646
- * Type guard to check if an event is a TransactionModificationEvent
647
- */
648
- export function isTransactionModificationEvent(
649
- event: Event
650
- ): event is CustomEvent<TransactionModification> {
651
- return (
652
- event instanceof CustomEvent &&
653
- event.type === TransactionViewEvents.TRANSACTION_MODIFICATION
654
- )
655
- }
656
- ```
657
-
658
- ## Security Considerations
659
-
660
- ### 1. Always Re-decode
661
-
662
- The parent container **must** re-decode modified data to verify it's valid:
663
-
664
- ```typescript
665
- // ✅ Good - Re-decode and verify
666
- const reDecoded = await decoder.decode({ ...tx, data: mod.data })
667
- if (!reDecoded) {
668
- throw new Error('Invalid modification - failed to decode')
669
- }
670
- ```
671
-
672
- ### 2. Show Modifications to User
673
-
674
- Display what changed before submission:
675
-
676
- ```typescript
677
- // ✅ Good - User sees what changed
678
- <div class="modifications">
679
- ${modifications.map(mod => html`
680
- <div>${mod.reason}</div>
681
- ${mod.summary ? html`
682
- <div>${mod.summary.field}: ${mod.summary.oldValue} → ${mod.summary.newValue}</div>
683
- ` : ''}
684
- `)}
685
- </div>
686
- ```
687
-
688
- ### 3. Validate Modifications
689
-
690
- Check that modifications make sense:
691
-
692
- ```typescript
693
- // ✅ Good - Validate before applying
694
- if (mod.validation && !mod.validation.isValid) {
695
- showError(mod.validation.error)
696
- return
697
- }
698
-
699
- if (mod.validation?.warnings) {
700
- showWarnings(mod.validation.warnings)
701
- }
702
- ```
703
-
704
- ### 4. Audit Trail
705
-
706
- Log all modifications:
707
-
708
- ```typescript
709
- // ✅ Good - Keep audit trail
710
- console.log('Transaction modified:', {
711
- transactionHash: tx.hash,
712
- modifications: modifications.map(m => ({
713
- field: m.summary?.field,
714
- oldValue: m.summary?.oldValue,
715
- newValue: m.summary?.newValue,
716
- reason: m.reason,
717
- userInitiated: m.userInitiated,
718
- timestamp: Date.now()
719
- }))
720
- })
721
- ```
722
-
723
- ## Best Practices
724
-
725
- ### 1. Emit Modifications Immediately
726
-
727
- Don't batch modifications - emit each change as it happens:
728
-
729
- ```typescript
730
- // ✅ Good
731
- private handleChange(value: string) {
732
- this.emitModification(value)
733
- }
734
-
735
- // ❌ Bad - Don't batch
736
- private handleChange(value: string) {
737
- this.pendingChanges.push(value)
738
- // Emit later somehow?
739
- }
740
- ```
741
-
742
- ### 2. Include Transaction Index
743
-
744
- Always include the correct transaction index:
745
-
746
- ```typescript
747
- // ✅ Good
748
- dispatchTransactionModification(this, {
749
- data: newData,
750
- transactionIndex: this.index, // From transaction-playback
751
- // ...
752
- })
753
-
754
- // ❌ Bad - Hardcoded or missing
755
- dispatchTransactionModification(this, {
756
- data: newData,
757
- transactionIndex: 1, // Wrong!
758
- // ...
759
- })
760
- ```
761
-
762
- ### 3. Provide Good Reasons
763
-
764
- Write clear, human-readable reasons:
765
-
766
- ```typescript
767
- // ✅ Good
768
- reason: 'User enabled force transfer to allow sending to EOA addresses'
769
-
770
- // ❌ Bad
771
- reason: 'force=true'
772
- ```
773
-
774
- ### 4. Context-Aware UI
775
-
776
- Show appropriate UI based on context:
777
-
778
- ```typescript
779
- render() {
780
- if (this.context === 'approval') {
781
- // Interactive controls
782
- return html`<input type="checkbox" @change=${this.handleChange} />`
783
- } else {
784
- // Read-only display
785
- return html`<div>Force: ${this.force}</div>`
786
- }
787
- }
788
- ```
789
-
790
- ## Transaction Path Examples
791
-
792
- ### Simple Transaction
793
- ```
794
- Transaction (index: 1)
795
- Path: [1]
796
- ```
797
-
798
- ### Execute Wrapper
799
- ```
800
- Execute (index: 0)
801
- └─ Transfer (index: 1)
802
- Path: [0, 1]
803
- ```
804
-
805
- ### Batch
806
- ```
807
- ExecuteBatch (index: 0)
808
- ├─ Transfer (index: 1)
809
- │ Path: [0, 1]
810
- └─ SetData (index: 2)
811
- Path: [0, 2]
812
- ```
813
-
814
- ### Nested Batch
815
- ```
816
- ExecuteBatch (index: 0)
817
- ├─ ExecuteBatch (index: 1)
818
- │ ├─ Transfer (index: 1, 0)
819
- │ │ Path: [0, 1, 0]
820
- │ └─ Transfer (index: 1, 1)
821
- │ Path: [0, 1, 1]
822
- └─ SetData (index: 2)
823
- Path: [0, 2]
824
- ```
825
-
826
- ## Event Constants
827
-
828
- Use constants instead of hardcoding strings:
829
-
830
- ```typescript
831
- import { TransactionViewEvents } from '@lukso/transaction-view-headless'
832
-
833
- // ✅ Good
834
- this.addEventListener(TransactionViewEvents.TRANSACTION_MODIFICATION, handler)
835
-
836
- // ❌ Bad
837
- this.addEventListener('transaction-modification', handler)
838
- ```
839
-
840
- ## Complete Flow Example
841
-
842
- ```
843
- 1. User clicks "Force Transfer" checkbox in LSP7TransferView
844
- └─> View re-encodes transfer(from, to, amount, true, data)
845
- └─> Emits { data: "0x...", transactionIndex: 1, reason: "..." }
846
-
847
- 2. <transaction-playback index="1"> receives event
848
- └─> Validates: mod.transactionIndex === this.index ✓
849
- └─> Re-decodes: decoder.decode({ ...tx, data: mod.data }) ✓
850
- └─> Adds path: transactionPath: [0, 1] (Execute wrapper)
851
- └─> Re-emits with full context
852
-
853
- 3. Top-level container receives event
854
- └─> Clones original transaction structure
855
- └─> Navigates to path [0, 1]
856
- └─> Replaces children[1].data with mod.data
857
- └─> Recomposes wrappers bottom-up:
858
- - Recompose execute(target, value, newTransferData)
859
- - Recompose executeBatch([newExecuteData, setDataData])
860
- - Recompose executeRelayCall(signature, nonce, newExecuteBatchData)
861
- └─> Re-decodes entire tree to verify
862
- └─> Updates state
863
- └─> UI re-renders with modified transaction
864
-
865
- 4. User sees updated transaction in approval screen
866
- └─> "Force: false → true" shown in modifications list
867
- └─> Transaction view shows updated state
868
- └─> User clicks "Approve"
869
- └─> Recomposed transaction is submitted (with original signature intact!)
870
- ```
871
-
872
- ## Why Recomposition Matters
873
-
874
- ### ❌ Wrong: Create New Wrappers
875
-
876
- ```typescript
877
- // DON'T DO THIS
878
- const newExecute = encodeFunctionData({
879
- abi: UP_ABI,
880
- functionName: 'execute',
881
- args: [0, target, 0, modifiedData] // <-- New wrapper, loses context
882
- })
883
-
884
- // This breaks:
885
- // - Original operationType is lost
886
- // - Original value is lost
887
- // - Signature becomes invalid (data changed)
888
- // - Nonce might be wrong
889
- ```
890
-
891
- ### ✅ Correct: Recompose Original Wrappers
892
-
893
- ```typescript
894
- // DO THIS
895
- const recomposed = structuredClone(originalTransaction)
896
-
897
- // Navigate and update
898
- recomposed.children[0].children[1].data = modifiedData
899
-
900
- // Recompose preserving ALL original args
901
- recomposed.children[0].data = encodeFunctionData({
902
- abi: UP_ABI,
903
- functionName: 'execute',
904
- args: [
905
- getArgByName(originalTransaction.children[0].args, 'operationType') as number, // ✓ Preserved
906
- getArgByName(originalTransaction.children[0].args, 'target') as string, // ✓ Preserved
907
- getArgByName(originalTransaction.children[0].args, 'value') as bigint, // ✓ Preserved
908
- modifiedData // ✓ Updated
909
- ]
910
- })
911
-
912
- // This preserves:
913
- // - Original operationType
914
- // - Original value
915
- // - Signature validity (if recomposed correctly)
916
- // - All other wrapper arguments
917
- ```
918
-
919
- ### Key Principle
920
-
921
- **Always start with the original transaction and recompose it, never create new wrappers from scratch.**
922
-
923
- This ensures:
924
- 1. ✅ All wrapper arguments are preserved
925
- 2. ✅ Signatures remain valid (if signing over final data)
926
- 3. ✅ Context is maintained (nonces, timestamps, etc.)
927
- 4. ✅ Only the intended modification is applied