@lukso/transaction-view-core 0.3.1-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 (62) hide show
  1. package/.turbo/turbo-build.log +33 -0
  2. package/CHANGELOG.md +104 -0
  3. package/LICENSE +201 -0
  4. package/README.md +264 -0
  5. package/dist/index.cjs +4573 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +493 -0
  8. package/dist/index.d.ts +493 -0
  9. package/dist/index.js +4534 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/shared/index.cjs +2480 -0
  12. package/dist/shared/index.cjs.map +1 -0
  13. package/dist/shared/index.d.cts +887 -0
  14. package/dist/shared/index.d.ts +887 -0
  15. package/dist/shared/index.js +2437 -0
  16. package/dist/shared/index.js.map +1 -0
  17. package/dist/styles/index.cjs +58 -0
  18. package/dist/styles/index.cjs.map +1 -0
  19. package/dist/styles/index.d.cts +9 -0
  20. package/dist/styles/index.d.ts +9 -0
  21. package/dist/styles/index.js +31 -0
  22. package/dist/styles/index.js.map +1 -0
  23. package/package.json +57 -0
  24. package/publish.log +0 -0
  25. package/src/components/asset-card.ts +426 -0
  26. package/src/components/contract-card.ts +238 -0
  27. package/src/components/description-view.ts +605 -0
  28. package/src/components/index.ts +20 -0
  29. package/src/components/info-panel.ts +437 -0
  30. package/src/components/permissions-block.ts +138 -0
  31. package/src/components/transaction-playback.ts +429 -0
  32. package/src/components/universal-token-view.ts +281 -0
  33. package/src/declaration.d.ts +4 -0
  34. package/src/examples/INTEGRATION_GUIDE.md +240 -0
  35. package/src/examples/TransactionContentWithReplacement.vue +269 -0
  36. package/src/index.ts +38 -0
  37. package/src/registry/index.ts +31 -0
  38. package/src/registry/lit-view-loader.ts +191 -0
  39. package/src/shared/address-display.ts +370 -0
  40. package/src/shared/address-view.ts +385 -0
  41. package/src/shared/custom-select.ts +164 -0
  42. package/src/shared/function-name-view.ts +89 -0
  43. package/src/shared/gas-provider-view.ts +218 -0
  44. package/src/shared/icon-view.ts +288 -0
  45. package/src/shared/image-list.ts +261 -0
  46. package/src/shared/image-loader-controller.ts +160 -0
  47. package/src/shared/index.ts +34 -0
  48. package/src/shared/load-web-components.ts +56 -0
  49. package/src/shared/notification-view.ts +132 -0
  50. package/src/shared/progress-view.ts +86 -0
  51. package/src/shared/safe-custom-element.ts +35 -0
  52. package/src/shared/scroll-to-bottom-button.ts +55 -0
  53. package/src/shared/signal-controller.ts +124 -0
  54. package/src/shared/transaction-view-controller.ts +345 -0
  55. package/src/shared/url-view.ts +129 -0
  56. package/src/shared/value-view.ts +510 -0
  57. package/src/styles/index.ts +28 -0
  58. package/src/styles/styles.css +1 -0
  59. package/src/vue-entry.ts +15 -0
  60. package/tailwind.config.cjs +9 -0
  61. package/tsconfig.json +22 -0
  62. package/tsup.config.ts +90 -0
