@lukso/transaction-view-headless 0.2.2-dev.a8c9315

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 (76) hide show
  1. package/.turbo/turbo-build.log +51 -0
  2. package/CHANGELOG.md +88 -0
  3. package/COMPONENT_ARCHITECTURE.md +190 -0
  4. package/COMPONENT_CONVERSION_ANALYSIS.md +371 -0
  5. package/CONTEXT_VARIANT_GUIDE.md +425 -0
  6. package/LAZY_LOADING_GUIDE.md +534 -0
  7. package/LICENSE +201 -0
  8. package/PLAYBACK_EXAMPLES.md +658 -0
  9. package/PLAYBACK_GUIDE.md +402 -0
  10. package/README.md +43 -0
  11. package/REGISTRY_SUMMARY.md +638 -0
  12. package/SUFFERING_ECONOMICS.md +346 -0
  13. package/SUFFERING_ECONOMICS.zip +0 -0
  14. package/VIEW_REGISTRATION_GUIDE.md +795 -0
  15. package/dist/composables/index.cjs +1321 -0
  16. package/dist/composables/index.cjs.map +1 -0
  17. package/dist/composables/index.d.cts +6 -0
  18. package/dist/composables/index.d.ts +6 -0
  19. package/dist/composables/index.js +1276 -0
  20. package/dist/composables/index.js.map +1 -0
  21. package/dist/index-CMLU1hKL.d.ts +374 -0
  22. package/dist/index-CSDjhiKV.d.cts +374 -0
  23. package/dist/index.cjs +1534 -0
  24. package/dist/index.cjs.map +1 -0
  25. package/dist/index.d.cts +93 -0
  26. package/dist/index.d.ts +93 -0
  27. package/dist/index.js +1463 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/registry/index.cjs +522 -0
  30. package/dist/registry/index.cjs.map +1 -0
  31. package/dist/registry/index.d.cts +46 -0
  32. package/dist/registry/index.d.ts +46 -0
  33. package/dist/registry/index.js +487 -0
  34. package/dist/registry/index.js.map +1 -0
  35. package/dist/types/index.cjs +19 -0
  36. package/dist/types/index.cjs.map +1 -0
  37. package/dist/types/index.d.cts +203 -0
  38. package/dist/types/index.d.ts +203 -0
  39. package/dist/types/index.js +1 -0
  40. package/dist/types/index.js.map +1 -0
  41. package/dist/utils/index.cjs +217 -0
  42. package/dist/utils/index.cjs.map +1 -0
  43. package/dist/utils/index.d.cts +47 -0
  44. package/dist/utils/index.d.ts +47 -0
  45. package/dist/utils/index.js +188 -0
  46. package/dist/utils/index.js.map +1 -0
  47. package/dist/view-loader-dD86QAlQ.d.cts +121 -0
  48. package/dist/view-loader-ii8AxiQR.d.ts +121 -0
  49. package/package.json +64 -0
  50. package/publish.log +0 -0
  51. package/src/composables/index.ts +59 -0
  52. package/src/composables/use-image-loader.ts +315 -0
  53. package/src/composables/use-transaction-playback.ts +396 -0
  54. package/src/composables/use-transaction-view.ts +309 -0
  55. package/src/config/lukso-config.ts +171 -0
  56. package/src/examples/context-aware-views.ts +300 -0
  57. package/src/examples/example-views.ts +197 -0
  58. package/src/examples/lit-plus-rn-strategy.ts +448 -0
  59. package/src/examples/lsp7-custom-matcher-examples.ts +174 -0
  60. package/src/examples/opt-in-context-examples.ts +372 -0
  61. package/src/examples/optimized-view-definitions.ts +408 -0
  62. package/src/examples/vue-to-lit-compilation.ts +358 -0
  63. package/src/hooks/useAddressQuery.ts +135 -0
  64. package/src/hooks/useAddressResolution.ts +437 -0
  65. package/src/index.ts +36 -0
  66. package/src/registry/index.ts +13 -0
  67. package/src/registry/view-loader.ts +305 -0
  68. package/src/registry/view-registry.ts +135 -0
  69. package/src/types/index.ts +16 -0
  70. package/src/types/view-contexts.ts +72 -0
  71. package/src/types/view-matching.ts +173 -0
  72. package/src/utils/context-matcher.ts +165 -0
  73. package/src/utils/index.ts +5 -0
  74. package/src/utils/view-matcher.ts +338 -0
  75. package/tsconfig.json +20 -0
  76. package/tsup.config.ts +34 -0
