@lukso/transaction-view-headless 0.4.3 → 0.4.4-dev.c5d67d4

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 (41) hide show
  1. package/.turbo/turbo-build.log +32 -30
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/ARG_ACCESS_PATTERN.md +265 -0
  4. package/CHANGELOG.md +14 -0
  5. package/CONTEXT_VARIANT_GUIDE.md +4 -4
  6. package/EVENT_SYSTEM_SUMMARY.md +601 -0
  7. package/HEADER_EVENTS_GUIDE.md +291 -0
  8. package/RECOMPOSITION_REFERENCE.md +303 -0
  9. package/TRANSACTION_MODIFICATION_GUIDE.md +927 -0
  10. package/dist/composables/index.cjs +1 -1
  11. package/dist/composables/index.cjs.map +1 -1
  12. package/dist/composables/index.d.cts +3 -3
  13. package/dist/composables/index.d.ts +3 -3
  14. package/dist/composables/index.js +1 -1
  15. package/dist/composables/index.js.map +1 -1
  16. package/dist/{index-CSDjhiKV.d.cts → index-BqL7xdDl.d.cts} +2 -2
  17. package/dist/{index-CMLU1hKL.d.ts → index-CNUHqRdk.d.ts} +2 -2
  18. package/dist/index.cjs +60 -1
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +5 -4
  21. package/dist/index.d.ts +5 -4
  22. package/dist/index.js +55 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/registry/index.d.cts +2 -2
  25. package/dist/registry/index.d.ts +2 -2
  26. package/dist/types/index.cjs +68 -0
  27. package/dist/types/index.cjs.map +1 -1
  28. package/dist/types/index.d.cts +229 -169
  29. package/dist/types/index.d.ts +229 -169
  30. package/dist/types/index.js +55 -0
  31. package/dist/types/index.js.map +1 -1
  32. package/dist/utils/index.d.cts +1 -1
  33. package/dist/utils/index.d.ts +1 -1
  34. package/dist/{view-loader-dD86QAlQ.d.cts → view-loader--mEEI8YY.d.ts} +1 -1
  35. package/dist/{view-loader-ii8AxiQR.d.ts → view-loader-D7ApWfdN.d.cts} +1 -1
  36. package/dist/view-matching-C9G3z8lQ.d.cts +203 -0
  37. package/dist/view-matching-C9G3z8lQ.d.ts +203 -0
  38. package/package.json +3 -3
  39. package/src/composables/use-transaction-playback.ts +8 -5
  40. package/src/types/index.ts +14 -0
  41. package/src/types/view-events.ts +322 -0
