@lukso/transaction-view-headless 0.4.3-dev.a07a66f → 0.4.3-dev.e33b252
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.
- package/.turbo/turbo-build.log +31 -33
- package/CONTEXT_VARIANT_GUIDE.md +4 -4
- package/dist/composables/index.cjs +1 -1
- package/dist/composables/index.cjs.map +1 -1
- package/dist/composables/index.d.cts +3 -3
- package/dist/composables/index.d.ts +3 -3
- package/dist/composables/index.js +1 -1
- package/dist/composables/index.js.map +1 -1
- package/dist/{index-CNUHqRdk.d.ts → index-CMLU1hKL.d.ts} +2 -2
- package/dist/{index-BqL7xdDl.d.cts → index-CSDjhiKV.d.cts} +2 -2
- package/dist/index.cjs +1 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -5
- package/dist/index.d.ts +4 -5
- package/dist/index.js +1 -55
- package/dist/index.js.map +1 -1
- package/dist/registry/index.d.cts +2 -2
- package/dist/registry/index.d.ts +2 -2
- package/dist/types/index.cjs +0 -68
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +169 -229
- package/dist/types/index.d.ts +169 -229
- package/dist/types/index.js +0 -55
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.cts +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/{view-loader--mEEI8YY.d.ts → view-loader-dD86QAlQ.d.cts} +1 -1
- package/dist/{view-loader-D7ApWfdN.d.cts → view-loader-ii8AxiQR.d.ts} +1 -1
- package/package.json +3 -3
- package/src/composables/use-transaction-playback.ts +5 -8
- package/src/types/index.ts +0 -14
- package/ARG_ACCESS_PATTERN.md +0 -265
- package/EVENT_SYSTEM_SUMMARY.md +0 -601
- package/HEADER_EVENTS_GUIDE.md +0 -291
- package/RECOMPOSITION_REFERENCE.md +0 -303
- package/TRANSACTION_MODIFICATION_GUIDE.md +0 -927
- package/dist/view-matching-C9G3z8lQ.d.cts +0 -203
- package/dist/view-matching-C9G3z8lQ.d.ts +0 -203
- package/src/types/view-events.ts +0 -322
package/HEADER_EVENTS_GUIDE.md
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
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
|
-
```
|
|
@@ -1,303 +0,0 @@
|
|
|
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.
|