@meddleware/walrus-storage-ui 0.1.64 → 0.1.66

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.
@@ -1,21 +1,25 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, onMounted, ref, watch } from 'vue'
3
- import { CopyableAddress, ExplorerLink } from '@meddleware/ui'
2
+ // The connected wallet's Walrus blobs, one row per blob id, with Extend (add storage) and Certify (finish an upload from its stored
3
+ // certificate). This component owns the data and runs the transactions; the rows are `BlobGroupRows`, the amount control `ExtendControl`.
4
+ import { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue'
5
+ import { UiButton, UiDataTable, UiEmptyState, UiLiveStatus, UiNotice, UiPanel } from '@meddleware/ui'
6
+ import { errorMessage } from '@meddleware/web-utils'
7
+ import type { Transaction } from '@mysten/sui/transactions'
4
8
  import type { OwnedBlob } from '@meddleware/walrus-storage-client'
5
- import { MAX_SINGLE_RESERVATION_EPOCHS, formatCoinAmount } from '@meddleware/walrus-storage-kit'
6
- import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
7
- import type { Executor } from '../wallet.js'
8
- import { walrusNetwork, walruscanBlobUrl } from '../config.js'
9
- import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
10
- import { useExtendQuotes } from '../composables/useExtendQuotes.js'
11
- import { groupBlobs, expiryLabel, maxExtendableEpochs, type BlobGroup } from '../blob-groups.js'
9
+ import { MAX_SINGLE_RESERVATION_EPOCHS } from '@meddleware/walrus-storage-kit'
12
10
  import {
13
- pendingCertifyKey,
14
- loadPendingCertifies,
11
+ browserStorage,
15
12
  clearPendingCertify,
13
+ loadPendingCertifies,
14
+ pendingCertifyKey,
16
15
  type PendingCertify,
17
- browserStorage,
18
16
  } from '@meddleware/walrus-storage-client/flow'
17
+ import type { Executor } from '../wallet.js'
18
+ import { walrusNetwork } from '../config.js'
19
+ import { walrusClient, walrusLib } from '../walrus.js'
20
+ import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
21
+ import { groupBlobs } from '../blob-groups.js'
22
+ import BlobGroupRows from './BlobGroupRows.vue'
19
23
 
20
24
  // Browser storage that never throws (blocked site data falls back to memory for this page).
21
25
  const storage = browserStorage()
@@ -41,50 +45,25 @@ const { blobs, currentEpoch, loading, error, load } = useOwnedBlobs()
41
45
 
42
46
  // One row per blobId; re-uploads of the same content are collapsed into a group's `copies`.
43
47
  const groups = computed(() => groupBlobs(blobs.value))
44
- const expanded = ref<Set<string>>(new Set())
48
+ const expanded = shallowRef<ReadonlySet<string>>(new Set())
45
49
 
46
50
  // Pending certifications (uploaded-but-not-certified copies we still hold a certificate for).
47
- const pending = ref<Record<string, PendingCertify>>({})
51
+ const pending = shallowRef<Record<string, PendingCertify>>({})
48
52
 
49
53
  // Per-object action state (keyed by Blob objectId), shared by Extend and Certify.
50
54
  const busy = ref<string | null>(null)
51
- const actionStatus = ref<Record<string, string>>({})
52
- const extendAmount = ref<Record<string, number>>({})
53
-
54
- const MAX = MAX_SINGLE_RESERVATION_EPOCHS
55
-
56
- function msg(e: unknown): string {
57
- return e instanceof Error ? e.message : String(e)
58
- }
59
-
60
- function maxAddable(endEpoch: number): number {
61
- return maxExtendableEpochs(endEpoch, currentEpoch.value, MAX)
62
- }
63
-
64
- /** The stored certificate for an uncertified copy, if we can certify it here. */
65
- function pendingFor(objectId: string): PendingCertify | null {
66
- return pending.value[objectId] ?? null
67
- }
68
- /** The first pending (certifiable) copy in a group, if any. */
69
- function groupPendingCopy(g: BlobGroup): OwnedBlob | null {
70
- return g.copies.find((c) => !c.certified && pending.value[c.objectId]) ?? null
71
- }
72
- /** Status shown in the Certified column for a group. */
73
- function groupCertStatus(g: BlobGroup): 'certified' | 'pending' | 'none' {
74
- if (g.anyCertified) return 'certified'
75
- return groupPendingCopy(g) ? 'pending' : 'none'
55
+ const statuses = shallowRef<Record<string, string>>({})
56
+ const say = (objectId: string, text: string): void => {
57
+ statuses.value = { ...statuses.value, [objectId]: text }
76
58
  }
77
59
 
78
60
  function toggleExpand(blobId: string): void {
79
61
  const next = new Set(expanded.value)
80
- if (next.has(blobId)) next.delete(blobId)
81
- else next.add(blobId)
62
+ if (!next.delete(blobId)) next.add(blobId)
82
63
  expanded.value = next
83
64
  }
84
65
 
85
- function refresh(): Promise<void> {
86
- return load(props.address, { force: true })
87
- }
66
+ const refresh = (): Promise<void> => load(props.address, { force: true })
88
67
 
89
68
  /** Reload the pending map from storage, dropping entries whose blob is already certified. */
90
69
  function refreshPending(): void {
@@ -103,110 +82,70 @@ function refreshPending(): void {
103
82
  pending.value = map
104
83
  }
105
84
 
106
- // ── Extend ──────────────────────────────────────────────────────────────────
107
85
  // Extend adds storage only (no one-time write cost), so the quote prices the `storageCost` component.
108
86
  async function estimateExtendCost(size: number, epochs: number): Promise<bigint | null> {
109
- const { createWalrusClient, estimateStorageCost } = await import('@meddleware/walrus-storage-client')
110
- const walrusClient = createWalrusClient({ network: currentNetwork(), wasmUrl: walrusWasmUrl })
111
- return (await estimateStorageCost(walrusClient, size, epochs)).storageCost
112
- }
113
- const extendQuotes = useExtendQuotes(estimateExtendCost)
114
-
115
- // User picked an amount → clamp and (re)price it. Only the picked amount is priced here, not every
116
- // row on load, to avoid spinning up a Walrus client per blob; a row that was never touched is priced
117
- // when Extend is first pressed (see `extendBlob`), before the wallet opens.
118
- function setExtendAmount(blob: OwnedBlob, value: number | string): void {
119
- const max = maxAddable(blob.endEpoch)
120
- const v = Math.min(max, Math.max(1, Math.floor(Number(value) || 1)))
121
- extendAmount.value = { ...extendAmount.value, [blob.objectId]: v }
122
- void extendQuotes.price(blob.objectId, blob.size, v)
87
+ const [{ estimateStorageCost }, client] = await Promise.all([walrusLib(), walrusClient(currentNetwork())])
88
+ return (await estimateStorageCost(client, size, epochs)).storageCost
123
89
  }
124
90
 
125
- /** What the row shows beside the Extend button for its current amount. */
126
- function extendQuoteLabel(blob: OwnedBlob): string {
127
- const epochs = extendAmount.value[blob.objectId]
128
- const q = extendQuotes.quoteFor(blob.objectId, epochs)
129
- if (q?.state === 'ready') return `≈${formatCoinAmount(q.costFrost, 'WAL')} for +${q.epochs} epochs`
130
- if (q?.state === 'unavailable') return 'price unavailable'
131
- return extendQuotes.pricing.value[blob.objectId] === epochs ? 'pricing…' : ''
132
- }
133
-
134
- async function extendBlob(blob: OwnedBlob): Promise<void> {
135
- const epochs = extendAmount.value[blob.objectId]
136
- if (!epochs || busy.value) return
137
- // The wallet opens only for an amount whose cost the user has been shown. The seeded default has
138
- // no quote yet: price it now, show it, and let the user press Extend again to confirm.
139
- if (!(await extendQuotes.shownBeforeSigning(blob.objectId, blob.size, epochs))) return
91
+ /** Build, sign and run one transaction for a blob, reporting each stage on its row and refreshing the list after. */
92
+ async function act(blob: OwnedBlob, done: string, build: () => Promise<Transaction>, after?: () => void): Promise<void> {
140
93
  busy.value = blob.objectId
141
- actionStatus.value = { ...actionStatus.value, [blob.objectId]: 'Building transaction…' }
94
+ say(blob.objectId, 'Building transaction…')
142
95
  try {
143
- const { createWalrusClient, extendBlobLifetimeTransaction } = await import('@meddleware/walrus-storage-client')
144
- const walrusClient = createWalrusClient({ network: currentNetwork(), wasmUrl: walrusWasmUrl })
145
- const tx = await extendBlobLifetimeTransaction(walrusClient, blob.objectId, { epochs })
96
+ const tx = await build()
146
97
  const executor = await props.buildExecutor()
147
- actionStatus.value = { ...actionStatus.value, [blob.objectId]: 'Approve in wallet…' }
98
+ say(blob.objectId, 'Approve in wallet…')
148
99
  const { digest } = await executor.signAndExecute(tx)
149
100
  await executor.waitForTransaction(digest)
150
- extendQuotes.clear(blob.objectId)
151
- actionStatus.value = { ...actionStatus.value, [blob.objectId]: `Extended ✓ (${digest.slice(0, 8)}…)` }
101
+ after?.()
102
+ say(blob.objectId, `${done} (${digest.slice(0, 8)}…)`)
152
103
  await refresh()
153
104
  } catch (e) {
154
- actionStatus.value = { ...actionStatus.value, [blob.objectId]: `Failed: ${msg(e)}` }
105
+ say(blob.objectId, `Failed: ${errorMessage(e)}`)
155
106
  } finally {
156
107
  busy.value = null
157
108
  }
158
109
  }
159
110
 
160
- // ── Certify (resume a pending upload from its stored certificate) ─────────────
161
- async function certifyBlob(blob: OwnedBlob): Promise<void> {
162
- const entry = pendingFor(blob.objectId)
163
- if (!entry || !props.address || busy.value) return
164
- busy.value = blob.objectId
165
- actionStatus.value = { ...actionStatus.value, [blob.objectId]: 'Building transaction…' }
166
- try {
167
- const { createWalrusClient, certifyBlobTransaction } = await import('@meddleware/walrus-storage-client')
168
- const walrusClient = createWalrusClient({ network: currentNetwork(), wasmUrl: walrusWasmUrl })
169
- const tx = certifyBlobTransaction(walrusClient, {
170
- blobId: entry.blobId,
171
- blobObjectId: entry.blobObjectId,
172
- certificate: entry.certificate,
173
- deletable: entry.deletable,
174
- })
175
- const executor = await props.buildExecutor()
176
- actionStatus.value = { ...actionStatus.value, [blob.objectId]: 'Approve in wallet…' }
177
- const { digest } = await executor.signAndExecute(tx)
178
- await executor.waitForTransaction(digest)
179
- clearPendingCertify(storage, pendingCertifyKey(currentNetwork(), props.address), blob.objectId)
180
- actionStatus.value = { ...actionStatus.value, [blob.objectId]: `Certified ✓ (${digest.slice(0, 8)}…)` }
181
- await refresh()
182
- } catch (e) {
183
- actionStatus.value = { ...actionStatus.value, [blob.objectId]: `Failed: ${msg(e)}` }
184
- } finally {
185
- busy.value = null
186
- }
187
- }
188
-
189
- // Load on first open and whenever the address changes; re-derive pending on list/address changes.
190
- onMounted(() => void load(props.address))
191
- watch([() => props.address, walrusNetwork], ([addr]) => void load(addr))
192
- watch(blobs, () => refreshPending())
193
- watch([() => props.address, walrusNetwork], () => refreshPending())
194
-
195
- // Seed each group's extend amount with a sensible default (+10, clamped) without pricing it — the
196
- // estimate is fetched on the first user interaction.
197
- watch(
198
- [groups, currentEpoch],
199
- () => {
200
- for (const g of groups.value) {
201
- const b = g.representative
202
- const max = maxAddable(b.endEpoch)
203
- if (max > 0 && extendAmount.value[b.objectId] === undefined) {
204
- extendAmount.value = { ...extendAmount.value, [b.objectId]: Math.min(10, max) }
205
- }
206
- }
207
- },
208
- { immediate: true },
209
- )
111
+ function extendBlob(blob: OwnedBlob, epochs: number): Promise<void> {
112
+ if (busy.value) return Promise.resolve()
113
+ return act(blob, 'Extended', async () => {
114
+ const [{ extendBlobLifetimeTransaction }, client] = await Promise.all([walrusLib(), walrusClient(currentNetwork())])
115
+ return extendBlobLifetimeTransaction(client, blob.objectId, { epochs })
116
+ })
117
+ }
118
+
119
+ function certifyBlob(blob: OwnedBlob): Promise<void> {
120
+ const entry = pending.value[blob.objectId]
121
+ const address = props.address
122
+ if (!entry || !address || busy.value) return Promise.resolve()
123
+ return act(
124
+ blob,
125
+ 'Certified',
126
+ async () => {
127
+ const [{ certifyBlobTransaction }, client] = await Promise.all([walrusLib(), walrusClient(currentNetwork())])
128
+ return certifyBlobTransaction(client, {
129
+ blobId: entry.blobId,
130
+ blobObjectId: entry.blobObjectId,
131
+ certificate: entry.certificate,
132
+ deletable: entry.deletable,
133
+ })
134
+ },
135
+ () => clearPendingCertify(storage, pendingCertifyKey(currentNetwork(), address), blob.objectId),
136
+ )
137
+ }
138
+
139
+ // Load on first open and whenever the address or network changes; re-derive pending on list/address changes.
140
+ onMounted(() => {
141
+ void load(props.address)
142
+ refreshPending() // the cached list may already be there (a re-mount), so it will not change
143
+ })
144
+ watch([() => props.address, walrusNetwork], ([addr]) => {
145
+ void load(addr)
146
+ refreshPending()
147
+ })
148
+ watch(blobs, refreshPending)
210
149
 
211
150
  // Highlight + expand the group routed from the duplicate-upload dialog.
212
151
  watch(
@@ -222,221 +161,45 @@ watch(
222
161
  </script>
223
162
 
224
163
  <template>
225
- <section class="my-blobs">
226
- <div class="toolbar">
227
- <h2>My Blobs</h2>
228
- <button type="button" :disabled="!address || loading" @click="refresh">
229
- {{ loading ? 'Loading…' : 'Refresh' }}
230
- </button>
231
- </div>
232
-
233
- <p v-if="!address" class="hint">Connect your wallet to list your Walrus blobs.</p>
234
- <p v-else-if="error" class="err">{{ error }}</p>
235
- <p v-else-if="!loading && groups.length === 0" class="hint">No Walrus blobs found for this address.</p>
236
-
237
- <table v-if="groups.length" class="blob-table">
238
- <thead>
239
- <tr>
240
- <th>Blob ID</th>
241
- <th>Size</th>
242
- <th>Expires</th>
243
- <th>Certified</th>
244
- <th>Actions</th>
245
- </tr>
246
- </thead>
247
- <tbody>
248
- <template v-for="g in groups" :key="g.blobId">
249
- <tr
250
- :id="`blob-row-${g.blobId}`"
251
- :class="{ warn: g.maxEndEpoch - currentEpoch < 10, highlight: g.blobId === highlightBlobId }"
252
- >
253
- <td>
254
- <CopyableAddress :address="g.blobId" label="Copy blob ID">
255
- <ExplorerLink :href="walruscanBlobUrl(walrusNetwork ?? 'testnet', g.blobId)" :value="g.blobId" :chars="[8, 6]" />
256
- </CopyableAddress>
257
- <button
258
- v-if="g.copies.length > 1"
259
- type="button"
260
- class="copies-toggle"
261
- @click="toggleExpand(g.blobId)"
262
- >
263
- {{ expanded.has(g.blobId) ? '▾' : '▸' }} {{ g.copies.length }} copies
264
- </button>
265
- </td>
266
- <td>{{ (g.size / 1024).toFixed(1) }} KB</td>
267
- <td>{{ expiryLabel(g.maxEndEpoch, currentEpoch) }}</td>
268
- <td>
269
- <span v-if="groupCertStatus(g) === 'certified'">✓</span>
270
- <span v-else-if="groupCertStatus(g) === 'pending'" class="pending-badge" title="Uploaded but not certified">pending</span>
271
- <span v-else>—</span>
272
- </td>
273
- <td class="actions">
274
- <span v-if="actionStatus[g.representative.objectId]" class="act-status">
275
- {{ actionStatus[g.representative.objectId] }}
276
- </span>
277
- <template v-else>
278
- <button
279
- v-if="groupPendingCopy(g)"
280
- type="button"
281
- class="certify-btn"
282
- :disabled="!!busy"
283
- @click="certifyBlob(groupPendingCopy(g)!)"
284
- >
285
- Certify
286
- </button>
287
- <span v-if="maxAddable(g.representative.endEpoch) === 0" class="at-max">at max lifetime</span>
288
- <span v-else class="extend">
289
- <input
290
- type="number"
291
- min="1"
292
- :max="maxAddable(g.representative.endEpoch)"
293
- :value="extendAmount[g.representative.objectId]"
294
- aria-label="Epochs to add"
295
- @input="setExtendAmount(g.representative, ($event.target as HTMLInputElement).value)"
296
- />
297
- <button type="button" class="preset" @click="setExtendAmount(g.representative, 10)">+10</button>
298
- <button type="button" class="preset" @click="setExtendAmount(g.representative, 25)">+25</button>
299
- <button
300
- type="button"
301
- class="preset"
302
- @click="setExtendAmount(g.representative, maxAddable(g.representative.endEpoch))"
303
- >
304
- Max
305
- </button>
306
- <button type="button" :disabled="!!busy" @click="extendBlob(g.representative)">
307
- {{ extendQuotes.quoteFor(g.representative.objectId, extendAmount[g.representative.objectId]) ? 'Extend' : 'Show cost' }}
308
- </button>
309
- <span class="est" aria-live="polite">{{ extendQuoteLabel(g.representative) }}</span>
310
- </span>
311
- </template>
312
- </td>
313
- </tr>
314
-
315
- <!-- Per-copy detail for grouped duplicates: certify a specific pending copy. -->
316
- <tr v-for="c in (expanded.has(g.blobId) ? g.copies : [])" :key="c.objectId" class="copy-row">
317
- <td class="copy-obj">
318
- <CopyableAddress :address="c.objectId" label="Copy object ID">
319
- <span class="mono">{{ c.objectId.slice(0, 10) }}…</span>
320
- </CopyableAddress>
321
- </td>
322
- <td></td>
323
- <td>{{ expiryLabel(c.endEpoch, currentEpoch) }}</td>
324
- <td>
325
- <span v-if="c.certified">✓</span>
326
- <span v-else-if="pendingFor(c.objectId)" class="pending-badge">pending</span>
327
- <span v-else>—</span>
328
- </td>
329
- <td class="actions">
330
- <span v-if="actionStatus[c.objectId]" class="act-status">{{ actionStatus[c.objectId] }}</span>
331
- <button
332
- v-else-if="pendingFor(c.objectId)"
333
- type="button"
334
- class="certify-btn"
335
- :disabled="!!busy"
336
- @click="certifyBlob(c)"
337
- >
338
- Certify
339
- </button>
340
- </td>
341
- </tr>
342
- </template>
343
- </tbody>
344
- </table>
345
- </section>
164
+ <UiPanel title="My Blobs" :level="3">
165
+ <p class="bar">
166
+ <UiButton size="sm" variant="secondary" :disabled="!address" :busy="loading" @click="refresh">Refresh</UiButton>
167
+ </p>
168
+ <UiLiveStatus label="Blobs" :message="loading ? 'Loading your blobs…' : ''" />
169
+
170
+ <UiEmptyState v-if="!address">Connect your wallet to list your Walrus blobs.</UiEmptyState>
171
+ <UiNotice v-else-if="error" type="error" :message="error" />
172
+ <UiDataTable v-else-if="walrusNetwork && (groups.length || !loading)" caption="Your Walrus blobs" empty="No Walrus blobs found for this address." :rows="groups.length">
173
+ <template #head>
174
+ <th scope="col">Blob ID</th>
175
+ <th scope="col">Size</th>
176
+ <th scope="col">Expires</th>
177
+ <th scope="col">Certified</th>
178
+ <th scope="col">Actions</th>
179
+ </template>
180
+ <BlobGroupRows
181
+ v-for="g in groups"
182
+ :key="g.blobId"
183
+ :group="g"
184
+ :network="walrusNetwork"
185
+ :current-epoch="currentEpoch"
186
+ :max-reservation="MAX_SINGLE_RESERVATION_EPOCHS"
187
+ :highlighted="g.blobId === highlightBlobId"
188
+ :expanded="expanded.has(g.blobId)"
189
+ :pending="pending"
190
+ :busy="busy"
191
+ :statuses="statuses"
192
+ :estimate="estimateExtendCost"
193
+ @toggle="toggleExpand(g.blobId)"
194
+ @extend="extendBlob"
195
+ @certify="certifyBlob"
196
+ />
197
+ </UiDataTable>
198
+ </UiPanel>
346
199
  </template>
347
200
 
348
201
  <style scoped>
349
- .my-blobs {
350
- margin-top: 1.5rem;
351
- }
352
- .toolbar {
353
- display: flex;
354
- align-items: center;
355
- gap: 1rem;
356
- margin-bottom: 0.75rem;
357
- }
358
- .toolbar h2 {
359
- margin: 0;
360
- font-size: 1.2rem;
361
- }
362
- .hint {
363
- color: var(--muted);
364
- font-size: 0.9rem;
365
- }
366
- .err {
367
- color: var(--danger);
368
- font-size: 0.9rem;
369
- }
370
- .blob-table {
371
- width: 100%;
372
- border-collapse: collapse;
373
- font-size: 0.88rem;
374
- }
375
- .blob-table th,
376
- .blob-table td {
377
- text-align: left;
378
- padding: 0.4rem 0.6rem;
379
- border-bottom: 1px solid var(--border);
380
- vertical-align: top;
381
- }
382
- .blob-table th {
383
- font-weight: 600;
384
- color: var(--muted);
385
- }
386
- .blob-table tr.warn td {
387
- background: color-mix(in srgb, var(--warning) 8%, transparent);
388
- }
389
- .blob-table tr.highlight td {
390
- background: color-mix(in srgb, var(--accent) 14%, transparent);
391
- }
392
- .copy-row td {
393
- background: color-mix(in srgb, var(--muted) 6%, transparent);
394
- font-size: 0.82rem;
395
- }
396
- .copies-toggle {
397
- display: inline-block;
398
- margin-top: 0.25rem;
399
- font-size: 0.78rem;
400
- padding: 0.05rem 0.4rem;
401
- }
402
- .mono {
403
- font-family: monospace;
404
- }
405
- .actions {
406
- white-space: nowrap;
407
- }
408
- .extend {
409
- display: inline-flex;
410
- align-items: center;
411
- gap: 0.3rem;
412
- flex-wrap: wrap;
413
- }
414
- .extend input {
415
- width: 4rem;
416
- }
417
- .preset {
418
- font-size: 0.75rem;
419
- padding: 0.1rem 0.4rem;
420
- }
421
- .est {
422
- font-size: 0.78rem;
423
- color: var(--muted);
424
- }
425
- .at-max {
426
- font-size: 0.8rem;
427
- color: var(--muted);
428
- }
429
- .act-status {
430
- font-size: 0.85rem;
431
- color: var(--muted);
432
- }
433
- .certify-btn {
434
- border-color: var(--accent);
435
- color: var(--accent);
436
- }
437
- .pending-badge {
438
- font-size: 0.78rem;
439
- color: var(--accent);
440
- font-weight: 600;
202
+ .bar {
203
+ margin: 0 0 var(--space-xs);
441
204
  }
442
205
  </style>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ // What an upload produced: the blob id and its aggregator URL, and the certify transaction when there was one.
3
+ import { CopyableAddress, ExplorerLink, UiCard, UiStatGrid, UiStatRow } from '@meddleware/ui'
4
+ import { safeHref, suiExplorerUrl, walruscanBlobUrl } from '@meddleware/web-utils'
5
+ import type { WalrusNetwork } from '@meddleware/walrus-storage-kit'
6
+ import type { UploadResult } from '../composables/useStorageUpload.js'
7
+
8
+ defineProps<{ result: UploadResult; network: WalrusNetwork }>()
9
+ </script>
10
+
11
+ <template>
12
+ <UiCard title="Uploaded" :level="3">
13
+ <UiStatGrid>
14
+ <UiStatRow label="Blob ID" align="left">
15
+ <CopyableAddress :address="result.blobId" label="Copy blob ID">
16
+ <ExplorerLink :href="walruscanBlobUrl(network, result.blobId)" :value="result.blobId" :chars="[8, 6]" />
17
+ </CopyableAddress>
18
+ </UiStatRow>
19
+ <UiStatRow label="URL" align="left">
20
+ <a class="url" :href="safeHref(result.url)" target="_blank" rel="noopener noreferrer">{{ result.url }}</a>
21
+ </UiStatRow>
22
+ <UiStatRow v-if="result.digest" label="Certify tx" align="left">
23
+ <CopyableAddress :address="result.digest" label="Copy transaction digest">
24
+ <ExplorerLink :href="suiExplorerUrl('txblock', result.digest, network)" :value="result.digest" :chars="[8, 6]" />
25
+ </CopyableAddress>
26
+ </UiStatRow>
27
+ </UiStatGrid>
28
+ </UiCard>
29
+ </template>
30
+
31
+ <style scoped>
32
+ .url {
33
+ overflow-wrap: anywhere;
34
+ }
35
+ </style>