@meddleware/walrus-ui 0.1.18 → 0.1.19
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/package.json +3 -3
- package/src/components/MyBlobs.vue +11 -2
- package/src/components/WalrusView.vue +154 -73
- package/src/composables/useOwnedBlobs.ts +5 -2
- package/src/config.ts +8 -0
- package/src/upload-flow.ts +58 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/walrus-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Standalone Vue 3 SPA for uploading blobs to Walrus decentralised storage and managing owned blobs — wallet-connected, NFT-gated relay support.",
|
|
5
5
|
"author": "Meddleware <dev@meddleware.co.uk>",
|
|
6
6
|
"license": "0BSD",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@meddleware/design-tokens": "^0.1.2",
|
|
44
44
|
"@meddleware/nft-gate-client": "^0.0.6",
|
|
45
|
-
"@meddleware/ui": "^0.1.
|
|
45
|
+
"@meddleware/ui": "^0.1.10",
|
|
46
46
|
"@meddleware/wallet-adapter": "^0.0.5",
|
|
47
|
-
"@meddleware/walrus-client": "^0.0.
|
|
47
|
+
"@meddleware/walrus-client": "^0.0.8",
|
|
48
48
|
"@meddleware/walrus-relay": "^0.1.8",
|
|
49
49
|
"@mysten/sui": "^2.30.0",
|
|
50
50
|
"@mysten/wallet-standard": "^0.20.0",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { CopyableAddress, ExplorerLink } from '@meddleware/ui'
|
|
3
4
|
import type { OwnedBlob } from '@meddleware/walrus-client'
|
|
4
5
|
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
5
6
|
import type { Executor } from '../wallet.js'
|
|
6
|
-
import { NETWORK } from '../config.js'
|
|
7
|
+
import { NETWORK, walruscanBlobUrl } from '../config.js'
|
|
7
8
|
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
8
9
|
|
|
9
10
|
const props = defineProps<{
|
|
@@ -92,7 +93,15 @@ watch(() => props.address, (addr) => void load(addr))
|
|
|
92
93
|
:key="blob.objectId"
|
|
93
94
|
:class="{ warn: blob.endEpoch - currentEpoch < EXPIRY_WARN_EPOCHS }"
|
|
94
95
|
>
|
|
95
|
-
<td
|
|
96
|
+
<td>
|
|
97
|
+
<CopyableAddress :address="blob.blobId" label="Copy blob ID">
|
|
98
|
+
<ExplorerLink
|
|
99
|
+
:href="walruscanBlobUrl(NETWORK, blob.blobId)"
|
|
100
|
+
:value="blob.blobId"
|
|
101
|
+
:chars="[8, 6]"
|
|
102
|
+
/>
|
|
103
|
+
</CopyableAddress>
|
|
104
|
+
</td>
|
|
96
105
|
<td>{{ (blob.size / 1024).toFixed(1) }} KB</td>
|
|
97
106
|
<td>
|
|
98
107
|
epoch {{ blob.endEpoch }}
|
|
@@ -11,13 +11,14 @@ import {
|
|
|
11
11
|
MAX_SINGLE_RESERVATION_EPOCHS,
|
|
12
12
|
} from '@meddleware/walrus-relay'
|
|
13
13
|
import type { UploadResult } from '@meddleware/walrus-relay'
|
|
14
|
+
import { CopyableAddress, ExplorerLink, suiExplorerUrl } from '@meddleware/ui'
|
|
14
15
|
// Lightweight URL import — just the wasm asset URL (does not pull the walrus client).
|
|
15
16
|
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
16
17
|
import { WalletGuard } from '@meddleware/wallet-adapter'
|
|
17
18
|
import { useWallet, getSuiClient } from '../wallet.js'
|
|
18
19
|
import { fetchChallenge, buildAccessProof } from '@meddleware/nft-gate-client'
|
|
19
|
-
import { NETWORK, relayHosts, accessGate, uploadRelayMaxTipMist } from '../config.js'
|
|
20
|
-
import { runBlobUpload } from '../upload-flow.js'
|
|
20
|
+
import { NETWORK, relayHosts, accessGate, uploadRelayMaxTipMist, walruscanBlobUrl } from '../config.js'
|
|
21
|
+
import { runBlobUpload, type UploadResumeState } from '../upload-flow.js'
|
|
21
22
|
import { consumeStorageKey, isRedeemedConflict, resolveGatedAuthToken } from '../access-resume.js'
|
|
22
23
|
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
23
24
|
import MyBlobs from './MyBlobs.vue'
|
|
@@ -51,15 +52,30 @@ async function onPurchase(): Promise<void> {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
// The relay auth token for the current attempt. The walrus client reads it PER REQUEST (a provider),
|
|
56
|
+
// so a retained flow resumed after a failure presents a fresh challenge signature. Undefined ⇒
|
|
57
|
+
// ungated (no header).
|
|
58
|
+
const authTokenRef = ref<string | undefined>(undefined)
|
|
59
|
+
// A same-session registered blob retained after a failed upload, keyed by file content, so a retry
|
|
60
|
+
// resumes from the relay upload instead of re-registering (no new WAL/gas). Cleared on success or
|
|
61
|
+
// when a resumed attempt itself fails (fall back to a fresh full upload).
|
|
62
|
+
const uploadSession = ref<{ key: string; state: UploadResumeState } | null>(null)
|
|
63
|
+
|
|
64
|
+
/** Cheap content key (length + head/tail bytes) to match a retry to the same selected file. */
|
|
65
|
+
function contentKey(bytes: Uint8Array): string {
|
|
66
|
+
const head = Array.from(bytes.slice(0, 16)).join(',')
|
|
67
|
+
const tail = Array.from(bytes.slice(-16)).join(',')
|
|
68
|
+
return `${bytes.length}:${head}:${tail}`
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
// Wire the shared WalrusUpload widget to the extracted upload orchestration + the wallet. The
|
|
55
|
-
// register/upload/certify sequence lives in src/upload-flow.ts (unit-tested); this closure
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// redeemed (a prior upload actually landed), we clear and consume a fresh use once.
|
|
72
|
+
// register/upload/certify sequence lives in src/upload-flow.ts (unit-tested); this closure gathers
|
|
73
|
+
// the wallet-bound inputs and manages two resume layers so an interrupted upload wastes nothing:
|
|
74
|
+
// 1. Single-use consume: the relay treats the permanent on-chain `consumeDigest` as the one-time
|
|
75
|
+
// redemption token, so a use is only spent when an upload succeeds. The digest is persisted and
|
|
76
|
+
// reused across retries/reload (re-signing a fresh challenge is free); cleared on success.
|
|
77
|
+
// 2. Walrus flow (same session): a registered-but-not-uploaded blob is retained and reused on a
|
|
78
|
+
// retry of the same file, skipping re-encode/re-register (no new WAL/gas).
|
|
63
79
|
async function performUpload(
|
|
64
80
|
bytes: Uint8Array,
|
|
65
81
|
opts: { relayHost: string; onStatus: (s: string) => void },
|
|
@@ -67,21 +83,23 @@ async function performUpload(
|
|
|
67
83
|
if (!account.value) throw new Error('Connect your wallet first.')
|
|
68
84
|
const executor = await buildExecutor()
|
|
69
85
|
const address = account.value.address
|
|
86
|
+
const key = contentKey(bytes)
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return runBlobUpload(uploadDeps(bytes, opts, executor, undefined))
|
|
74
|
-
}
|
|
88
|
+
const gated = !!(gate && gateState.hasAccess.value === true && gateState.nftId.value)
|
|
89
|
+
const consumeKey = gate ? consumeStorageKey(NETWORK, gate.gateId, address) : null
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
// Resolve this attempt's relay token (gated only) and stash it where the client reads it.
|
|
92
|
+
async function setToken(forceFresh: boolean): Promise<void> {
|
|
93
|
+
if (!gated) {
|
|
94
|
+
authTokenRef.value = undefined
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
authTokenRef.value = await resolveGatedAuthToken({
|
|
80
98
|
storage: window.localStorage,
|
|
81
|
-
key,
|
|
99
|
+
key: consumeKey as string,
|
|
82
100
|
relayHost: opts.relayHost,
|
|
83
101
|
address,
|
|
84
|
-
nftId,
|
|
102
|
+
nftId: gateState.nftId.value as string,
|
|
85
103
|
fetchChallenge,
|
|
86
104
|
buildConsume: (id, nonce) => gateState.buildConsume(id, nonce),
|
|
87
105
|
signAndExecute: (tx) => executor.signAndExecute(tx),
|
|
@@ -90,44 +108,59 @@ async function performUpload(
|
|
|
90
108
|
sign: signPersonalMessage,
|
|
91
109
|
forceFresh,
|
|
92
110
|
})
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const authToken = await resolve(false)
|
|
96
|
-
const result = await runBlobUpload(uploadDeps(bytes, opts, executor, authToken))
|
|
97
|
-
window.localStorage.removeItem(key) // upload succeeded → the use is now spent
|
|
98
|
-
return result
|
|
99
|
-
} catch (e) {
|
|
100
|
-
if (!isRedeemedConflict(e)) throw e // transient failure → keep the digest so the next try resumes
|
|
101
|
-
// The stored consume was already redeemed (a prior upload actually landed). Clear it and spend
|
|
102
|
-
// one fresh use for this new upload.
|
|
103
|
-
window.localStorage.removeItem(key)
|
|
104
|
-
const authToken = await resolve(true)
|
|
105
|
-
const result = await runBlobUpload(uploadDeps(bytes, opts, executor, authToken))
|
|
106
|
-
window.localStorage.removeItem(key)
|
|
107
|
-
return result
|
|
108
111
|
}
|
|
109
|
-
}
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
function uploadDeps(
|
|
113
|
-
bytes: Uint8Array,
|
|
114
|
-
opts: { relayHost: string; onStatus: (s: string) => void },
|
|
115
|
-
executor: Awaited<ReturnType<typeof buildExecutor>>,
|
|
116
|
-
authToken: string | undefined,
|
|
117
|
-
) {
|
|
118
|
-
if (!account.value) throw new Error('Connect your wallet first.')
|
|
119
|
-
return {
|
|
113
|
+
const deps = (resume?: UploadResumeState) => ({
|
|
120
114
|
bytes,
|
|
121
115
|
network: NETWORK,
|
|
122
116
|
relayHost: opts.relayHost,
|
|
123
|
-
address
|
|
117
|
+
address,
|
|
124
118
|
wasmUrl: walrusWasmUrl,
|
|
125
119
|
maxTipMist: uploadRelayMaxTipMist(),
|
|
126
120
|
epochs: MAX_SINGLE_RESERVATION_EPOCHS,
|
|
127
121
|
executor,
|
|
128
122
|
suiClient: getSuiClient(),
|
|
129
|
-
|
|
123
|
+
// Provider: resolved per request so a resumed flow uses the fresh token.
|
|
124
|
+
authToken: () => authTokenRef.value,
|
|
130
125
|
onStatus: opts.onStatus,
|
|
126
|
+
resume,
|
|
127
|
+
onRegistered: (state: UploadResumeState) => {
|
|
128
|
+
uploadSession.value = { key, state }
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const succeed = (r: UploadResult): UploadResult => {
|
|
133
|
+
uploadSession.value = null
|
|
134
|
+
if (consumeKey) window.localStorage.removeItem(consumeKey) // use spent only on success
|
|
135
|
+
return r
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Reuse a retained registration for this exact file, if any.
|
|
139
|
+
const resumeFor = () => (uploadSession.value?.key === key ? uploadSession.value.state : undefined)
|
|
140
|
+
const wasResuming = resumeFor() !== undefined
|
|
141
|
+
|
|
142
|
+
await setToken(false)
|
|
143
|
+
try {
|
|
144
|
+
return succeed(await runBlobUpload(deps(resumeFor())))
|
|
145
|
+
} catch (e) {
|
|
146
|
+
// A resumed attempt failed → drop the retained registration so the next try does a full fresh
|
|
147
|
+
// upload (re-register), guaranteeing behaviour no worse than a non-resumed run.
|
|
148
|
+
if (wasResuming) uploadSession.value = null
|
|
149
|
+
|
|
150
|
+
if (gated && isRedeemedConflict(e)) {
|
|
151
|
+
// Stored consume already redeemed (a prior upload actually landed): clear it, spend a fresh
|
|
152
|
+
// use, and retry — resuming the registered blob if we still hold it.
|
|
153
|
+
window.localStorage.removeItem(consumeKey as string)
|
|
154
|
+
await setToken(true)
|
|
155
|
+
const resume2 = resumeFor()
|
|
156
|
+
try {
|
|
157
|
+
return succeed(await runBlobUpload(deps(resume2)))
|
|
158
|
+
} catch (e2) {
|
|
159
|
+
if (resume2) uploadSession.value = null
|
|
160
|
+
throw e2
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw e // keep the retained registration so a manual retry resumes
|
|
131
164
|
}
|
|
132
165
|
}
|
|
133
166
|
|
|
@@ -173,32 +206,65 @@ function onSettled(): void {
|
|
|
173
206
|
|
|
174
207
|
<WalletGuard message="Connect a Sui wallet to upload and manage your blobs.">
|
|
175
208
|
<template v-if="activeTab === 'upload'">
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
<!-- Gated + no access: replace the form with the purchase CTA so the user is guided to buy
|
|
210
|
+
first, rather than facing a disabled form. -->
|
|
211
|
+
<div v-if="gateState.gateConfigured && gateState.hasAccess.value === false" class="gate-card">
|
|
212
|
+
<AccessGateCta
|
|
213
|
+
:gate-configured="gateState.gateConfigured"
|
|
214
|
+
:has-access="gateState.hasAccess.value"
|
|
215
|
+
:busy="purchasing"
|
|
216
|
+
:price-mist="gate?.priceMist ?? null"
|
|
217
|
+
@purchase="onPurchase"
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<!-- Ownership check still in flight. -->
|
|
222
|
+
<p
|
|
223
|
+
v-else-if="gateState.gateConfigured && gateState.hasAccess.value === null"
|
|
224
|
+
class="checking"
|
|
225
|
+
>
|
|
226
|
+
Checking access…
|
|
227
|
+
</p>
|
|
228
|
+
|
|
229
|
+
<!-- Access held (or ungated relay): show the upload form. -->
|
|
230
|
+
<template v-else>
|
|
231
|
+
<WalrusUpload
|
|
232
|
+
:hosts="relayHosts(NETWORK)"
|
|
233
|
+
:connected="!!account"
|
|
234
|
+
:access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess }"
|
|
235
|
+
:perform-upload="performUpload"
|
|
236
|
+
@uploaded="onUploaded"
|
|
237
|
+
@settled="onSettled"
|
|
238
|
+
/>
|
|
239
|
+
|
|
240
|
+
<section v-if="result" class="result">
|
|
241
|
+
<h2>Uploaded ✓</h2>
|
|
242
|
+
<p>
|
|
243
|
+
<strong>Blob ID:</strong>
|
|
244
|
+
<CopyableAddress :address="result.blobId" label="Copy blob ID">
|
|
245
|
+
<ExplorerLink
|
|
246
|
+
:href="walruscanBlobUrl(NETWORK, result.blobId)"
|
|
247
|
+
:value="result.blobId"
|
|
248
|
+
:chars="[8, 6]"
|
|
249
|
+
/>
|
|
250
|
+
</CopyableAddress>
|
|
251
|
+
</p>
|
|
252
|
+
<p>
|
|
253
|
+
<strong>URL:</strong>
|
|
254
|
+
<a :href="result.url" target="_blank" rel="noopener">{{ result.url }}</a>
|
|
255
|
+
</p>
|
|
256
|
+
<p v-if="result.digest">
|
|
257
|
+
<strong>Certify tx:</strong>
|
|
258
|
+
<CopyableAddress :address="result.digest" label="Copy transaction digest">
|
|
259
|
+
<ExplorerLink
|
|
260
|
+
:href="suiExplorerUrl('txblock', result.digest, NETWORK)"
|
|
261
|
+
:value="result.digest"
|
|
262
|
+
:chars="[8, 6]"
|
|
263
|
+
/>
|
|
264
|
+
</CopyableAddress>
|
|
265
|
+
</p>
|
|
266
|
+
</section>
|
|
267
|
+
</template>
|
|
202
268
|
</template>
|
|
203
269
|
|
|
204
270
|
<MyBlobs
|
|
@@ -235,6 +301,21 @@ function onSettled(): void {
|
|
|
235
301
|
font-size: 0.85rem;
|
|
236
302
|
}
|
|
237
303
|
|
|
304
|
+
.gate-card {
|
|
305
|
+
margin-top: 1rem;
|
|
306
|
+
padding: 1.5rem;
|
|
307
|
+
border: 1px solid var(--border);
|
|
308
|
+
border-radius: 12px;
|
|
309
|
+
background: var(--surface, transparent);
|
|
310
|
+
text-align: center;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.checking {
|
|
314
|
+
margin-top: 1.5rem;
|
|
315
|
+
color: var(--muted);
|
|
316
|
+
text-align: center;
|
|
317
|
+
}
|
|
318
|
+
|
|
238
319
|
.tabs {
|
|
239
320
|
display: flex;
|
|
240
321
|
gap: 0.25rem;
|
|
@@ -34,11 +34,14 @@ async function load(address: string | null, opts: { force?: boolean } = {}): Pro
|
|
|
34
34
|
const { createWalrusClient, fetchOwnedWalrusBlobs } = await import('@meddleware/walrus-client')
|
|
35
35
|
const suiClient = getSuiClient()
|
|
36
36
|
const walrusClient = createWalrusClient({ network: NETWORK, wasmUrl: walrusWasmUrl })
|
|
37
|
+
// Blob `endEpoch` is a WALRUS epoch, so compare against the Walrus committee epoch — NOT the
|
|
38
|
+
// Sui system-state epoch (they are different clocks; the Sui epoch, ~1218 vs ~570, made every
|
|
39
|
+
// blob read as "expired").
|
|
37
40
|
const [sys, fetched] = await Promise.all([
|
|
38
|
-
|
|
41
|
+
walrusClient.walrus.systemState(),
|
|
39
42
|
fetchOwnedWalrusBlobs(suiClient, walrusClient, address),
|
|
40
43
|
])
|
|
41
|
-
currentEpoch.value = Number(sys.
|
|
44
|
+
currentEpoch.value = Number(sys.committee.epoch)
|
|
42
45
|
blobs.value = fetched.sort((a: OwnedBlob, b: OwnedBlob) => a.endEpoch - b.endEpoch)
|
|
43
46
|
loadedFor.value = address
|
|
44
47
|
} catch (e) {
|
package/src/config.ts
CHANGED
|
@@ -34,6 +34,14 @@ export function relayHosts(network: WalrusNetwork): { operator: string; public:
|
|
|
34
34
|
return { operator: OPERATOR_RELAY_HOSTS[network], public: PUBLIC_WALRUS_RELAY_HOSTS[network] }
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Walruscan explorer URL for a blob. A Walrus blob id is not a Sui object, so it links to the
|
|
39
|
+
* Walrus-native explorer rather than a Sui explorer (SuiVision handles Sui entities elsewhere).
|
|
40
|
+
*/
|
|
41
|
+
export function walruscanBlobUrl(network: WalrusNetwork, blobId: string): string {
|
|
42
|
+
return `https://walruscan.com/${network}/blob/${blobId}`
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
/** Default relay tip ceiling (MIST) when `VITE_UPLOAD_RELAY_MAX_TIP_MIST` is unset (0.5 SUI). */
|
|
38
46
|
export const DEFAULT_UPLOAD_RELAY_MAX_TIP_MIST = 500_000_000
|
|
39
47
|
|
package/src/upload-flow.ts
CHANGED
|
@@ -24,7 +24,7 @@ interface BuiltTx {
|
|
|
24
24
|
build(opts: { client: unknown }): Promise<unknown>
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
interface BlobUploadFlow {
|
|
27
|
+
export interface BlobUploadFlow {
|
|
28
28
|
encode(): Promise<void>
|
|
29
29
|
register(opts: { owner: string; epochs: number; deletable: boolean }): BuiltTx
|
|
30
30
|
upload(opts: { digest: string }): Promise<void>
|
|
@@ -32,6 +32,17 @@ interface BlobUploadFlow {
|
|
|
32
32
|
getBlob(): Promise<{ blobId: string }>
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A same-session resume point: a flow whose blob is already registered on-chain, plus the register
|
|
37
|
+
* transaction digest. Passing this to {@link runBlobUpload} skips encode + register (no new WAL/gas)
|
|
38
|
+
* and retries from the relay upload — so an interrupted upload never re-registers. Not serialisable
|
|
39
|
+
* / not for cross-reload use (the flow holds the encoded blob in memory).
|
|
40
|
+
*/
|
|
41
|
+
export interface UploadResumeState {
|
|
42
|
+
flow: BlobUploadFlow
|
|
43
|
+
registerDigest: string
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export interface RunBlobUploadDeps {
|
|
36
47
|
bytes: Uint8Array
|
|
37
48
|
network: string
|
|
@@ -47,11 +58,24 @@ export interface RunBlobUploadDeps {
|
|
|
47
58
|
executor: UploadExecutor
|
|
48
59
|
/** A Sui client used to `build()` the register/certify transactions. */
|
|
49
60
|
suiClient: unknown
|
|
50
|
-
/**
|
|
51
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Bearer proof token for an NFT-gated relay. May be a provider resolved per request so a resumed
|
|
63
|
+
* upload presents a fresh challenge signature (see `@meddleware/walrus-client`).
|
|
64
|
+
*/
|
|
65
|
+
authToken?: string | (() => string | undefined)
|
|
52
66
|
onStatus: (s: string) => void
|
|
53
67
|
/** Lazy loader for the Walrus client module (keeps wasm out of the eager bundle). */
|
|
54
68
|
loadWalrusClient?: () => Promise<WalrusClientModule>
|
|
69
|
+
/**
|
|
70
|
+
* Resume a prior same-session upload from its registered blob (skips encode + register). Omit for
|
|
71
|
+
* a fresh upload.
|
|
72
|
+
*/
|
|
73
|
+
resume?: UploadResumeState
|
|
74
|
+
/**
|
|
75
|
+
* Called once the blob is registered (fresh uploads only), handing back the flow + register digest
|
|
76
|
+
* so the caller can retain them and resume the relay upload after a failure without re-registering.
|
|
77
|
+
*/
|
|
78
|
+
onRegistered?: (state: UploadResumeState) => void
|
|
55
79
|
}
|
|
56
80
|
|
|
57
81
|
/**
|
|
@@ -66,27 +90,41 @@ export async function runBlobUpload(deps: RunBlobUploadDeps): Promise<UploadResu
|
|
|
66
90
|
(async () => (await import('@meddleware/walrus-client')) as unknown as WalrusClientModule)
|
|
67
91
|
const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await load()
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
let flow: BlobUploadFlow
|
|
94
|
+
let registerDigest: string
|
|
95
|
+
|
|
96
|
+
if (deps.resume) {
|
|
97
|
+
// Same-session resume: the blob is already registered on-chain. Skip encode + register (no new
|
|
98
|
+
// WAL/gas) and retry from the relay upload. The retained flow keeps its registered state; its
|
|
99
|
+
// client resolves the relay token per request, so a fresh challenge is used on retry.
|
|
100
|
+
flow = deps.resume.flow
|
|
101
|
+
registerDigest = deps.resume.registerDigest
|
|
102
|
+
} else {
|
|
103
|
+
const client = createWalrusClient({
|
|
104
|
+
network: deps.network,
|
|
105
|
+
wasmUrl: deps.wasmUrl,
|
|
106
|
+
uploadRelayHost: deps.relayHost,
|
|
107
|
+
uploadRelayAuthToken: deps.authToken,
|
|
108
|
+
uploadRelayMaxTipMist: deps.maxTipMist,
|
|
109
|
+
})
|
|
110
|
+
flow = createBlobUploadFlow(client, deps.bytes)
|
|
77
111
|
|
|
78
|
-
|
|
79
|
-
|
|
112
|
+
deps.onStatus('Encoding…')
|
|
113
|
+
await flow.encode()
|
|
80
114
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
deps.onStatus('Registering blob (approve in wallet)…')
|
|
116
|
+
const regTx = flow.register({ owner: deps.address, epochs: deps.epochs, deletable: false })
|
|
117
|
+
regTx.setSenderIfNotSet(deps.address)
|
|
118
|
+
await regTx.build({ client: deps.suiClient })
|
|
119
|
+
const reg = await deps.executor.signAndExecute(regTx)
|
|
120
|
+
await deps.executor.waitForTransaction(reg.digest)
|
|
121
|
+
registerDigest = reg.digest
|
|
122
|
+
// Hand the registered flow back so the caller can resume the relay upload after a failure.
|
|
123
|
+
deps.onRegistered?.({ flow, registerDigest })
|
|
124
|
+
}
|
|
87
125
|
|
|
88
126
|
deps.onStatus('Uploading to the relay…')
|
|
89
|
-
await flow.upload({ digest:
|
|
127
|
+
await flow.upload({ digest: registerDigest })
|
|
90
128
|
|
|
91
129
|
deps.onStatus('Certifying (approve in wallet)…')
|
|
92
130
|
const certTx = flow.certify()
|