@meddleware/walrus-storage-ui 0.1.65 → 0.1.67
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/CHANGELOG.md +14 -0
- package/package.json +25 -13
- package/src/App.vue +20 -41
- package/src/components/BlobGroupRows.vue +133 -0
- package/src/components/ExtendControl.vue +102 -0
- package/src/components/MyBlobs.vue +108 -345
- package/src/components/UploadResultCard.vue +35 -0
- package/src/components/WalrusStorageView.vue +94 -310
- package/src/composables/useExtendQuotes.ts +3 -3
- package/src/composables/useOwnedBlobs.ts +8 -9
- package/src/composables/useStorageUpload.ts +111 -0
- package/src/config.ts +3 -16
- package/src/main.ts +3 -1
- package/src/wallet.ts +5 -44
- package/src/walrus.ts +29 -0
- package/vite.config.ts +13 -7
|
@@ -4,37 +4,16 @@
|
|
|
4
4
|
// from the shared @meddleware/wallet-adapter singleton (via ./wallet.js), so connecting here or
|
|
5
5
|
// in any other inline tool view reflects everywhere.
|
|
6
6
|
import { computed, ref, watch } from 'vue'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
WalrusUpload,
|
|
10
|
-
AccessGateCta,
|
|
11
|
-
useAccessGate,
|
|
12
|
-
type WalrusNetwork,
|
|
13
|
-
} from '@meddleware/walrus-storage-kit'
|
|
14
|
-
import { AppTabNav, CopyableAddress, ExplorerLink, UiNotice, UiTabPanel, UiToolIntro, suiExplorerUrl, safeHref, type AppTab } from '@meddleware/ui'
|
|
15
|
-
// Lightweight URL import — just the wasm asset URL (does not pull the walrus client).
|
|
16
|
-
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
7
|
+
import { AppTabNav, UiButton, UiLiveStatus, UiNotice, UiTabPanel, UiToolIntro, type AppTab } from '@meddleware/ui'
|
|
17
8
|
import { WalletGuard } from '@meddleware/wallet-adapter'
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
createGatedAccess,
|
|
23
|
-
consumeStorageKey,
|
|
24
|
-
pendingCertifyKey,
|
|
25
|
-
savePendingCertify,
|
|
26
|
-
clearPendingCertify,
|
|
27
|
-
loadPendingCertifies,
|
|
28
|
-
type BlobUploadResult as UploadResult,
|
|
29
|
-
type ExistingCopy,
|
|
30
|
-
type UploadProgress,
|
|
31
|
-
browserStorage,
|
|
32
|
-
} from '@meddleware/walrus-storage-client/flow'
|
|
33
|
-
|
|
34
|
-
// Browser storage that never throws (blocked site data falls back to memory for this page).
|
|
35
|
-
const storage = browserStorage()
|
|
9
|
+
import { AccessGateCta, WalrusUpload, useAccessGate, type WalrusNetwork } from '@meddleware/walrus-storage-kit'
|
|
10
|
+
import type { ExistingCopy } from '@meddleware/walrus-storage-client/flow'
|
|
11
|
+
import { buildExecutor, getSuiClient, useWallet } from '../wallet.js'
|
|
12
|
+
import { accessGate, network, relayHosts, walrusNetwork } from '../config.js'
|
|
36
13
|
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
14
|
+
import { useStorageUpload, type UploadResult } from '../composables/useStorageUpload.js'
|
|
37
15
|
import MyBlobs from './MyBlobs.vue'
|
|
16
|
+
import UploadResultCard from './UploadResultCard.vue'
|
|
38
17
|
|
|
39
18
|
const TABS: AppTab[] = [
|
|
40
19
|
{ id: 'upload', label: 'Upload' },
|
|
@@ -42,29 +21,33 @@ const TABS: AppTab[] = [
|
|
|
42
21
|
]
|
|
43
22
|
const activeTab = ref<string>('upload')
|
|
44
23
|
|
|
45
|
-
const { account
|
|
24
|
+
const { account } = useWallet()
|
|
46
25
|
|
|
47
|
-
// One access-gate state per Walrus network (the gate is a per-network setting)
|
|
48
|
-
//
|
|
49
|
-
// `
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
26
|
+
// One access-gate state per Walrus network (the gate is a per-network setting), created when its network is first the active one and
|
|
27
|
+
// inert until an ownership check runs. `useAccessGate` throws for a gate that is not the published deployment on its network;
|
|
28
|
+
// `accessGate()` only returns such configs (or null, ungated). The shared client serves the active network only, so a state for
|
|
29
|
+
// another network refuses to read rather than reading the wrong chain.
|
|
30
|
+
const gateStates: Partial<Record<WalrusNetwork, ReturnType<typeof useAccessGate>>> = {}
|
|
31
|
+
function gateFor(net: WalrusNetwork) {
|
|
32
|
+
return (gateStates[net] ??= useAccessGate({
|
|
33
|
+
network: net,
|
|
34
|
+
gate: accessGate(net),
|
|
35
|
+
getClient: () => {
|
|
36
|
+
if (net !== network.value) throw new Error(`No Sui client for ${net} while ${network.value} is active.`)
|
|
37
|
+
return getSuiClient()
|
|
38
|
+
},
|
|
39
|
+
}))
|
|
59
40
|
}
|
|
60
41
|
/** The active network's gate state (testnet's while the view is hidden on a non-Walrus network). */
|
|
61
|
-
const gateState = computed(() =>
|
|
42
|
+
const gateState = computed(() => gateFor(walrusNetwork.value ?? 'testnet'))
|
|
62
43
|
const purchasing = ref(false)
|
|
63
44
|
const result = ref<UploadResult | null>(null)
|
|
45
|
+
/** A gate is configured and the ownership check has not decided yet. */
|
|
46
|
+
const checkingAccess = computed(() => gateState.value.gateConfigured && gateState.value.hasAccess.value === null)
|
|
47
|
+
const needsPass = computed(() => gateState.value.gateConfigured && gateState.value.hasAccess.value === false)
|
|
64
48
|
|
|
65
|
-
// Check gate ownership whenever the connected account or the network changes. A switched or
|
|
66
|
-
//
|
|
67
|
-
// any in-flight check.
|
|
49
|
+
// Check gate ownership whenever the connected account or the network changes. A switched or disconnected wallet never inherits the
|
|
50
|
+
// previous wallet's access state: `reset()` also discards any in-flight check.
|
|
68
51
|
watch(
|
|
69
52
|
[() => account.value?.address ?? null, walrusNetwork],
|
|
70
53
|
([addr], previous) => {
|
|
@@ -81,8 +64,7 @@ async function onPurchase(): Promise<void> {
|
|
|
81
64
|
if (!account.value) return
|
|
82
65
|
purchasing.value = true
|
|
83
66
|
try {
|
|
84
|
-
|
|
85
|
-
await gateState.value.purchase(executor, account.value.address)
|
|
67
|
+
await gateState.value.purchase(await buildExecutor(), account.value.address)
|
|
86
68
|
} catch {
|
|
87
69
|
// The reason is published in `purchaseError` and shown by the prompt.
|
|
88
70
|
} finally {
|
|
@@ -90,117 +72,8 @@ async function onPurchase(): Promise<void> {
|
|
|
90
72
|
}
|
|
91
73
|
}
|
|
92
74
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// and, for a gated relay, `createGatedAccess` keeps the single-use consume resumable: its digest is
|
|
96
|
-
// persisted before the upload, reused after an interruption, cleared once the upload lands, and
|
|
97
|
-
// replaced only after the gateway reports it redeemed.
|
|
98
|
-
async function performUpload(
|
|
99
|
-
bytes: Uint8Array,
|
|
100
|
-
opts: {
|
|
101
|
-
relayHost: string
|
|
102
|
-
epochs: number
|
|
103
|
-
force?: boolean
|
|
104
|
-
deletable?: boolean
|
|
105
|
-
onStatus: (s: string | UploadProgress) => void
|
|
106
|
-
},
|
|
107
|
-
): Promise<UploadResult> {
|
|
108
|
-
if (!account.value) throw new Error('Connect your wallet first.')
|
|
109
|
-
const net = walrusNetwork.value
|
|
110
|
-
if (!net) throw new Error(`Walrus storage is not available on ${network.value}.`)
|
|
111
|
-
const executor = await buildExecutor()
|
|
112
|
-
const address = account.value.address
|
|
113
|
-
const state = gateState.value
|
|
114
|
-
const gate = state.gate
|
|
115
|
-
const nftId = state.nftId.value
|
|
116
|
-
|
|
117
|
-
const access =
|
|
118
|
-
gate && state.hasAccess.value === true && nftId
|
|
119
|
-
? createGatedAccess({
|
|
120
|
-
storage,
|
|
121
|
-
key: consumeStorageKey(net, gate.gateId, address),
|
|
122
|
-
relayHost: opts.relayHost,
|
|
123
|
-
address,
|
|
124
|
-
// Signed into every access proof with the relay's origin: a proof made here works nowhere else.
|
|
125
|
-
gateId: normalizeSuiAddress(gate.gateId),
|
|
126
|
-
network: net,
|
|
127
|
-
nftId,
|
|
128
|
-
singleUse: state.singleUse.value,
|
|
129
|
-
buildConsume: (id, nonce) => state.buildConsume(id, nonce),
|
|
130
|
-
signAndExecute: (tx) => executor.signAndExecute(tx),
|
|
131
|
-
waitForTransaction: (digest) => executor.waitForTransaction(digest),
|
|
132
|
-
sign: signPersonalMessage,
|
|
133
|
-
onStatus: opts.onStatus,
|
|
134
|
-
})
|
|
135
|
-
: undefined
|
|
136
|
-
|
|
137
|
-
return runBlobUpload({
|
|
138
|
-
bytes,
|
|
139
|
-
network: net,
|
|
140
|
-
relayHost: opts.relayHost,
|
|
141
|
-
address,
|
|
142
|
-
wasmUrl: walrusWasmUrl,
|
|
143
|
-
maxTipMist: uploadRelayMaxTipMist(),
|
|
144
|
-
epochs: opts.epochs,
|
|
145
|
-
deletable: opts.deletable,
|
|
146
|
-
force: opts.force,
|
|
147
|
-
findExistingCopy,
|
|
148
|
-
executor,
|
|
149
|
-
suiClient: getSuiClient(),
|
|
150
|
-
access,
|
|
151
|
-
onStatus: opts.onStatus,
|
|
152
|
-
// Persist the certificate the moment the upload lands, and drop it once certified — so a
|
|
153
|
-
// dismissed certify can be finished from My Blobs after a tab switch or reload.
|
|
154
|
-
onUploaded: (info) => savePendingCertify(storage, pendingCertifyKey(net, address), info),
|
|
155
|
-
onCertified: (blobObjectId) => clearPendingCertify(storage, pendingCertifyKey(net, address), blobObjectId),
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Precheck (before paying to register): does the wallet already own this exact blob? Returns a
|
|
160
|
-
// `certified` match (offer Extend) or a `pending` one — uncertified but with a saved certificate
|
|
161
|
-
// (offer Certify). Best-effort: any failure returns null so the upload simply proceeds.
|
|
162
|
-
async function findExistingCopy(blobId: string): Promise<ExistingCopy | null> {
|
|
163
|
-
const address = account.value?.address
|
|
164
|
-
const net = walrusNetwork.value
|
|
165
|
-
if (!address || !net) return null
|
|
166
|
-
try {
|
|
167
|
-
const { createWalrusClient, fetchOwnedWalrusBlobs } = await import('@meddleware/walrus-storage-client')
|
|
168
|
-
const walrusClient = createWalrusClient({ network: net, wasmUrl: walrusWasmUrl })
|
|
169
|
-
const [sys, owned] = await Promise.all([
|
|
170
|
-
walrusClient.walrus.systemState(),
|
|
171
|
-
fetchOwnedWalrusBlobs(getSuiClient(), walrusClient, address),
|
|
172
|
-
])
|
|
173
|
-
const currentEpoch = Number(sys.committee.epoch)
|
|
174
|
-
const copies = owned.filter((b) => b.blobId === blobId && b.endEpoch > currentEpoch)
|
|
175
|
-
const certified = copies.find((b) => b.certified)
|
|
176
|
-
if (certified) {
|
|
177
|
-
return { kind: 'certified', blobId, objectId: certified.objectId, endEpoch: certified.endEpoch }
|
|
178
|
-
}
|
|
179
|
-
const store = loadPendingCertifies(storage, pendingCertifyKey(net, address))
|
|
180
|
-
const pending = copies.find((b) => !b.certified && b.objectId in store)
|
|
181
|
-
if (pending) {
|
|
182
|
-
return { kind: 'pending', blobId, objectId: pending.objectId, endEpoch: pending.endEpoch }
|
|
183
|
-
}
|
|
184
|
-
return null
|
|
185
|
-
} catch {
|
|
186
|
-
return null
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Injected into the upload widget so it can price a chosen reservation duration (storage is WAL,
|
|
191
|
-
// billed per size × epochs). Returns total cost in FROST, or null on error.
|
|
192
|
-
async function estimateUploadStorageCost(sizeBytes: number, epochs: number): Promise<bigint | null> {
|
|
193
|
-
const net = walrusNetwork.value
|
|
194
|
-
if (!net) return null
|
|
195
|
-
try {
|
|
196
|
-
const { createWalrusClient, estimateStorageCost } = await import('@meddleware/walrus-storage-client')
|
|
197
|
-
const walrusClient = createWalrusClient({ network: net, wasmUrl: walrusWasmUrl })
|
|
198
|
-
const cost = await estimateStorageCost(walrusClient, sizeBytes, epochs)
|
|
199
|
-
return cost.totalCost
|
|
200
|
-
} catch {
|
|
201
|
-
return null
|
|
202
|
-
}
|
|
203
|
-
}
|
|
75
|
+
const { performUpload, estimateStorageCost } = useStorageUpload(() => gateState.value)
|
|
76
|
+
const ownedBlobs = useOwnedBlobs()
|
|
204
77
|
|
|
205
78
|
// The user declined a duplicate upload and chose to manage the existing copy: jump to My Blobs and
|
|
206
79
|
// highlight it (the grouped row exposes Extend for certified, Certify for pending).
|
|
@@ -210,167 +83,78 @@ function onManageExisting(existing: ExistingCopy): void {
|
|
|
210
83
|
activeTab.value = 'blobs'
|
|
211
84
|
}
|
|
212
85
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
function onUploaded(r: UploadResult): void {
|
|
216
|
-
result.value = r
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Fires after every upload attempt (success or failure) — a failed UI run may still have landed
|
|
220
|
-
// on-chain, so force-refresh the owned-blobs cache and NFT ownership (uses remaining) in the
|
|
221
|
-
// background regardless of outcome.
|
|
86
|
+
// Fires after every upload attempt (success or failure): a failed run may still have landed on-chain, so force-refresh the
|
|
87
|
+
// owned-blobs cache and NFT ownership (uses remaining) in the background regardless of outcome.
|
|
222
88
|
function onSettled(): void {
|
|
223
|
-
if (account.value)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
89
|
+
if (!account.value) return
|
|
90
|
+
void ownedBlobs.load(account.value.address, { force: true })
|
|
91
|
+
if (gateState.value.gate) void gateState.value.checkOwnership(account.value.address)
|
|
227
92
|
}
|
|
228
93
|
</script>
|
|
229
94
|
|
|
230
95
|
<template>
|
|
96
|
+
<section aria-labelledby="walrus-title">
|
|
97
|
+
<h2 id="walrus-title">Walrus storage</h2>
|
|
231
98
|
<UiToolIntro>Upload and manage blobs on Walrus decentralised storage ({{ network }}).</UiToolIntro>
|
|
232
99
|
|
|
233
|
-
<UiNotice v-if="!walrusNetwork" type="info"
|
|
100
|
+
<UiNotice v-if="!walrusNetwork" type="info" :message="`Walrus storage is not available on ${network}. Switch to testnet or mainnet.`" />
|
|
234
101
|
<template v-else>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
v-if="gateState.termsError.value && !gateState.termsLoading.value"
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<WalrusUpload
|
|
287
|
-
:hosts="relayHosts(walrusNetwork)"
|
|
288
|
-
:connected="!!account"
|
|
289
|
-
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess, singleUse: gateState.singleUse }"
|
|
290
|
-
:session-key="`${walrusNetwork}:${account?.address ?? ''}`"
|
|
291
|
-
:perform-upload="performUpload"
|
|
292
|
-
:estimate-storage-cost="estimateUploadStorageCost"
|
|
293
|
-
@uploaded="onUploaded"
|
|
294
|
-
@settled="onSettled"
|
|
295
|
-
@manage-existing="onManageExisting"
|
|
296
|
-
/>
|
|
297
|
-
|
|
298
|
-
<section v-if="result" class="result">
|
|
299
|
-
<h2>Uploaded ✓</h2>
|
|
300
|
-
<p>
|
|
301
|
-
<strong>Blob ID:</strong>
|
|
302
|
-
<CopyableAddress :address="result.blobId" label="Copy blob ID">
|
|
303
|
-
<ExplorerLink
|
|
304
|
-
:href="walruscanBlobUrl(walrusNetwork, result.blobId)"
|
|
305
|
-
:value="result.blobId"
|
|
306
|
-
:chars="[8, 6]"
|
|
307
|
-
/>
|
|
308
|
-
</CopyableAddress>
|
|
309
|
-
</p>
|
|
310
|
-
<p>
|
|
311
|
-
<strong>URL:</strong>
|
|
312
|
-
<a :href="safeHref(result.url)" target="_blank" rel="noopener noreferrer">{{ result.url }}</a>
|
|
313
|
-
</p>
|
|
314
|
-
<p v-if="result.digest">
|
|
315
|
-
<strong>Certify tx:</strong>
|
|
316
|
-
<CopyableAddress :address="result.digest" label="Copy transaction digest">
|
|
317
|
-
<ExplorerLink
|
|
318
|
-
:href="suiExplorerUrl('txblock', result.digest, walrusNetwork)"
|
|
319
|
-
:value="result.digest"
|
|
320
|
-
:chars="[8, 6]"
|
|
321
|
-
/>
|
|
322
|
-
</CopyableAddress>
|
|
323
|
-
</p>
|
|
324
|
-
</section>
|
|
102
|
+
<AppTabNav v-model="activeTab" :tabs="TABS" id-prefix="walrus" aria-label="Feature tabs" />
|
|
103
|
+
|
|
104
|
+
<!-- The tab list and its panel always render (the selected tab controls a live panel); the wallet
|
|
105
|
+
prompt replaces only the panel's content until a wallet is connected. -->
|
|
106
|
+
<UiTabPanel id-prefix="walrus" :tab="activeTab">
|
|
107
|
+
<WalletGuard message="Connect a Sui wallet to upload and manage your blobs.">
|
|
108
|
+
<template v-if="activeTab === 'upload'">
|
|
109
|
+
<UiLiveStatus label="Access pass" :message="checkingAccess ? 'Checking access…' : ''" />
|
|
110
|
+
|
|
111
|
+
<!-- Gated + no access: replace the form with the purchase prompt, so the person is guided to buy first rather than
|
|
112
|
+
facing a disabled form. -->
|
|
113
|
+
<template v-if="needsPass">
|
|
114
|
+
<AccessGateCta
|
|
115
|
+
:gate-configured="gateState.gateConfigured"
|
|
116
|
+
:has-access="gateState.hasAccess.value"
|
|
117
|
+
:busy="purchasing"
|
|
118
|
+
:terms="gateState.terms.value"
|
|
119
|
+
:terms-error="gateState.termsError.value"
|
|
120
|
+
:purchase-error="gateState.purchaseError.value"
|
|
121
|
+
@purchase="onPurchase"
|
|
122
|
+
/>
|
|
123
|
+
<UiButton v-if="gateState.termsError.value && !gateState.termsLoading.value" variant="secondary" @click="gateState.loadTerms()">
|
|
124
|
+
Read the price again
|
|
125
|
+
</UiButton>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<!-- Access held (or ungated relay): the upload form. -->
|
|
129
|
+
<template v-else-if="!checkingAccess">
|
|
130
|
+
<!-- Gated relays spend a credit before the file is stored: say that it pays for an attempt, not a guarantee, and
|
|
131
|
+
that attempts resume. -->
|
|
132
|
+
<UiNotice v-if="gateState.gateConfigured" type="info">
|
|
133
|
+
Uploading spends <strong>one credit</strong> from your access NFT (an on-chain step) before the file is stored — it pays
|
|
134
|
+
for an upload <em>attempt</em>, not a guaranteed upload. Your attempt resumes automatically, even after a page reload if
|
|
135
|
+
you re-select the same file, so a credit is normally not lost. A credit is spent without a completed upload only if you
|
|
136
|
+
abandon the upload entirely, cancel a required wallet approval, or wait long enough that the reserved storage lapses.
|
|
137
|
+
</UiNotice>
|
|
138
|
+
|
|
139
|
+
<WalrusUpload
|
|
140
|
+
:hosts="relayHosts(walrusNetwork)"
|
|
141
|
+
:connected="!!account"
|
|
142
|
+
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess, singleUse: gateState.singleUse }"
|
|
143
|
+
:session-key="`${walrusNetwork}:${account?.address ?? ''}`"
|
|
144
|
+
:perform-upload="performUpload"
|
|
145
|
+
:estimate-storage-cost="estimateStorageCost"
|
|
146
|
+
@uploaded="result = $event"
|
|
147
|
+
@settled="onSettled"
|
|
148
|
+
@manage-existing="onManageExisting"
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
<UploadResultCard v-if="result" :result="result" :network="walrusNetwork" />
|
|
152
|
+
</template>
|
|
325
153
|
</template>
|
|
326
|
-
</template>
|
|
327
154
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
:build-executor="() => buildExecutor()"
|
|
332
|
-
:highlight-blob-id="highlightBlobId"
|
|
333
|
-
/>
|
|
334
|
-
</WalletGuard>
|
|
335
|
-
</UiTabPanel>
|
|
155
|
+
<MyBlobs v-else :address="account?.address ?? null" :build-executor="buildExecutor" :highlight-blob-id="highlightBlobId" />
|
|
156
|
+
</WalletGuard>
|
|
157
|
+
</UiTabPanel>
|
|
336
158
|
</template>
|
|
159
|
+
</section>
|
|
337
160
|
</template>
|
|
338
|
-
|
|
339
|
-
<style scoped>
|
|
340
|
-
.walrus-tabs {
|
|
341
|
-
margin: 1rem 0 0.5rem;
|
|
342
|
-
}
|
|
343
|
-
.result {
|
|
344
|
-
margin-top: 1.5rem;
|
|
345
|
-
padding: 1rem;
|
|
346
|
-
border: 1px solid var(--border);
|
|
347
|
-
border-radius: 10px;
|
|
348
|
-
word-break: break-all;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
.result code {
|
|
352
|
-
font-size: 0.85rem;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
.gate-card {
|
|
356
|
-
margin-top: 1rem;
|
|
357
|
-
padding: 1.5rem;
|
|
358
|
-
border: 1px solid var(--border);
|
|
359
|
-
border-radius: 12px;
|
|
360
|
-
background: var(--surface, transparent);
|
|
361
|
-
text-align: center;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
.checking {
|
|
365
|
-
margin-top: 1.5rem;
|
|
366
|
-
color: var(--muted);
|
|
367
|
-
text-align: center;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
.credit-notice {
|
|
371
|
-
margin: 1rem 0;
|
|
372
|
-
font-size: 0.85rem;
|
|
373
|
-
line-height: 1.5;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Prices an Extend before the wallet opens (audit F12). A row's quote is the WAL cost of adding
|
|
2
2
|
// `epochs`; Extend opens the wallet only for an amount whose quote the user has been shown. A quote is
|
|
3
3
|
// an estimate (labelled `≈`): the wallet and the chain still decide the amount actually paid.
|
|
4
|
-
import {
|
|
4
|
+
import { shallowRef } from 'vue'
|
|
5
5
|
|
|
6
6
|
export type ExtendQuote =
|
|
7
7
|
| { epochs: number; state: 'ready'; costFrost: bigint }
|
|
@@ -12,8 +12,8 @@ export type EstimateExtendCost = (size: number, epochs: number) => Promise<bigin
|
|
|
12
12
|
|
|
13
13
|
export function useExtendQuotes(estimate: EstimateExtendCost) {
|
|
14
14
|
/** Latest quote per Blob object id (one per row). */
|
|
15
|
-
const quotes =
|
|
16
|
-
const pricing =
|
|
15
|
+
const quotes = shallowRef<Record<string, ExtendQuote>>({})
|
|
16
|
+
const pricing = shallowRef<Record<string, number>>({})
|
|
17
17
|
/** Per-row request counter: an older answer never replaces a newer amount's quote. */
|
|
18
18
|
const tokens: Record<string, number> = {}
|
|
19
19
|
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
// `load()` is a no-op when the list is already loaded for the given address on the active network,
|
|
7
7
|
// unless `force` is set
|
|
8
8
|
// (used after an upload/extend, which can create or mutate on-chain blobs).
|
|
9
|
-
import { ref } from 'vue'
|
|
9
|
+
import { ref, shallowRef } from 'vue'
|
|
10
10
|
import type { OwnedBlob } from '@meddleware/walrus-storage-client'
|
|
11
|
-
|
|
12
|
-
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
11
|
+
import { errorMessage } from '@meddleware/web-utils'
|
|
13
12
|
import { getSuiClient } from '../wallet.js'
|
|
14
13
|
import { walrusNetwork } from '../config.js'
|
|
14
|
+
import { walrusClient, walrusLib } from '../walrus.js'
|
|
15
15
|
|
|
16
|
-
const blobs =
|
|
16
|
+
const blobs = shallowRef<OwnedBlob[]>([])
|
|
17
17
|
const currentEpoch = ref(0)
|
|
18
18
|
const loading = ref(false)
|
|
19
19
|
const error = ref<string | null>(null)
|
|
@@ -58,15 +58,14 @@ async function load(address: string | null, opts: { force?: boolean } = {}): Pro
|
|
|
58
58
|
loading.value = true
|
|
59
59
|
error.value = null
|
|
60
60
|
try {
|
|
61
|
-
const {
|
|
61
|
+
const [{ fetchOwnedWalrusBlobs }, client] = await Promise.all([walrusLib(), walrusClient(net)])
|
|
62
62
|
const suiClient = getSuiClient()
|
|
63
|
-
const walrusClient = createWalrusClient({ network: net, wasmUrl: walrusWasmUrl })
|
|
64
63
|
// Blob `endEpoch` is a WALRUS epoch, so compare against the Walrus committee epoch — NOT the
|
|
65
64
|
// Sui system-state epoch (they are different clocks; the Sui epoch, ~1218 vs ~570, made every
|
|
66
65
|
// blob read as "expired").
|
|
67
66
|
const [sys, fetched] = await Promise.all([
|
|
68
|
-
|
|
69
|
-
fetchOwnedWalrusBlobs(suiClient,
|
|
67
|
+
client.walrus.systemState(),
|
|
68
|
+
fetchOwnedWalrusBlobs(suiClient, client, address),
|
|
70
69
|
])
|
|
71
70
|
// A newer load (or a switch) took over while this one was in flight: its answer is for another
|
|
72
71
|
// account or network, so drop it without touching the list, the error or `loading`.
|
|
@@ -76,7 +75,7 @@ async function load(address: string | null, opts: { force?: boolean } = {}): Pro
|
|
|
76
75
|
loadedFor.value = key
|
|
77
76
|
} catch (e) {
|
|
78
77
|
if (mine !== latest) return
|
|
79
|
-
error.value =
|
|
78
|
+
error.value = errorMessage(e)
|
|
80
79
|
} finally {
|
|
81
80
|
if (mine === latest) {
|
|
82
81
|
loading.value = false
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// The Upload tab's orchestration: wires the shared `WalrusUpload` widget to walrus-storage-client's upload flow and the wallet.
|
|
2
|
+
// `runBlobUpload` registers fresh on every attempt (the relay rejects an old register transaction); for a gated relay `useGatedUpload`
|
|
3
|
+
// keeps the single-use consume resumable (its digest is persisted before the upload, reused after an interruption, cleared once the
|
|
4
|
+
// upload lands, and replaced only after the gateway reports it redeemed).
|
|
5
|
+
import { useGatedUpload, type AccessGateState } from '@meddleware/walrus-storage-kit'
|
|
6
|
+
import {
|
|
7
|
+
browserStorage,
|
|
8
|
+
clearPendingCertify,
|
|
9
|
+
loadPendingCertifies,
|
|
10
|
+
pendingCertifyKey,
|
|
11
|
+
runBlobUpload,
|
|
12
|
+
savePendingCertify,
|
|
13
|
+
type BlobUploadResult,
|
|
14
|
+
type ExistingCopy,
|
|
15
|
+
type UploadProgress,
|
|
16
|
+
} from '@meddleware/walrus-storage-client/flow'
|
|
17
|
+
import { network, relayHosts, uploadRelayMaxTipMist, walrusNetwork } from '../config.js'
|
|
18
|
+
import { buildExecutor, getSuiClient, useWallet } from '../wallet.js'
|
|
19
|
+
import { walrusClient, walrusLib, walrusWasmUrl } from '../walrus.js'
|
|
20
|
+
|
|
21
|
+
export type UploadResult = BlobUploadResult
|
|
22
|
+
|
|
23
|
+
export interface UploadOptions {
|
|
24
|
+
relayHost: string
|
|
25
|
+
epochs: number
|
|
26
|
+
force?: boolean
|
|
27
|
+
deletable?: boolean
|
|
28
|
+
onStatus: (s: string | UploadProgress) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `gate` is the active network's access-gate state (a getter: it changes with the network). */
|
|
32
|
+
export function useStorageUpload(gate: () => AccessGateState) {
|
|
33
|
+
// Browser storage that never throws (blocked site data falls back to memory for this page).
|
|
34
|
+
const storage = browserStorage()
|
|
35
|
+
const { account, signPersonalMessage } = useWallet()
|
|
36
|
+
const { accessFor } = useGatedUpload(gate)
|
|
37
|
+
|
|
38
|
+
// Precheck (before paying to register): does the wallet already own this exact blob? Returns a `certified` match (offer Extend) or a
|
|
39
|
+
// `pending` one: uncertified but with a saved certificate (offer Certify). Best-effort: any failure returns null so the upload proceeds.
|
|
40
|
+
async function findExistingCopy(blobId: string): Promise<ExistingCopy | null> {
|
|
41
|
+
const address = account.value?.address
|
|
42
|
+
const net = walrusNetwork.value
|
|
43
|
+
if (!address || !net) return null
|
|
44
|
+
try {
|
|
45
|
+
const [{ fetchOwnedWalrusBlobs }, client] = await Promise.all([walrusLib(), walrusClient(net)])
|
|
46
|
+
const [sys, owned] = await Promise.all([client.walrus.systemState(), fetchOwnedWalrusBlobs(getSuiClient(), client, address)])
|
|
47
|
+
const currentEpoch = Number(sys.committee.epoch)
|
|
48
|
+
const copies = owned.filter((b) => b.blobId === blobId && b.endEpoch > currentEpoch)
|
|
49
|
+
const certified = copies.find((b) => b.certified)
|
|
50
|
+
if (certified) return { kind: 'certified', blobId, objectId: certified.objectId, endEpoch: certified.endEpoch }
|
|
51
|
+
const store = loadPendingCertifies(storage, pendingCertifyKey(net, address))
|
|
52
|
+
const pending = copies.find((b) => !b.certified && b.objectId in store)
|
|
53
|
+
return pending ? { kind: 'pending', blobId, objectId: pending.objectId, endEpoch: pending.endEpoch } : null
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function performUpload(bytes: Uint8Array, opts: UploadOptions): Promise<UploadResult> {
|
|
60
|
+
if (!account.value) throw new Error('Connect your wallet first.')
|
|
61
|
+
const net = walrusNetwork.value
|
|
62
|
+
if (!net) throw new Error(`Walrus storage is not available on ${network.value}.`)
|
|
63
|
+
const executor = await buildExecutor()
|
|
64
|
+
const address = account.value.address
|
|
65
|
+
const access = accessFor({
|
|
66
|
+
relayHost: opts.relayHost,
|
|
67
|
+
publicHost: relayHosts(net).public,
|
|
68
|
+
address,
|
|
69
|
+
network: net,
|
|
70
|
+
executor,
|
|
71
|
+
sign: signPersonalMessage,
|
|
72
|
+
onStatus: opts.onStatus,
|
|
73
|
+
storage,
|
|
74
|
+
})
|
|
75
|
+
return runBlobUpload({
|
|
76
|
+
bytes,
|
|
77
|
+
network: net,
|
|
78
|
+
relayHost: opts.relayHost,
|
|
79
|
+
address,
|
|
80
|
+
wasmUrl: walrusWasmUrl,
|
|
81
|
+
maxTipMist: uploadRelayMaxTipMist(),
|
|
82
|
+
epochs: opts.epochs,
|
|
83
|
+
deletable: opts.deletable,
|
|
84
|
+
force: opts.force,
|
|
85
|
+
findExistingCopy,
|
|
86
|
+
executor,
|
|
87
|
+
suiClient: getSuiClient(),
|
|
88
|
+
access,
|
|
89
|
+
onStatus: opts.onStatus,
|
|
90
|
+
// Persist the certificate the moment the upload lands, and drop it once certified, so a dismissed certify can be finished
|
|
91
|
+
// from My Blobs after a tab switch or reload.
|
|
92
|
+
onUploaded: (info) => savePendingCertify(storage, pendingCertifyKey(net, address), info),
|
|
93
|
+
onCertified: (blobObjectId) => clearPendingCertify(storage, pendingCertifyKey(net, address), blobObjectId),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Injected into the upload widget so it can price a chosen reservation duration (storage is WAL, billed per size × epochs).
|
|
98
|
+
// Returns the total cost in FROST, or null when it cannot be priced.
|
|
99
|
+
async function estimateStorageCost(sizeBytes: number, epochs: number): Promise<bigint | null> {
|
|
100
|
+
const net = walrusNetwork.value
|
|
101
|
+
if (!net) return null
|
|
102
|
+
try {
|
|
103
|
+
const [{ estimateStorageCost: estimate }, client] = await Promise.all([walrusLib(), walrusClient(net)])
|
|
104
|
+
return (await estimate(client, sizeBytes, epochs)).totalCost
|
|
105
|
+
} catch {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { performUpload, estimateStorageCost }
|
|
111
|
+
}
|