@@ -0,0 +1,315 @@
1
+ import type { ImageData } from '@lukso/transaction-decoder'
2
+
3
+ // Force cache invalidation
4
+ console.log(
5
+ '🔥 LOADING NEW useAddressResolver CODE - should see GraphQL integration'
6
+ )
7
+
8
+ /**
9
+ * Image loading configuration for different contexts
10
+ */
11
+ export interface ImageLoadOptions {
12
+ width?: number
13
+ height?: number
14
+ dpr?: number
15
+ index?: number
16
+ }
17
+
18
+ /**
19
+ * Resolved image result
20
+ */
21
+ export interface ImageResult {
22
+ src: string
23
+ width?: number
24
+ height?: number
25
+ }
26
+
27
+ /**
28
+ * Address/Profile data structure for image resolution
29
+ * Note: GraphQL pluralizes field names (icons, not icon)
30
+ */
31
+ export interface AddressImageData {
32
+ profileImages?: ImageData[]
33
+ avatar?: ImageData[]
34
+ icons?: ImageData[] // GraphQL pluralizes to "icons"
35
+ images?: ImageData[]
36
+ __gqltype?: string
37
+ }
38
+
39
+ // Image loading cache for preventing duplicate requests
40
+ const imageLoadCache = new Map<string, Promise<ImageResult | null>>()
41
+
42
+ /**
43
+ * Generate cache key for image loading based on image data and options
44
+ */
45
+ function getImageCacheKey(imageData: any[], options: ImageLoadOptions): string {
46
+ // Create a stable key based on the image data sources and size requirements
47
+ const imageUrls = imageData
48
+ .filter((img) => img?.url || img?.src)
49
+ .map((img) => img.url || img.src)
50
+ .sort()
51
+ .join(',')
52
+
53
+ const sizeKey = `${options.width || 32}x${options.height || options.width || 32}@${options.dpr || 1}`
54
+
55
+ return `${imageUrls}-${sizeKey}`
56
+ }
57
+
58
+ /**
59
+ * Composable for loading images from various sources
60
+ * Extracted from Vue AddressView component for framework-agnostic use
61
+ */
62
+ export function useImageLoader() {
63
+ /**
64
+ * Load image from GraphQL-resolved image groups using proper size selection
65
+ */
66
+ async function loadImage(
67
+ imageData: any[],
68
+ options: ImageLoadOptions = {}
69
+ ): Promise<ImageResult | null> {
70
+ try {
71
+ if (!imageData || imageData.length === 0) {
72
+ return null
73
+ }
74
+
75
+ // Default options
76
+ const loadOptions = {
77
+ width: options.width || 32,
78
+ height: options.height || options.width || 32,
79
+ dpr:
80
+ options.dpr ||
81
+ (typeof window !== 'undefined' ? window.devicePixelRatio : 1),
82
+ ignoreHead: true, // Skip HEAD requests in component context
83
+ }
84
+
85
+ // Check cache first to prevent duplicate requests
86
+ const cacheKey = getImageCacheKey(imageData, loadOptions)
87
+ const cachedPromise = imageLoadCache.get(cacheKey)
88
+ if (cachedPromise) {
89
+ console.log('📸 loadImage: Using cached promise for', cacheKey)
90
+ return cachedPromise
91
+ }
92
+
93
+ console.log(
94
+ '📸 loadImage: Creating new request for',
95
+ imageData.length,
96
+ 'options:',
97
+ loadOptions
98
+ )
99
+
100
+ // Create and cache the loading promise
101
+ const loadingPromise = (async (): Promise<ImageResult | null> => {
102
+ // Import the proper getImage function from decoder package
103
+ const { getImage } = await import('@lukso/transaction-decoder')
104
+
105
+ const result = await getImage(imageData, loadOptions)
106
+
107
+ if (result?.src) {
108
+ console.log('📸 loadImage: Selected image:', result.src)
109
+ return {
110
+ src: result.src,
111
+ width: result.width,
112
+ height: result.height,
113
+ }
114
+ }
115
+
116
+ return null
117
+ })()
118
+
119
+ // Cache the promise
120
+ imageLoadCache.set(cacheKey, loadingPromise)
121
+
122
+ // Clean up cache on completion (success or failure)
123
+ loadingPromise.finally(() => {
124
+ // Keep successful results cached, remove failures
125
+ loadingPromise
126
+ .then((result) => {
127
+ if (!result) {
128
+ imageLoadCache.delete(cacheKey)
129
+ }
130
+ })
131
+ .catch(() => {
132
+ imageLoadCache.delete(cacheKey)
133
+ })
134
+ })
135
+
136
+ return loadingPromise
137
+ } catch (error) {
138
+ console.warn('Failed to load image:', error)
139
+ return null
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get profile/avatar image from address data
145
+ * Priority differs based on type:
146
+ * - Profiles: profileImages first
147
+ * - NFTs/Contracts: icons first (GraphQL pluralizes to "icons"), then images
148
+ * - Avatars are from other contracts (not used yet)
149
+ */
150
+ async function loadProfileImage(
151
+ addressData: AddressImageData,
152
+ options: ImageLoadOptions = {}
153
+ ): Promise<ImageResult | null> {
154
+ // For Profiles, prioritize profileImages
155
+ if (addressData.__gqltype === 'Profile') {
156
+ // Try profileImages first
157
+ if (addressData.profileImages?.length) {
158
+ const result = await loadImage(addressData.profileImages, options)
159
+ if (result) return result
160
+ }
161
+ // Fallback to icons
162
+ if (addressData.icons?.length) {
163
+ const result = await loadImage(addressData.icons, options)
164
+ if (result) return result
165
+ }
166
+ // Fallback to images
167
+ if (addressData.images?.length) {
168
+ return loadImage(addressData.images, options)
169
+ }
170
+ return null
171
+ }
172
+
173
+ // For NFTs/Contracts, prioritize icons, fallback to images
174
+ // Tokens/Assets will never have profileImages, so only check icons and images
175
+ // Try icons first
176
+ if (addressData.icons?.length) {
177
+ const result = await loadImage(addressData.icons, options)
178
+ if (result) return result
179
+ }
180
+ // Fallback to images
181
+ if (addressData.images?.length) {
182
+ return loadImage(addressData.images, options)
183
+ }
184
+
185
+ return null
186
+ }
187
+
188
+ /**
189
+ * Load background image from resolved address data
190
+ */
191
+ async function loadBackgroundImage(
192
+ addressData: AddressImageData & { backgroundImages?: any[] },
193
+ options: ImageLoadOptions = {}
194
+ ): Promise<ImageResult | null> {
195
+ const imageGroup = addressData.backgroundImages || []
196
+ return loadImage(imageGroup, options)
197
+ }
198
+
199
+ /**
200
+ * Get fallback image based on address type
201
+ */
202
+ function getFallbackImage(addressData: AddressImageData): ImageResult {
203
+ const isProfile = addressData.__gqltype === 'Profile'
204
+ return {
205
+ src: isProfile
206
+ ? '/assets/images/profile-default.svg'
207
+ : '/assets/images/token-default.svg',
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Determine if address represents a contract (has square icon)
213
+ */
214
+ function isContractAddress(addressData: AddressImageData): boolean {
215
+ return addressData.__gqltype !== 'Profile'
216
+ }
217
+
218
+ return {
219
+ loadImage,
220
+ loadProfileImage,
221
+ loadBackgroundImage,
222
+ getFallbackImage,
223
+ isContractAddress,
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Composable for address resolution and formatting
229
+ * Connected to decoder package's GraphQL AddressResolver
230
+ */
231
+ export function useAddressResolver() {
232
+ /**
233
+ * Resolve address data using decoder package integration
234
+ * Uses the batched address resolution from useAddressResolution
235
+ */
236
+ async function resolveAddress(address: string): Promise<any> {
237
+ console.log('🔥 useAddressResolver.resolveAddress called with:', address)
238
+
239
+ try {
240
+ // Import and use the batched resolution from the hooks
241
+ const { resolveAddress: batchedResolve } = await import(
242
+ '../hooks/useAddressResolution'
243
+ )
244
+
245
+ console.log('🔥 useAddressResolver: calling batchedResolve...')
246
+ const resolved = await batchedResolve(address)
247
+ console.log('🔥 useAddressResolver: batchedResolve returned:', resolved)
248
+
249
+ // Always return something, even if no profile - this ensures we don't return null unnecessarily
250
+ const result = {
251
+ // Preserve all image arrays from GraphQL
252
+ ...resolved,
253
+ // Override with computed values only if not already set by GraphQL
254
+ address: resolved.address,
255
+ name: resolved.name || '',
256
+ __gqltype:
257
+ resolved.__gqltype || (resolved.hasProfile ? 'Profile' : 'Contract'),
258
+ }
259
+
260
+ console.log('🔥 useAddressResolver: returning result:', result)
261
+ return result
262
+ } catch (error) {
263
+ console.error('🔥 useAddressResolver: Failed to resolve address:', error)
264
+
265
+ // Return a fallback instead of null
266
+ return {
267
+ address,
268
+ name: '',
269
+ __gqltype: 'Contract',
270
+ profileImages: [],
271
+ avatar: [],
272
+ icon: [],
273
+ }
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Format address display name from resolved data
279
+ */
280
+ function formatDisplayName(addressData: any): string {
281
+ const baseName =
282
+ addressData.name?.toLowerCase?.() ||
283
+ addressData.lsp4TokenName ||
284
+ addressData.baseAsset?.lsp4TokenName ||
285
+ ''
286
+
287
+ const symbol =
288
+ addressData.lsp4TokenSymbol ||
289
+ addressData.baseAsset?.lsp4TokenSymbol ||
290
+ ''
291
+
292
+ return symbol ? `${baseName} (${symbol})` : baseName
293
+ }
294
+
295
+ /**
296
+ * Get address prefix based on type
297
+ */
298
+ function getAddressPrefix(addressData: any): string {
299
+ return addressData.__gqltype !== 'Profile' ? '🪙 ' : '@'
300
+ }
301
+
302
+ /**
303
+ * Check if address should show name colors
304
+ */
305
+ function shouldShowNameColor(addressData: any): boolean {
306
+ return addressData.__gqltype === 'Profile'
307
+ }
308
+
309
+ return {
310
+ resolveAddress,
311
+ formatDisplayName,
312
+ getAddressPrefix,
313
+ shouldShowNameColor,
314
+ }
315
+ }
@@ -0,0 +1,396 @@
1
+ import type { DecoderResult } from '@lukso/transaction-decoder'
2
+ import type { Chain } from 'viem'
3
+ import { getGlobalRegistry } from '../registry/view-registry'
4
+ import type {
5
+ ViewMatch,
6
+ ViewMatchOptions,
7
+ ViewRegistry,
8
+ } from '../types/view-matching'
9
+
10
+ /**
11
+ * Transaction Playback System
12
+ *
13
+ * Provides index-based access to transaction views with smart handling of batches:
14
+ *
15
+ * **Single Transaction:**
16
+ * - Index 0: The transaction itself
17
+ * - Index 1: The transaction itself (same as 0)
18
+ * - Index 2+: null
19
+ *
20
+ * **Batch Transaction (setDataBatch, executeBatch):**
21
+ * - Index 0: Summary/expandable view of the batch
22
+ * - Index 1: First child transaction
23
+ * - Index 2: Second child transaction
24
+ * - Index N: Nth child transaction
25
+ *
26
+ * This design allows UI flexibility:
27
+ * - Always use index 1 to skip batch summaries
28
+ * - Use index 0 to show batch overview (optional)
29
+ * - Navigate batch children by index
30
+ */
31
+
32
+ /**
33
+ * Options for transaction playback
34
+ */
35
+ export interface TransactionPlaybackOptions extends Partial<ViewMatchOptions> {
36
+ /** Custom registry to use instead of global */
37
+ registry?: ViewRegistry
38
+ /** Fallback view ID if no matches found */
39
+ fallbackViewId?: string
40
+ /** Chain for address resolution */
41
+ chain?: Chain
42
+ }
43
+
44
+ /**
45
+ * Result of selecting a view at a specific index
46
+ */
47
+ export interface PlaybackIndexResult {
48
+ /** The selected view match (or null if invalid index) */
49
+ match: ViewMatch | null
50
+ /** The transaction data for this index */
51
+ transaction: DecoderResult | null
52
+ /** Index information */
53
+ indexInfo: {
54
+ /** Requested index */
55
+ requested: number
56
+ /** Actual index in children array (for batches) */
57
+ childIndex: number | null
58
+ /** Whether this is a batch summary view (index 0 of batch) */
59
+ isBatchSummary: boolean
60
+ /** Whether this is a valid index */
61
+ isValid: boolean
62
+ }
63
+ /** Batch information (if applicable) */
64
+ batchInfo: {
65
+ /** Whether the root transaction is a batch */
66
+ isBatch: boolean
67
+ /** Total number of children in batch (0 if not batch) */
68
+ childCount: number
69
+ /** Available indices (e.g., [0, 1, 2, 3] for 3-child batch) */
70
+ availableIndices: number[]
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Complete playback state for a transaction
76
+ */
77
+ export interface TransactionPlaybackState {
78
+ /** The root transaction */
79
+ rootTransaction: DecoderResult
80
+ /** Whether this is a batch transaction */
81
+ isBatch: boolean
82
+ /** Child transactions (empty if not batch) */
83
+ children: DecoderResult[]
84
+ /** Total count of available indices */
85
+ indexCount: number
86
+ /** Batch information */
87
+ batchInfo: {
88
+ /** Whether the root transaction is a batch */
89
+ isBatch: boolean
90
+ /** Total number of children in batch (0 if not batch) */
91
+ childCount: number
92
+ /** Available indices (e.g., [0, 1, 2, 3] for 3-child batch) */
93
+ availableIndices: number[]
94
+ }
95
+ /** Get view match for a specific index */
96
+ getViewAtIndex: (index: number) => PlaybackIndexResult
97
+ /** Get all available views */
98
+ getAllViews: () => PlaybackIndexResult[]
99
+ }
100
+
101
+ /**
102
+ * Check if a transaction is a batch transaction
103
+ */
104
+ function isBatchTransaction(transaction: DecoderResult): boolean {
105
+ return (
106
+ transaction.resultType === 'executeBatch' ||
107
+ transaction.resultType === 'setDataBatch' ||
108
+ !!(transaction as any).children?.length
109
+ )
110
+ }
111
+
112
+ /**
113
+ * Get children from a batch transaction
114
+ */
115
+ function getBatchChildren(transaction: DecoderResult): DecoderResult[] {
116
+ if (!isBatchTransaction(transaction)) {
117
+ return []
118
+ }
119
+ return (transaction as any).children || []
120
+ }
121
+
122
+ /**
123
+ * Core function to select view at a specific index
124
+ */
125
+ function selectViewAtIndex(
126
+ rootTransaction: DecoderResult,
127
+ index: number,
128
+ options: TransactionPlaybackOptions
129
+ ): PlaybackIndexResult {
130
+ const {
131
+ registry = getGlobalRegistry(),
132
+ framework = 'lit',
133
+ fallbackViewId,
134
+ minScore = 0,
135
+ includeFrameworkConfig = true,
136
+ } = options
137
+
138
+ const isBatch = isBatchTransaction(rootTransaction)
139
+ const children = getBatchChildren(rootTransaction)
140
+ const childCount = children.length
141
+
142
+ // Initialize result
143
+ const result: PlaybackIndexResult = {
144
+ match: null,
145
+ transaction: null,
146
+ indexInfo: {
147
+ requested: index,
148
+ childIndex: null,
149
+ isBatchSummary: false,
150
+ isValid: false,
151
+ },
152
+ batchInfo: {
153
+ isBatch,
154
+ childCount,
155
+ availableIndices: [],
156
+ },
157
+ }
158
+
159
+ // Calculate available indices
160
+ if (isBatch) {
161
+ // Batch: 0 (summary) + N children
162
+ result.batchInfo.availableIndices = Array.from(
163
+ { length: childCount + 1 },
164
+ (_, i) => i
165
+ )
166
+ } else {
167
+ // Single: 0 and 1 both map to the transaction
168
+ result.batchInfo.availableIndices = [0, 1]
169
+ }
170
+
171
+ // Validate index
172
+ if (index < 0) {
173
+ return result // Invalid: negative index
174
+ }
175
+
176
+ // Handle single transaction
177
+ if (!isBatch) {
178
+ if (index === 0 || index === 1) {
179
+ result.indexInfo.isValid = true
180
+ result.transaction = rootTransaction
181
+
182
+ // Find matching view for the transaction
183
+ const matchOptions: ViewMatchOptions = {
184
+ framework: framework as any,
185
+ minScore,
186
+ maxMatches: 1,
187
+ includeFrameworkConfig,
188
+ }
189
+
190
+ const matches = registry.findMatches(rootTransaction, matchOptions)
191
+ result.match = matches[0] || null
192
+
193
+ // Try fallback if no match
194
+ if (!result.match && fallbackViewId) {
195
+ const fallbackView = registry.getView(fallbackViewId)
196
+ if (fallbackView && fallbackView.frameworks[framework]) {
197
+ result.match = {
198
+ view: fallbackView,
199
+ score: 0,
200
+ matchedCriteria: [],
201
+ frameworkConfig: includeFrameworkConfig
202
+ ? fallbackView.frameworks[framework]
203
+ : undefined,
204
+ }
205
+ }
206
+ }
207
+
208
+ return result
209
+ }
210
+
211
+ // Invalid index for single transaction
212
+ return result
213
+ }
214
+
215
+ // Handle batch transaction
216
+ if (index === 0) {
217
+ // Index 0: Batch summary view
218
+ result.indexInfo.isValid = true
219
+ result.indexInfo.isBatchSummary = true
220
+ result.transaction = rootTransaction
221
+
222
+ // Find batch summary view
223
+ const matchOptions: ViewMatchOptions = {
224
+ framework: framework as any,
225
+ minScore,
226
+ maxMatches: 1,
227
+ includeFrameworkConfig,
228
+ }
229
+
230
+ const matches = registry.findMatches(rootTransaction, matchOptions)
231
+ result.match = matches[0] || null
232
+
233
+ // Try fallback if no match
234
+ if (!result.match && fallbackViewId) {
235
+ const fallbackView = registry.getView(fallbackViewId)
236
+ if (fallbackView && fallbackView.frameworks[framework]) {
237
+ result.match = {
238
+ view: fallbackView,
239
+ score: 0,
240
+ matchedCriteria: [],
241
+ frameworkConfig: includeFrameworkConfig
242
+ ? fallbackView.frameworks[framework]
243
+ : undefined,
244
+ }
245
+ }
246
+ }
247
+
248
+ return result
249
+ }
250
+
251
+ // Index 1+: Child transactions
252
+ const childIndex = index - 1
253
+ if (childIndex >= 0 && childIndex < childCount) {
254
+ result.indexInfo.isValid = true
255
+ result.indexInfo.childIndex = childIndex
256
+ result.transaction = children[childIndex]
257
+
258
+ // Find matching view for the child transaction
259
+ const matchOptions: ViewMatchOptions = {
260
+ framework: framework as any,
261
+ minScore,
262
+ maxMatches: 1,
263
+ includeFrameworkConfig,
264
+ }
265
+
266
+ const matches = registry.findMatches(result.transaction, matchOptions)
267
+ result.match = matches[0] || null
268
+
269
+ // Try fallback if no match
270
+ if (!result.match && fallbackViewId) {
271
+ const fallbackView = registry.getView(fallbackViewId)
272
+ if (fallbackView && fallbackView.frameworks[framework]) {
273
+ result.match = {
274
+ view: fallbackView,
275
+ score: 0,
276
+ matchedCriteria: [],
277
+ frameworkConfig: includeFrameworkConfig
278
+ ? fallbackView.frameworks[framework]
279
+ : undefined,
280
+ }
281
+ }
282
+ }
283
+
284
+ return result
285
+ }
286
+
287
+ // Invalid index for batch
288
+ return result
289
+ }
290
+
291
+ /**
292
+ * Create a playback state for a transaction
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * const playback = createTransactionPlayback(transaction, { framework: 'lit' })
297
+ *
298
+ * // For single transaction:
299
+ * playback.getViewAtIndex(0) // Returns the transaction
300
+ * playback.getViewAtIndex(1) // Returns the transaction (same)
301
+ * playback.getViewAtIndex(2) // Returns null (invalid)
302
+ *
303
+ * // For batch transaction:
304
+ * playback.getViewAtIndex(0) // Returns batch summary
305
+ * playback.getViewAtIndex(1) // Returns first child
306
+ * playback.getViewAtIndex(2) // Returns second child
307
+ * ```
308
+ */
309
+ export function createTransactionPlayback(
310
+ rootTransaction: DecoderResult,
311
+ options: TransactionPlaybackOptions = {}
312
+ ): TransactionPlaybackState {
313
+ const isBatch = isBatchTransaction(rootTransaction)
314
+ const children = getBatchChildren(rootTransaction)
315
+ const childCount = children.length
316
+ const indexCount = isBatch ? childCount + 1 : 2 // batch: 0 + N, single: 0 and 1
317
+
318
+ // Calculate available indices
319
+ const availableIndices = isBatch
320
+ ? Array.from({ length: childCount + 1 }, (_, i) => i) // [0, 1, 2, ...]
321
+ : [0, 1] // Single: 0 and 1 both valid
322
+
323
+ return {
324
+ rootTransaction,
325
+ isBatch,
326
+ children,
327
+ indexCount,
328
+ batchInfo: {
329
+ isBatch,
330
+ childCount,
331
+ availableIndices,
332
+ },
333
+ getViewAtIndex: (index: number) =>
334
+ selectViewAtIndex(rootTransaction, index, options),
335
+ getAllViews: () => {
336
+ const views: PlaybackIndexResult[] = []
337
+ for (let i = 0; i < indexCount; i++) {
338
+ views.push(selectViewAtIndex(rootTransaction, i, options))
339
+ }
340
+ return views
341
+ },
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Simplified composable that just returns the view at a specific index
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * // Vue
351
+ * const index = ref(1) // Always show main transaction, skip batch summaries
352
+ * const result = useTransactionPlayback(transaction, index, { framework: 'lit' })
353
+ *
354
+ * // React
355
+ * const [index, setIndex] = useState(1)
356
+ * const result = useTransactionPlayback(transaction, index, { framework: 'lit' })
357
+ * ```
358
+ */
359
+ export function useTransactionPlayback(
360
+ rootTransaction: DecoderResult,
361
+ index: number,
362
+ options: TransactionPlaybackOptions = {}
363
+ ): PlaybackIndexResult {
364
+ return selectViewAtIndex(rootTransaction, index, options)
365
+ }
366
+
367
+ /**
368
+ * Helper to check if an index is valid for a transaction
369
+ */
370
+ export function isValidPlaybackIndex(
371
+ transaction: DecoderResult,
372
+ index: number
373
+ ): boolean {
374
+ const isBatch = isBatchTransaction(transaction)
375
+
376
+ if (!isBatch) {
377
+ return index === 0 || index === 1
378
+ }
379
+
380
+ const childCount = getBatchChildren(transaction).length
381
+ return index >= 0 && index <= childCount
382
+ }
383
+
384
+ /**
385
+ * Helper to get the range of valid indices for a transaction
386
+ */
387
+ export function getValidPlaybackIndices(transaction: DecoderResult): number[] {
388
+ const isBatch = isBatchTransaction(transaction)
389
+
390
+ if (!isBatch) {
391
+ return [0, 1]
392
+ }
393
+
394
+ const childCount = getBatchChildren(transaction).length
395
+ return Array.from({ length: childCount + 1 }, (_, i) => i)
396
+ }