@@ -0,0 +1,291 @@
1
+ # Header Events Guide
2
+
3
+ This guide explains how transaction views can communicate header information to their parent containers using the event system.
4
+
5
+ ## Problem
6
+
7
+ By default, transaction containers (like `<transaction-playback>`) display the transaction's top-level `from` and `to` addresses in the header. However, for nested operations, this isn't always appropriate.
8
+
9
+ **Example:**
10
+ ```
11
+ Execute Transaction
12
+ from: 0xUserProfile (Universal Profile)
13
+ to: 0xTokenContract (LSP7 Token)
14
+
15
+ Contains: LSP7 Transfer
16
+ from: 0xUserProfile
17
+ to: 0xRecipient
18
+ ```
19
+
20
+ When displaying the LSP7 Transfer view, we want the header to show:
21
+ - ✅ Transfer from `0xUserProfile` to `0xRecipient` (the actual transfer)
22
+ - ❌ Execute from `0xUserProfile` to `0xTokenContract` (the wrapper call)
23
+
24
+ ## Solution: Header Info Events
25
+
26
+ Views can emit a `header-info` event to tell their parent container what addresses should be displayed.
27
+
28
+ ### Basic Usage (in View Components)
29
+
30
+ ```typescript
31
+ import { dispatchHeaderInfo, getArgByName } from '@lukso/transaction-view-headless'
32
+ import { LitElement, html } from 'lit'
33
+ import { customElement, property } from 'lit/decorators.js'
34
+
35
+ @customElement('lsp7-transfer-view')
36
+ export class LSP7TransferView extends LitElement {
37
+ @property({ type: Object }) transaction!: DecoderResult
38
+ @property({ type: String }) context?: ViewContext
39
+
40
+ connectedCallback() {
41
+ super.connectedCallback()
42
+
43
+ // Extract the actual transfer addresses from transaction args
44
+ const fromAddress = getArgByName(this.transaction.args, 'from') as string | undefined
45
+ const toAddress = getArgByName(this.transaction.args, 'to') as string | undefined
46
+
47
+ // Emit header info
48
+ dispatchHeaderInfo(this, {
49
+ from: fromAddress,
50
+ to: toAddress,
51
+ title: 'LSP7 Token Transfer',
52
+ subtitle: `Send ${this.formatAmount()} tokens`,
53
+ icon: 'transfer'
54
+ })
55
+ }
56
+
57
+ render() {
58
+ // Render the transfer details
59
+ return html`...`
60
+ }
61
+ }
62
+ ```
63
+
64
+ ### Context-Aware Headers
65
+
66
+ Views can emit different header information based on the display context:
67
+
68
+ ```typescript
69
+ connectedCallback() {
70
+ super.connectedCallback()
71
+
72
+ if (this.context === 'approval') {
73
+ // Approval context: emphasize security
74
+ dispatchHeaderInfo(this, {
75
+ from: getArgByName(this.transaction.args, 'from') as string | undefined,
76
+ to: getArgByName(this.transaction.args, 'to') as string | undefined,
77
+ title: '⚠️ Approve Transfer',
78
+ subtitle: 'Review the transfer details carefully',
79
+ })
80
+ } else if (this.context === 'feed') {
81
+ // Feed context: compact summary
82
+ dispatchHeaderInfo(this, {
83
+ from: getArgByName(this.transaction.args, 'from') as string | undefined,
84
+ to: getArgByName(this.transaction.args, 'to') as string | undefined,
85
+ title: 'Transfer',
86
+ icon: 'transfer'
87
+ })
88
+ } else {
89
+ // Detail context: full information
90
+ dispatchHeaderInfo(this, {
91
+ from: getArgByName(this.transaction.args, 'from') as string | undefined,
92
+ to: getArgByName(this.transaction.args, 'to') as string | undefined,
93
+ title: 'LSP7 Token Transfer',
94
+ subtitle: this.getDetailedSubtitle(),
95
+ metadata: {
96
+ tokenId: this.getTokenId(),
97
+ amount: this.getAmount()
98
+ }
99
+ })
100
+ }
101
+ }
102
+ ```
103
+
104
+ ### Listening for Header Events (in Parent Containers)
105
+
106
+ Parent containers like `<transaction-playback>` automatically listen for and re-emit these events. You can also listen at a higher level:
107
+
108
+ ```html
109
+ <transaction-playback
110
+ .transaction="${tx}"
111
+ @header-info="${this.handleHeaderInfo}"
112
+ ></transaction-playback>
113
+ ```
114
+
115
+ ```typescript
116
+ private handleHeaderInfo(event: CustomEvent) {
117
+ const { from, to, title, subtitle } = event.detail
118
+
119
+ // Update your UI's header
120
+ this.headerFrom = from
121
+ this.headerTo = to
122
+ this.headerTitle = title
123
+ }
124
+ ```
125
+
126
+ ## TypeScript Types
127
+
128
+ ```typescript
129
+ import type { TransactionHeaderInfo, HeaderInfoEvent } from '@lukso/transaction-view-headless'
130
+
131
+ interface TransactionHeaderInfo {
132
+ from?: string // Override "from" address
133
+ to?: string // Override "to" address
134
+ title?: string // Transaction title
135
+ subtitle?: string // Additional description
136
+ icon?: string // Icon identifier
137
+ metadata?: Record<string, any> // Custom data
138
+ }
139
+ ```
140
+
141
+ ## Event Flow
142
+
143
+ ```
144
+ ┌─────────────────────────────────────┐
145
+ │ LSP7TransferView │
146
+ │ - Emits header-info event │
147
+ │ { from: 0xA, to: 0xB } │
148
+ └──────────────┬──────────────────────┘
149
+ │ bubbles: true
150
+ │ composed: true
151
+
152
+ ┌─────────────────────────────────────┐
153
+ │ <transaction-playback> │
154
+ │ - Receives event │
155
+ │ - Stores headerInfo state │
156
+ │ - Re-emits event (for parent) │
157
+ └──────────────┬──────────────────────┘
158
+ │ bubbles: true
159
+ │ composed: true
160
+
161
+ ┌─────────────────────────────────────┐
162
+ │ Parent Container (e.g., App) │
163
+ │ - Updates header UI │
164
+ │ - Displays from: 0xA, to: 0xB │
165
+ └─────────────────────────────────────┘
166
+ ```
167
+
168
+ ## Debug Mode
169
+
170
+ The `<transaction-playback>` component shows received header information when `debug` mode is enabled:
171
+
172
+ ```html
173
+ <transaction-playback
174
+ .transaction="${tx}"
175
+ debug
176
+ ></transaction-playback>
177
+ ```
178
+
179
+ This will display:
180
+ ```
181
+ 🐛 Playback Debug Info
182
+ ...
183
+ 📋 Header Info
184
+ From: 0x1234...
185
+ To: 0x5678...
186
+ Title: LSP7 Token Transfer
187
+ ```
188
+
189
+ ## Best Practices
190
+
191
+ ### 1. Emit Early
192
+ Emit header info in `connectedCallback()` so it's available before the view renders:
193
+
194
+ ```typescript
195
+ connectedCallback() {
196
+ super.connectedCallback()
197
+ this.emitHeaderInfo() // ✅ Early
198
+ }
199
+
200
+ firstUpdated() {
201
+ this.emitHeaderInfo() // ⚠️ Late - header might flash
202
+ }
203
+ ```
204
+
205
+ ### 2. Update on Changes
206
+ If header info depends on reactive properties, update it when they change:
207
+
208
+ ```typescript
209
+ updated(changedProperties: PropertyValues) {
210
+ if (changedProperties.has('transaction') ||
211
+ changedProperties.has('context')) {
212
+ this.emitHeaderInfo()
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### 3. Provide Fallbacks
218
+ Always provide reasonable fallbacks if data is missing:
219
+
220
+ ```typescript
221
+ dispatchHeaderInfo(this, {
222
+ from: (getArgByName(this.transaction.args, 'from') as string | undefined) ?? this.transaction.from,
223
+ to: (getArgByName(this.transaction.args, 'to') as string | undefined) ?? this.transaction.to,
224
+ title: this.getTitle() ?? 'Transaction',
225
+ })
226
+ ```
227
+
228
+ ### 4. Context-Aware Rendering
229
+ Use the `context` prop to adapt header information:
230
+
231
+ ```typescript
232
+ @property({ type: String }) context?: ViewContext
233
+
234
+ private getHeaderTitle(): string {
235
+ switch (this.context) {
236
+ case 'approval': return '⚠️ Approve Transfer'
237
+ case 'feed': return 'Transfer'
238
+ case 'detail': return 'LSP7 Token Transfer'
239
+ default: return 'Transfer'
240
+ }
241
+ }
242
+ ```
243
+
244
+ ## Future: Self-Contained Headers (`inject-header` flag)
245
+
246
+ In the future, views may optionally render their own headers using an `inject-header` flag:
247
+
248
+ ```typescript
249
+ @property({ type: Boolean, attribute: 'inject-header' })
250
+ injectHeader = false
251
+
252
+ render() {
253
+ return html`
254
+ ${this.injectHeader ? this.renderHeader() : ''}
255
+ ${this.renderContent()}
256
+ `
257
+ }
258
+ ```
259
+
260
+ This is not yet implemented but is planned for views that need complete control over header rendering.
261
+
262
+ ## Event Constants
263
+
264
+ Use the constants from `TransactionViewEvents` instead of hardcoding strings:
265
+
266
+ ```typescript
267
+ import { TransactionViewEvents } from '@lukso/transaction-view-headless'
268
+
269
+ // ✅ Good
270
+ this.addEventListener(TransactionViewEvents.HEADER_INFO, handler)
271
+
272
+ // ❌ Bad
273
+ this.addEventListener('header-info', handler)
274
+ ```
275
+
276
+ ## Helper Functions
277
+
278
+ ### `dispatchHeaderInfo(element, headerInfo)`
279
+ Dispatches a header-info event from a view component.
280
+
281
+ ### `isHeaderInfoEvent(event)`
282
+ Type guard to check if an event is a HeaderInfoEvent.
283
+
284
+ ```typescript
285
+ private handleEvent(event: Event) {
286
+ if (isHeaderInfoEvent(event)) {
287
+ // TypeScript knows event.detail is TransactionHeaderInfo
288
+ console.log(event.detail.from, event.detail.to)
289
+ }
290
+ }
291
+ ```
@@ -0,0 +1,303 @@
1
+ # Transaction Recomposition - Quick Reference
2
+
3
+ This is a quick reference for implementing transaction modification recomposition in parent containers.
4
+
5
+ ## The Golden Rule
6
+
7
+ **Always recompose the original transaction structure, never create new wrappers from scratch.**
8
+
9
+ ## Why Recomposition?
10
+
11
+ Your transaction might be deeply nested:
12
+
13
+ ```
14
+ executeRelayCall( ← signature, nonce, timestamps
15
+ execute( ← operationType, target, value
16
+ executeBatch([ ← arrays of types, targets, values
17
+ execute( ← another wrapper
18
+ transfer(...) ← THE ACTUAL TRANSACTION (what user modifies)
19
+ ),
20
+ setData(...)
21
+ ])
22
+ )
23
+ )
24
+ ```
25
+
26
+ If you create new wrappers, you lose:
27
+ - ❌ Signatures (now invalid)
28
+ - ❌ Nonces (wrong or missing)
29
+ - ❌ Operation types (defaulted incorrectly)
30
+ - ❌ Values (lost ether amounts)
31
+ - ❌ Timestamps (validity lost)
32
+
33
+ ## The Recomposition Pattern
34
+
35
+ ### Step 1: Clone the Original
36
+
37
+ ```typescript
38
+ const recomposed = structuredClone(originalTransaction)
39
+ ```
40
+
41
+ **Why clone?** Preserve the original for comparison and rollback.
42
+
43
+ ### Step 2: Navigate to Target
44
+
45
+ ```typescript
46
+ // Path example: [0, 1, 2] means batch[0].children[1].children[2]
47
+ let current = recomposed
48
+ const parents = [recomposed]
49
+
50
+ for (let i = 0; i < path.length - 1; i++) {
51
+ current = current.children[path[i]]
52
+ parents.push(current)
53
+ }
54
+
55
+ // Update the target transaction
56
+ current.children[path[path.length - 1]].data = modifiedData
57
+ ```
58
+
59
+ ### Step 3: Recompose Bottom-Up
60
+
61
+ ```typescript
62
+ // Work backwards from deepest parent to root
63
+ for (let i = parents.length - 1; i >= 0; i--) {
64
+ const parent = parents[i]
65
+
66
+ if (parent.children && parent.functionName) {
67
+ parent.data = recomposeWrapper(parent)
68
+ }
69
+ }
70
+ ```
71
+
72
+ **Why bottom-up?** Each parent needs its children's updated data.
73
+
74
+ ### Step 4: Re-decode and Verify
75
+
76
+ ```typescript
77
+ const reDecoded = await decoder.decode(recomposed)
78
+
79
+ // Now you have a fresh DecoderResult with:
80
+ // - Modified transaction at the correct path
81
+ // - All wrappers recomposed with original args
82
+ // - Everything validated and ready to submit
83
+ ```
84
+
85
+ ## Wrapper Recomposition
86
+
87
+ Each wrapper type needs its own recomposition logic:
88
+
89
+ ### Execute
90
+
91
+ ```typescript
92
+ case 'execute':
93
+ return encodeFunctionData({
94
+ abi: UP_ABI,
95
+ functionName: 'execute',
96
+ args: [
97
+ getArgByName(wrapper.args, 'operationType') as number, // ✓ From original
98
+ getArgByName(wrapper.args, 'target') as string, // ✓ From original
99
+ getArgByName(wrapper.args, 'value') as bigint, // ✓ From original
100
+ wrapper.children[0].data // ✓ Updated child
101
+ ]
102
+ })
103
+ ```
104
+
105
+ ### ExecuteBatch
106
+
107
+ ```typescript
108
+ case 'executeBatch':
109
+ return encodeFunctionData({
110
+ abi: UP_ABI,
111
+ functionName: 'executeBatch',
112
+ args: [
113
+ getArgByName(wrapper.args, 'operationTypes') as number[], // ✓ From original (array)
114
+ getArgByName(wrapper.args, 'targets') as string[], // ✓ From original (array)
115
+ getArgByName(wrapper.args, 'values') as bigint[], // ✓ From original (array)
116
+ wrapper.children.map(c => c.data) // ✓ Updated children (array)
117
+ ]
118
+ })
119
+ ```
120
+
121
+ ### ExecuteRelayCall
122
+
123
+ ```typescript
124
+ case 'executeRelayCall':
125
+ return encodeFunctionData({
126
+ abi: KEY_MANAGER_ABI,
127
+ functionName: 'executeRelayCall',
128
+ args: [
129
+ getArgByName(wrapper.args, 'signature') as string, // ✓ From original
130
+ getArgByName(wrapper.args, 'nonce') as bigint, // ✓ From original
131
+ getArgByName(wrapper.args, 'validityTimestamps') as bigint,// ✓ From original
132
+ wrapper.children[0].data // ✓ Updated execute/executeBatch
133
+ ]
134
+ })
135
+ ```
136
+
137
+ ## Common Mistakes
138
+
139
+ ### ❌ Mistake 1: Creating New Wrappers
140
+
141
+ ```typescript
142
+ // WRONG - You lose all the original arguments
143
+ const newData = encodeFunctionData({
144
+ abi: UP_ABI,
145
+ functionName: 'execute',
146
+ args: [0, target, 0, modifiedTransfer] // operationType=0? value=0? Wrong!
147
+ })
148
+ ```
149
+
150
+ ### ❌ Mistake 2: Only Updating Leaf
151
+
152
+ ```typescript
153
+ // WRONG - Parent wrappers still have old data
154
+ recomposed.children[0].children[1].data = modifiedData
155
+ // Forgot to recompose the parent wrappers!
156
+ // The actual transaction.data is still old
157
+ ```
158
+
159
+ ### ❌ Mistake 3: Top-Down Recomposition
160
+
161
+ ```typescript
162
+ // WRONG - Children not updated yet
163
+ for (let i = 0; i < parents.length; i++) { // ❌ Going forward
164
+ parent.data = recomposeWrapper(parent) // Children still have old data!
165
+ }
166
+ ```
167
+
168
+ ## Correct Flow Visualization
169
+
170
+ ```
171
+ Original Transaction:
172
+ executeRelayCall(0xSIG, 42, 1234567890,
173
+ execute(0, 0xToken, 0,
174
+ transfer(alice, bob, 100, false, 0x) ← force=false
175
+ )
176
+ )
177
+
178
+ User modifies force to true ↓
179
+
180
+ Step 1: Clone
181
+ recomposed = clone(original)
182
+
183
+ Step 2: Navigate & Update
184
+ recomposed.children[0].data = transfer(..., true, ...) ← force=true
185
+
186
+ Step 3: Recompose Bottom-Up
187
+ recomposed.data = executeRelayCall(0xSIG, 42, 1234567890, ← Same signature!
188
+ execute(0, 0xToken, 0, ← Same args!
189
+ transfer(alice, bob, 100, true, 0x) ← NEW! force=true
190
+ )
191
+ )
192
+
193
+ Step 4: Re-decode
194
+ reDecoded = decoder.decode(recomposed)
195
+
196
+ Result: Complete valid transaction ready to submit ✓
197
+ ```
198
+
199
+ ## Testing Recomposition
200
+
201
+ ### Verify Original Args Preserved
202
+
203
+ ```typescript
204
+ // After recomposition, check all wrappers
205
+ console.assert(
206
+ getArgByName(recomposed.args, 'signature') === getArgByName(original.args, 'signature'),
207
+ 'Signature must be preserved!'
208
+ )
209
+
210
+ console.assert(
211
+ getArgByName(recomposed.children[0].args, 'operationType') ===
212
+ getArgByName(original.children[0].args, 'operationType'),
213
+ 'OperationType must be preserved!'
214
+ )
215
+ ```
216
+
217
+ ### Verify Only Target Changed
218
+
219
+ ```typescript
220
+ // Only the modified transaction should differ
221
+ const originalTransfer = getTransactionAtPath(original, [0, 1])
222
+ const recomposedTransfer = getTransactionAtPath(recomposed, [0, 1])
223
+
224
+ console.assert(
225
+ getArgByName(originalTransfer.args, 'force') === false,
226
+ 'Original has force=false'
227
+ )
228
+
229
+ console.assert(
230
+ getArgByName(recomposedTransfer.args, 'force') === true,
231
+ 'Recomposed has force=true'
232
+ )
233
+
234
+ // Everything else should be the same
235
+ console.assert(
236
+ getArgByName(originalTransfer.args, 'from') === getArgByName(recomposedTransfer.args, 'from'),
237
+ 'Other args unchanged'
238
+ )
239
+ ```
240
+
241
+ ## Checklist
242
+
243
+ When implementing recomposition:
244
+
245
+ - [ ] Clone original transaction (don't mutate)
246
+ - [ ] Navigate to target using transactionPath
247
+ - [ ] Update target transaction's data
248
+ - [ ] Recompose wrappers **bottom-up** (deepest first)
249
+ - [ ] Use getArgByName with original wrapper's args (preserve all args)
250
+ - [ ] Only update children data (don't change wrapper args)
251
+ - [ ] Re-decode entire transaction at the end
252
+ - [ ] Verify signature/nonce/timestamps preserved
253
+ - [ ] Test with nested wrappers (executeRelayCall)
254
+
255
+ ## Real-World Example
256
+
257
+ ```typescript
258
+ async function recomposeTransaction(
259
+ original: DecoderResult,
260
+ modification: TransactionModificationWithContext
261
+ ): Promise<DecoderResult> {
262
+ // 1. Clone
263
+ const recomposed = structuredClone(original)
264
+
265
+ // 2. Navigate
266
+ let current = recomposed
267
+ const parents = [recomposed]
268
+
269
+ for (let i = 0; i < modification.transactionPath.length - 1; i++) {
270
+ current = current.children[modification.transactionPath[i]]
271
+ parents.push(current)
272
+ }
273
+
274
+ // 3. Update target
275
+ const targetIndex = modification.transactionPath[modification.transactionPath.length - 1]
276
+ current.children[targetIndex].data = modification.data
277
+
278
+ // 4. Recompose bottom-up
279
+ for (let i = parents.length - 1; i >= 0; i--) {
280
+ const parent = parents[i]
281
+ if (parent.children && parent.functionName) {
282
+ parent.data = await recomposeWrapper(parent)
283
+ }
284
+ }
285
+
286
+ // 5. Re-decode
287
+ const reDecoded = await decoder.decode(recomposed)
288
+
289
+ return reDecoded
290
+ }
291
+ ```
292
+
293
+ ## Key Takeaways
294
+
295
+ 1. **Recompose, don't recreate** - Start with original, update incrementally
296
+ 2. **Bottom-up** - Deepest children first, root last
297
+ 3. **Preserve everything** - Use getArgByName with original's args for all wrappers
298
+ 4. **Re-decode** - Always verify the final result
299
+ 5. **Test thoroughly** - Especially with nested transactions and signatures
300
+
301
+ ---
302
+
303
+ **Remember:** The goal is to change **only** what the user modified, while keeping everything else (signatures, nonces, operation types, values) exactly as they were in the original transaction.