@@ -0,0 +1,218 @@
1
+ import { Task } from '@lit/task'
2
+ import { useAddressResolver } from '@lukso/transaction-view-headless'
3
+ import makeBlockie from 'ethereum-blockies-base64'
4
+ import { html } from 'lit'
5
+ import { customElement, property, state } from 'lit/decorators.js'
6
+ import { safeCustomElement } from './safe-custom-element'
7
+ import './load-web-components'
8
+ import { CoreLitElement } from '../styles'
9
+
10
+ type GasProvider = { type: 'relayer' | 'controller'; id: string }
11
+ type Relayer = {
12
+ id: string
13
+ name: string
14
+ isCustom: boolean
15
+ apiUrl: string
16
+ chainIds: string[]
17
+ createdAt: number
18
+ website?: string
19
+ }
20
+
21
+ @safeCustomElement('gas-provider-view')
22
+ export class GasProviderView extends CoreLitElement {
23
+ @property({ type: Array, reflect: true }) allRelayers: Relayer[] = []
24
+ @property({ type: Number, reflect: true }) balance = 0
25
+ @property({ type: Object, attribute: 'selected-gas-provider', reflect: true })
26
+ selectedGasProvider?: GasProvider
27
+ @property({ attribute: 'controller-address', reflect: true })
28
+ controllerAddress?: string
29
+ @property({ attribute: 'controller-name', reflect: true })
30
+ controllerName?: string
31
+ @property({ attribute: 'symbol', reflect: true }) symbol: string = 'LYX'
32
+ @property({ type: Number, reflect: true }) decimals = 18
33
+ @property({ type: Number, attribute: 'transaction-fee', reflect: true })
34
+ transactionFee = 0
35
+ @property({ type: Number, attribute: 'relayer-available', reflect: true })
36
+ relayerAvailable = 0
37
+ @property({ type: Number, attribute: 'relayer-used', reflect: true })
38
+ relayerUsed = 0
39
+ @property({ type: Number, attribute: 'relayer-to-be-used', reflect: true })
40
+ relayerToBeUsed = 0
41
+ @property({ attribute: 'relayer-reset-date', reflect: true })
42
+ relayerResetDate: string = 'N/A'
43
+
44
+ private formatToken(amount: number) {
45
+ // naive formatting for display; consumers can override by slotting
46
+ return (amount / 10 ** this.decimals).toLocaleString(undefined, {
47
+ maximumFractionDigits: 6,
48
+ })
49
+ }
50
+
51
+ private onSelectChanged(e: Event) {
52
+ const value = (e.target as any).value as string | undefined
53
+ if (!value) return
54
+ if (this.controllerAddress && value === this.controllerAddress) {
55
+ const next: GasProvider = {
56
+ type: 'controller',
57
+ id: this.controllerAddress,
58
+ }
59
+ if (
60
+ !this.selectedGasProvider ||
61
+ this.selectedGasProvider.id !== next.id ||
62
+ this.selectedGasProvider.type !== next.type
63
+ ) {
64
+ this.selectedGasProvider = next
65
+ this.dispatchEvent(
66
+ new CustomEvent('on-gas-provider-change', {
67
+ detail: next,
68
+ bubbles: true,
69
+ })
70
+ )
71
+ }
72
+ return
73
+ }
74
+ // Map selection back to relayer by website/apiUrl fallback
75
+ const match = this.allRelayers.find(
76
+ (r) => r.website === value || r.apiUrl === value
77
+ )
78
+ if (match) {
79
+ const next: GasProvider = { type: 'relayer', id: match.id }
80
+ if (
81
+ !this.selectedGasProvider ||
82
+ this.selectedGasProvider.id !== next.id ||
83
+ this.selectedGasProvider.type !== next.type
84
+ ) {
85
+ this.selectedGasProvider = next
86
+ this.dispatchEvent(
87
+ new CustomEvent('on-gas-provider-change', {
88
+ detail: next,
89
+ bubbles: true,
90
+ })
91
+ )
92
+ }
93
+ return
94
+ }
95
+ }
96
+
97
+ private renderProgress(
98
+ totalAvailable: number,
99
+ used: number,
100
+ toBeUsed: number,
101
+ symbol: string,
102
+ hasError: boolean
103
+ ) {
104
+ const usedPercent = totalAvailable ? (used / totalAvailable) * 100 : 0
105
+ const toBeUsedPercent = totalAvailable
106
+ ? (toBeUsed / totalAvailable) * 100
107
+ : 0
108
+ const barOverHalf = usedPercent > 50
109
+ return html`<progress-view
110
+ class=${hasError ? 'error' : ''}
111
+ .data=${[
112
+ {
113
+ value: usedPercent,
114
+ color: 'var(--green-54)',
115
+ title: `${usedPercent}%`,
116
+ },
117
+ {
118
+ value: toBeUsedPercent,
119
+ color: 'var(--neutral-60)',
120
+ title: `${toBeUsedPercent}%`,
121
+ },
122
+ ]}
123
+ label-position=${barOverHalf ? 'left' : 'right'}
124
+ .label=${`${toBeUsed} / ${String(totalAvailable)} ${symbol}`}
125
+ >
126
+ </progress-view>`
127
+ }
128
+
129
+ private renderControllerProgress() {
130
+ return this.renderProgress(
131
+ this.balance,
132
+ 0,
133
+ this.transactionFee,
134
+ this.symbol,
135
+ this.transactionFee > this.balance
136
+ )
137
+ }
138
+
139
+ private renderRelayerProgress() {
140
+ return this.renderProgress(
141
+ this.relayerAvailable,
142
+ this.relayerUsed,
143
+ this.relayerToBeUsed,
144
+ 'M GAS',
145
+ this.relayerToBeUsed > this.relayerAvailable
146
+ )
147
+ }
148
+
149
+ private displayRelayerResetDate() {
150
+ const date = new Date(this.relayerResetDate)
151
+ return isNaN(date.getTime()) ? 'N/A' : date.toLocaleDateString()
152
+ }
153
+
154
+ override render() {
155
+ const formatted = this.formatToken(this.balance)
156
+
157
+ // Build select options: relayers first, then controller
158
+ const relayerOptions = this.allRelayers.map((r) => ({
159
+ value: r.website || r.apiUrl || `relayer:${r.id}`,
160
+ label: r.name,
161
+ disabled: false,
162
+ }))
163
+ const controllerOption = this.controllerAddress
164
+ ? [
165
+ {
166
+ value: this.controllerAddress,
167
+ label: this.controllerName
168
+ ? `${this.controllerName} (${formatted} ${this.symbol})`
169
+ : `UP Browser Extension (${formatted} ${this.symbol})`,
170
+ disabled: false,
171
+ },
172
+ ]
173
+ : []
174
+ const options = [...relayerOptions, ...controllerOption]
175
+
176
+ const selectedValue = this.selectedGasProvider
177
+ ? this.selectedGasProvider.type === 'controller'
178
+ ? this.controllerAddress
179
+ : (() => {
180
+ const r = this.allRelayers.find(
181
+ (x) => x.id === this.selectedGasProvider!.id
182
+ )
183
+ return (
184
+ r?.website ||
185
+ r?.apiUrl ||
186
+ `relayer:${this.selectedGasProvider!.id}`
187
+ )
188
+ })()
189
+ : undefined
190
+
191
+ return html`
192
+ <div class="w-full">
193
+ <custom-select .value=${selectedValue} .options=${options} @change=${this.onSelectChanged}></custom-select>
194
+ ${
195
+ this.selectedGasProvider
196
+ ? html`<div class="mt-2">
197
+ ${this.selectedGasProvider?.type === 'controller' ? this.renderControllerProgress() : this.renderRelayerProgress()}
198
+ ${
199
+ this.selectedGasProvider?.type === 'relayer'
200
+ ? html`<span class="text-[12px] text-neutral-60 mt-2 block">
201
+ Your total of ${this.relayerAvailable}M resets on ${this.displayRelayerResetDate()}.
202
+ </span>`
203
+ : html``
204
+ }
205
+ </div>`
206
+ : html``
207
+ }
208
+
209
+ </div>
210
+ `
211
+ }
212
+ }
213
+
214
+ declare global {
215
+ interface HTMLElementTagNameMap {
216
+ 'gas-provider-view': GasProviderView
217
+ }
218
+ }
@@ -0,0 +1,288 @@
1
+ import { Task } from '@lit/task'
2
+ import {
3
+ useAddressResolver,
4
+ useImageLoader,
5
+ } from '@lukso/transaction-view-headless'
6
+ import { html } from 'lit'
7
+ import { customElement, property, state } from 'lit/decorators.js'
8
+ import { CoreLitElement } from '../styles'
9
+ import { safeCustomElement } from './safe-custom-element'
10
+
11
+ // LUKSO web components are loaded dynamically via plugin
12
+
13
+ /**
14
+ * Address data interface for display
15
+ */
16
+ export interface AddressData {
17
+ address: string
18
+ name?: string
19
+ lsp4TokenName?: string
20
+ lsp4TokenSymbol?: string
21
+ baseAsset?: {
22
+ lsp4TokenName?: string
23
+ lsp4TokenSymbol?: string
24
+ }
25
+ profileImages?: any[]
26
+ avatar?: any[]
27
+ icon?: any[]
28
+ images?: any[]
29
+ __gqltype?: string
30
+ __identity?: string
31
+ identity?: string
32
+ }
33
+
34
+ /**
35
+ * Icon/Avatar display component for addresses
36
+ *
37
+ * Displays just the profile image/avatar for an address with proper styling.
38
+ * Supports both Universal Profiles and contract addresses.
39
+ *
40
+ * @example
41
+ * ```html
42
+ * <icon-view
43
+ * address="0x1234..."
44
+ * .resolved="${resolvedData}"
45
+ * size="medium"
46
+ * ></icon-view>
47
+ * ```
48
+ */
49
+ @safeCustomElement('icon-view')
50
+ export class IconView extends CoreLitElement {
51
+ /**
52
+ * The address to display (required)
53
+ */
54
+ @property() address!: string
55
+
56
+ /**
57
+ * Resolved address data (profile, images, etc.)
58
+ */
59
+ @property({ type: Object }) resolved?: AddressData
60
+
61
+ /**
62
+ * Display size
63
+ */
64
+ @property() size: 'x-small' | 'small' | 'medium' | 'large' | '2x-large' =
65
+ 'small'
66
+
67
+ /**
68
+ * Optional label to display above the icon
69
+ */
70
+ @property() label?: string
71
+
72
+ /**
73
+ * Whether to show the resolved display name below/beside the icon
74
+ */
75
+ @property({ type: Boolean, attribute: 'show-name' }) showName = true
76
+
77
+ /**
78
+ * Name placement relative to the icon: 'below' (stacked) or 'beside' (inline)
79
+ */
80
+ @property({ attribute: 'name-position' }) namePosition: 'below' | 'beside' =
81
+ 'below'
82
+
83
+ /**
84
+ * Maximum width for the name label (pixels)
85
+ */
86
+ @property({ type: Number, attribute: 'max-width' }) maxWidth = 200
87
+
88
+ /**
89
+ * Whether to show small identicon badge on the avatar
90
+ */
91
+ @property({ type: Boolean, attribute: 'show-icon-identicon' })
92
+ showIconIdenticon = true
93
+
94
+ /**
95
+ * Whether to show small identicon badge on the label
96
+ */
97
+ @property({ type: Boolean, attribute: 'show-label-identicon' })
98
+ showLabelIdenticon = false
99
+
100
+ /**
101
+ * Force unresolved display (for testing)
102
+ */
103
+ @property({ type: Boolean, attribute: 'force-unresolved' }) forceUnresolved =
104
+ false
105
+
106
+ /**
107
+ * Property name to use for loading images (e.g., 'images', 'icons', 'profileImages')
108
+ * If not set, uses default image loading logic
109
+ */
110
+ @property({ type: String, attribute: 'image-hint' }) imageHint?: string
111
+
112
+ /**
113
+ * Index to use when loading from an image array (e.g., images[0])
114
+ * Only used when imageHint points to an array property
115
+ */
116
+ @property({ type: Number, attribute: 'image-index' }) imageIndex?: number
117
+
118
+ // Internal state
119
+ @state() private imageData?: { src: string }
120
+ @state() private isLoadingImage = false
121
+ @state() private isLoadingAddress = false
122
+
123
+ // Composables
124
+ private imageLoader = useImageLoader()
125
+ private addressResolver = useAddressResolver()
126
+
127
+ // Task for resolving address data
128
+ private addressTask = new Task(this, {
129
+ task: async ([address], { signal }) => {
130
+ if (!address) return null
131
+
132
+ this.isLoadingAddress = true
133
+ try {
134
+ // Pass the full compound address to resolver (e.g., "0x123...-0x456...")
135
+ const resolved = await this.addressResolver.resolveAddress(address)
136
+ this.requestUpdate() // Force re-render after resolution
137
+ return resolved
138
+ } catch (error) {
139
+ console.warn('❌ IconView: Failed to resolve address:', error)
140
+ return null
141
+ } finally {
142
+ this.isLoadingAddress = false
143
+ }
144
+ },
145
+ args: () => [this.address],
146
+ })
147
+
148
+ // Task for loading profile image
149
+ private imageTask = new Task(this, {
150
+ task: async ([resolved], { signal }) => {
151
+ if (!resolved) return null
152
+
153
+ this.isLoadingImage = true
154
+ try {
155
+ let image
156
+
157
+ // If imageHint is specified, load directly from that property
158
+ if (this.imageHint && (resolved as any)[this.imageHint]) {
159
+ const imageArray = (resolved as any)[this.imageHint]
160
+ if (Array.isArray(imageArray) && imageArray.length > 0) {
161
+ image = await this.imageLoader.loadImage(imageArray, {
162
+ width: this.getSizePixels(),
163
+ height: this.getSizePixels(),
164
+ dpr: window.devicePixelRatio || 1,
165
+ index: this.imageIndex,
166
+ })
167
+ }
168
+ } else {
169
+ // Use default profile image loading logic
170
+ image = await this.imageLoader.loadProfileImage(resolved, {
171
+ width: this.getSizePixels(),
172
+ height: this.getSizePixels(),
173
+ dpr: window.devicePixelRatio || 1,
174
+ index: this.imageIndex,
175
+ })
176
+ }
177
+
178
+ return image || this.imageLoader.getFallbackImage(resolved)
179
+ } catch (error) {
180
+ console.warn('Failed to load address image:', error)
181
+ return this.imageLoader.getFallbackImage(resolved || {})
182
+ } finally {
183
+ this.isLoadingImage = false
184
+ }
185
+ },
186
+ args: () => [this.addressTask.value],
187
+ })
188
+
189
+ /**
190
+ * Get pixel size for the current size setting
191
+ */
192
+ private getSizePixels(): number {
193
+ switch (this.size) {
194
+ case 'x-small':
195
+ return 16
196
+ case 'small':
197
+ return 24
198
+ case 'medium':
199
+ return 32
200
+ case 'large':
201
+ return 48
202
+ case '2x-large':
203
+ return 124
204
+ default:
205
+ return 24
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Check if this is a contract address (shows square icon)
211
+ */
212
+ private get isContract(): boolean {
213
+ const resolved = this.addressTask.value
214
+ return resolved ? this.imageLoader.isContractAddress(resolved) : false
215
+ }
216
+
217
+ /**
218
+ * Handle icon click
219
+ */
220
+ private handleClick() {
221
+ this.dispatchEvent(
222
+ new CustomEvent('address-click', {
223
+ detail: {
224
+ address: this.address,
225
+ resolved: this.addressTask.value,
226
+ },
227
+ bubbles: true,
228
+ })
229
+ )
230
+ }
231
+
232
+ override render() {
233
+ const imageResult = this.imageTask.value
234
+
235
+ const containerClass = this.namePosition === 'beside' ? 'row' : 'column'
236
+ return html`
237
+ <div class="${
238
+ containerClass === 'row'
239
+ ? 'flex items-start gap-1'
240
+ : 'flex flex-col items-center gap-1'
241
+ }">
242
+ ${
243
+ this.label
244
+ ? html`<div class="text-[12px] text-neutral-60 font-medium">${
245
+ this.label
246
+ }</div>`
247
+ : ''
248
+ }
249
+
250
+ <div class="${
251
+ this.namePosition === 'beside'
252
+ ? 'inline-flex flex-row items-center gap-2'
253
+ : 'inline-flex flex-col items-center gap-1.5'
254
+ }">
255
+ <lukso-profile
256
+ size=${this.size}
257
+ profile-address=${this.address}
258
+ profile-url=${imageResult?.src || ''}
259
+ ?has-identicon=${this.showIconIdenticon}
260
+ ?is-square=${this.isContract}
261
+ class="inline-flex items-center cursor-pointer transition-transform duration-200 hover:scale-105"
262
+ @click=${this.handleClick}
263
+ ></lukso-profile>
264
+
265
+ ${
266
+ this.showName
267
+ ? html`
268
+ <address-view
269
+ class="leading-none"
270
+ address=${this.address}
271
+ .showIdenticon=${this.showLabelIdenticon}
272
+ size=${this.size}
273
+ max-width=${this.maxWidth}
274
+ ></address-view>
275
+ `
276
+ : ''
277
+ }
278
+ </div>
279
+ </div>
280
+ `
281
+ }
282
+ }
283
+
284
+ declare global {
285
+ interface HTMLElementTagNameMap {
286
+ 'icon-view': IconView
287
+ }
288
+ }