@meddleware/walrus-ui 0.1.19 → 0.1.20
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 +1 -1
- package/src/access-resume.ts +49 -0
- package/src/components/WalrusView.vue +91 -88
- package/src/upload-flow.ts +38 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/walrus-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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",
|
package/src/access-resume.ts
CHANGED
|
@@ -21,6 +21,55 @@ export function consumeStorageKey(network: string, gateId: string, address: stri
|
|
|
21
21
|
return `mw:walrus:consume:${network}:${gateId}:${address}`
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// ── Register-resume (Walrus flow) ───────────────────────────────────────────────────────────
|
|
25
|
+
// A blob registered on-chain but not yet uploaded/certified can be resumed WITHOUT re-registering
|
|
26
|
+
// (no new WAL/gas): persist its register transaction digest, keyed by a hash of the file content,
|
|
27
|
+
// and reuse it when the same file is uploaded again — even after a page reload, since the user
|
|
28
|
+
// re-selects the file (only the tiny digest is persisted, never the bytes).
|
|
29
|
+
|
|
30
|
+
/** Stable per-(network, address) key under which a pending register digest + content hash is stored. */
|
|
31
|
+
export function registerStorageKey(network: string, address: string): string {
|
|
32
|
+
return `mw:walrus:register:${network}:${address}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Cheap content fingerprint (length + head/tail bytes) to match a retry to the same file. */
|
|
36
|
+
export function contentKey(bytes: Uint8Array): string {
|
|
37
|
+
const head = Array.from(bytes.slice(0, 16)).join(',')
|
|
38
|
+
const tail = Array.from(bytes.slice(-16)).join(',')
|
|
39
|
+
return `${bytes.length}:${head}:${tail}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Return the stored register digest iff it was saved for this exact file content. */
|
|
43
|
+
export function loadRegisterResume(
|
|
44
|
+
storage: StorageLike,
|
|
45
|
+
key: string,
|
|
46
|
+
content: string,
|
|
47
|
+
): string | null {
|
|
48
|
+
const raw = storage.getItem(key)
|
|
49
|
+
if (!raw) return null
|
|
50
|
+
try {
|
|
51
|
+
const v = JSON.parse(raw) as { contentKey?: string; registerDigest?: string }
|
|
52
|
+
return v.contentKey === content && v.registerDigest ? v.registerDigest : null
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Persist a register digest against a file content key so the upload can resume later. */
|
|
59
|
+
export function saveRegisterResume(
|
|
60
|
+
storage: StorageLike,
|
|
61
|
+
key: string,
|
|
62
|
+
content: string,
|
|
63
|
+
registerDigest: string,
|
|
64
|
+
): void {
|
|
65
|
+
storage.setItem(key, JSON.stringify({ contentKey: content, registerDigest }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Clear any stored register-resume entry (on success, or to fall back to a fresh register). */
|
|
69
|
+
export function clearRegisterResume(storage: StorageLike, key: string): void {
|
|
70
|
+
storage.removeItem(key)
|
|
71
|
+
}
|
|
72
|
+
|
|
24
73
|
/**
|
|
25
74
|
* True if `err` is the gateway's "this consume was already redeemed" rejection (HTTP 409 with
|
|
26
75
|
* `code: 'redeemed'`). Distinguished from transient failures so we only re-consume (spend a new
|
|
@@ -11,15 +11,24 @@ 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
|
+
import { CopyableAddress, ExplorerLink, UiNotice, suiExplorerUrl } from '@meddleware/ui'
|
|
15
15
|
// Lightweight URL import — just the wasm asset URL (does not pull the walrus client).
|
|
16
16
|
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
|
|
17
17
|
import { WalletGuard } from '@meddleware/wallet-adapter'
|
|
18
18
|
import { useWallet, getSuiClient } from '../wallet.js'
|
|
19
19
|
import { fetchChallenge, buildAccessProof } from '@meddleware/nft-gate-client'
|
|
20
20
|
import { NETWORK, relayHosts, accessGate, uploadRelayMaxTipMist, walruscanBlobUrl } from '../config.js'
|
|
21
|
-
import { runBlobUpload
|
|
22
|
-
import {
|
|
21
|
+
import { runBlobUpload } from '../upload-flow.js'
|
|
22
|
+
import {
|
|
23
|
+
consumeStorageKey,
|
|
24
|
+
isRedeemedConflict,
|
|
25
|
+
resolveGatedAuthToken,
|
|
26
|
+
registerStorageKey,
|
|
27
|
+
contentKey,
|
|
28
|
+
loadRegisterResume,
|
|
29
|
+
saveRegisterResume,
|
|
30
|
+
clearRegisterResume,
|
|
31
|
+
} from '../access-resume.js'
|
|
23
32
|
import { useOwnedBlobs } from '../composables/useOwnedBlobs.js'
|
|
24
33
|
import MyBlobs from './MyBlobs.vue'
|
|
25
34
|
|
|
@@ -52,30 +61,17 @@ async function onPurchase(): Promise<void> {
|
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
|
|
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
|
-
|
|
71
64
|
// Wire the shared WalrusUpload widget to the extracted upload orchestration + the wallet. The
|
|
72
65
|
// 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
|
|
66
|
+
// the wallet-bound inputs and manages two resume layers so an interrupted upload wastes nothing —
|
|
67
|
+
// both persisted in localStorage, so they survive a page reload once the same file is re-selected:
|
|
74
68
|
// 1. Single-use consume: the relay treats the permanent on-chain `consumeDigest` as the one-time
|
|
75
69
|
// redemption token, so a use is only spent when an upload succeeds. The digest is persisted and
|
|
76
70
|
// reused across retries/reload (re-signing a fresh challenge is free); cleared on success.
|
|
77
|
-
// 2. Walrus
|
|
78
|
-
//
|
|
71
|
+
// 2. Walrus register: a registered-but-not-uploaded blob is resumed by its persisted register
|
|
72
|
+
// digest (matched to the re-selected file by content hash), skipping the register transaction
|
|
73
|
+
// (no new WAL/gas). Cleared on success, or when a resumed attempt itself fails (fall back to a
|
|
74
|
+
// fresh register).
|
|
79
75
|
async function performUpload(
|
|
80
76
|
bytes: Uint8Array,
|
|
81
77
|
opts: { relayHost: string; onStatus: (s: string) => void },
|
|
@@ -84,83 +80,73 @@ async function performUpload(
|
|
|
84
80
|
const executor = await buildExecutor()
|
|
85
81
|
const address = account.value.address
|
|
86
82
|
const key = contentKey(bytes)
|
|
83
|
+
const storage = window.localStorage
|
|
87
84
|
|
|
88
85
|
const gated = !!(gate && gateState.hasAccess.value === true && gateState.nftId.value)
|
|
89
86
|
const consumeKey = gate ? consumeStorageKey(NETWORK, gate.gateId, address) : null
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
const regKey = registerStorageKey(NETWORK, address)
|
|
88
|
+
|
|
89
|
+
// Resolve this attempt's relay token (gated only); reuses a stored consume, fresh challenge each time.
|
|
90
|
+
const token = (forceFresh: boolean): Promise<string | undefined> =>
|
|
91
|
+
!gated
|
|
92
|
+
? Promise.resolve(undefined)
|
|
93
|
+
: resolveGatedAuthToken({
|
|
94
|
+
storage,
|
|
95
|
+
key: consumeKey as string,
|
|
96
|
+
relayHost: opts.relayHost,
|
|
97
|
+
address,
|
|
98
|
+
nftId: gateState.nftId.value as string,
|
|
99
|
+
fetchChallenge,
|
|
100
|
+
buildConsume: (id, nonce) => gateState.buildConsume(id, nonce),
|
|
101
|
+
signAndExecute: (tx) => executor.signAndExecute(tx),
|
|
102
|
+
waitForTransaction: (digest) => executor.waitForTransaction(digest),
|
|
103
|
+
buildAccessProof,
|
|
104
|
+
sign: signPersonalMessage,
|
|
105
|
+
forceFresh,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const runOnce = async (authToken: string | undefined): Promise<UploadResult> => {
|
|
109
|
+
// Resume the register step iff a digest was saved for THIS exact file (survives reload).
|
|
110
|
+
const resumeRegisterDigest = loadRegisterResume(storage, regKey, key) ?? undefined
|
|
111
|
+
try {
|
|
112
|
+
const r = await runBlobUpload({
|
|
113
|
+
bytes,
|
|
114
|
+
network: NETWORK,
|
|
115
|
+
relayHost: opts.relayHost,
|
|
116
|
+
address,
|
|
117
|
+
wasmUrl: walrusWasmUrl,
|
|
118
|
+
maxTipMist: uploadRelayMaxTipMist(),
|
|
119
|
+
epochs: MAX_SINGLE_RESERVATION_EPOCHS,
|
|
120
|
+
executor,
|
|
121
|
+
suiClient: getSuiClient(),
|
|
122
|
+
authToken,
|
|
123
|
+
onStatus: opts.onStatus,
|
|
124
|
+
resumeRegisterDigest,
|
|
125
|
+
onRegistered: (digest) => saveRegisterResume(storage, regKey, key, digest),
|
|
126
|
+
})
|
|
127
|
+
// Success → clear both resume layers (the use is now genuinely spent for an upload).
|
|
128
|
+
clearRegisterResume(storage, regKey)
|
|
129
|
+
if (consumeKey) storage.removeItem(consumeKey)
|
|
130
|
+
return r
|
|
131
|
+
} catch (e) {
|
|
132
|
+
// A resumed attempt that fails drops the saved register digest so the next try does a full
|
|
133
|
+
// fresh register (never worse than today). A non-resumed failure keeps the digest that
|
|
134
|
+
// onRegistered saved, so the next try (or a reload + re-select) resumes without re-registering.
|
|
135
|
+
if (resumeRegisterDigest !== undefined) clearRegisterResume(storage, regKey)
|
|
136
|
+
throw e
|
|
96
137
|
}
|
|
97
|
-
authTokenRef.value = await resolveGatedAuthToken({
|
|
98
|
-
storage: window.localStorage,
|
|
99
|
-
key: consumeKey as string,
|
|
100
|
-
relayHost: opts.relayHost,
|
|
101
|
-
address,
|
|
102
|
-
nftId: gateState.nftId.value as string,
|
|
103
|
-
fetchChallenge,
|
|
104
|
-
buildConsume: (id, nonce) => gateState.buildConsume(id, nonce),
|
|
105
|
-
signAndExecute: (tx) => executor.signAndExecute(tx),
|
|
106
|
-
waitForTransaction: (digest) => executor.waitForTransaction(digest),
|
|
107
|
-
buildAccessProof,
|
|
108
|
-
sign: signPersonalMessage,
|
|
109
|
-
forceFresh,
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const deps = (resume?: UploadResumeState) => ({
|
|
114
|
-
bytes,
|
|
115
|
-
network: NETWORK,
|
|
116
|
-
relayHost: opts.relayHost,
|
|
117
|
-
address,
|
|
118
|
-
wasmUrl: walrusWasmUrl,
|
|
119
|
-
maxTipMist: uploadRelayMaxTipMist(),
|
|
120
|
-
epochs: MAX_SINGLE_RESERVATION_EPOCHS,
|
|
121
|
-
executor,
|
|
122
|
-
suiClient: getSuiClient(),
|
|
123
|
-
// Provider: resolved per request so a resumed flow uses the fresh token.
|
|
124
|
-
authToken: () => authTokenRef.value,
|
|
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
138
|
}
|
|
137
139
|
|
|
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
140
|
try {
|
|
144
|
-
return
|
|
141
|
+
return await runOnce(await token(false))
|
|
145
142
|
} 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
143
|
if (gated && isRedeemedConflict(e)) {
|
|
151
144
|
// Stored consume already redeemed (a prior upload actually landed): clear it, spend a fresh
|
|
152
|
-
// use, and retry — resuming the registered blob if
|
|
153
|
-
|
|
154
|
-
await
|
|
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
|
-
}
|
|
145
|
+
// use, and retry — resuming the registered blob if one is still saved for this file.
|
|
146
|
+
storage.removeItem(consumeKey as string)
|
|
147
|
+
return await runOnce(await token(true))
|
|
162
148
|
}
|
|
163
|
-
throw e // keep the
|
|
149
|
+
throw e // keep the saved register digest so a manual retry / reload resumes
|
|
164
150
|
}
|
|
165
151
|
}
|
|
166
152
|
|
|
@@ -228,6 +214,17 @@ function onSettled(): void {
|
|
|
228
214
|
|
|
229
215
|
<!-- Access held (or ungated relay): show the upload form. -->
|
|
230
216
|
<template v-else>
|
|
217
|
+
<!-- Gated relays spend a use before the file is stored — make the "attempt, not a
|
|
218
|
+
guarantee" nature explicit, while reassuring that attempts resume. -->
|
|
219
|
+
<UiNotice v-if="gateState.gateConfigured" type="info" class="use-notice">
|
|
220
|
+
Uploading spends <strong>one use</strong> of your access NFT (an on-chain step) before
|
|
221
|
+
the file is stored — it pays for an upload <em>attempt</em>, not a guaranteed upload.
|
|
222
|
+
Your attempt resumes automatically, even after a page reload if you re-select the same
|
|
223
|
+
file, so a use is normally not lost. A use is spent without a completed upload only if
|
|
224
|
+
you abandon the upload entirely, cancel a required wallet approval, or wait long enough
|
|
225
|
+
that the reserved storage lapses.
|
|
226
|
+
</UiNotice>
|
|
227
|
+
|
|
231
228
|
<WalrusUpload
|
|
232
229
|
:hosts="relayHosts(NETWORK)"
|
|
233
230
|
:connected="!!account"
|
|
@@ -316,6 +313,12 @@ function onSettled(): void {
|
|
|
316
313
|
text-align: center;
|
|
317
314
|
}
|
|
318
315
|
|
|
316
|
+
.use-notice {
|
|
317
|
+
margin: 1rem 0;
|
|
318
|
+
font-size: 0.85rem;
|
|
319
|
+
line-height: 1.5;
|
|
320
|
+
}
|
|
321
|
+
|
|
319
322
|
.tabs {
|
|
320
323
|
display: flex;
|
|
321
324
|
gap: 0.25rem;
|
package/src/upload-flow.ts
CHANGED
|
@@ -27,22 +27,14 @@ interface BuiltTx {
|
|
|
27
27
|
export interface BlobUploadFlow {
|
|
28
28
|
encode(): Promise<void>
|
|
29
29
|
register(opts: { owner: string; epochs: number; deletable: boolean }): BuiltTx
|
|
30
|
-
|
|
30
|
+
// `digest` = the register transaction digest. When resuming without a prior `register()` call on
|
|
31
|
+
// this flow instance, the SDK accepts the digest + `deletable` to upload against the already
|
|
32
|
+
// registered blob (see `@mysten/walrus` WriteBlobFlowUploadOptions).
|
|
33
|
+
upload(opts: { digest: string; deletable?: boolean }): Promise<void>
|
|
31
34
|
certify(): BuiltTx
|
|
32
35
|
getBlob(): Promise<{ blobId: string }>
|
|
33
36
|
}
|
|
34
37
|
|
|
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
|
-
|
|
46
38
|
export interface RunBlobUploadDeps {
|
|
47
39
|
bytes: Uint8Array
|
|
48
40
|
network: string
|
|
@@ -67,20 +59,25 @@ export interface RunBlobUploadDeps {
|
|
|
67
59
|
/** Lazy loader for the Walrus client module (keeps wasm out of the eager bundle). */
|
|
68
60
|
loadWalrusClient?: () => Promise<WalrusClientModule>
|
|
69
61
|
/**
|
|
70
|
-
* Resume a
|
|
71
|
-
*
|
|
62
|
+
* Resume from a blob registered in a PRIOR attempt (this session or a previous page load) by
|
|
63
|
+
* passing its register transaction digest. The file is re-encoded (client-side, no gas) but the
|
|
64
|
+
* on-chain `register` transaction is skipped — so an interrupted upload never re-registers (no new
|
|
65
|
+
* WAL/gas), even across a reload once the same file is re-selected. Omit for a fresh upload.
|
|
72
66
|
*/
|
|
73
|
-
|
|
67
|
+
resumeRegisterDigest?: string
|
|
74
68
|
/**
|
|
75
|
-
* Called
|
|
76
|
-
*
|
|
69
|
+
* Called with the register transaction digest immediately after a fresh register succeeds, so the
|
|
70
|
+
* caller can PERSIST it (e.g. to localStorage) and resume the upload after a failure/reload
|
|
71
|
+
* without re-registering.
|
|
77
72
|
*/
|
|
78
|
-
onRegistered?: (
|
|
73
|
+
onRegistered?: (registerDigest: string) => void
|
|
79
74
|
}
|
|
80
75
|
|
|
81
76
|
/**
|
|
82
|
-
* Register → upload → certify a blob and resolve its id + public URL.
|
|
83
|
-
* (
|
|
77
|
+
* Register → upload → certify a blob and resolve its id + public URL. Encoding is always performed
|
|
78
|
+
* (client-side, no gas); when `resumeRegisterDigest` is supplied the on-chain register transaction
|
|
79
|
+
* is skipped and the upload proceeds against the already-registered blob. Up to two wallet approvals
|
|
80
|
+
* (register — skipped on resume — and certify) are requested via `executor`; `onStatus` narrates.
|
|
84
81
|
*/
|
|
85
82
|
export async function runBlobUpload(deps: RunBlobUploadDeps): Promise<UploadResult> {
|
|
86
83
|
// The real module's flow types are richer than the narrow structural subset we use here, so the
|
|
@@ -90,28 +87,26 @@ export async function runBlobUpload(deps: RunBlobUploadDeps): Promise<UploadResu
|
|
|
90
87
|
(async () => (await import('@meddleware/walrus-client')) as unknown as WalrusClientModule)
|
|
91
88
|
const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await load()
|
|
92
89
|
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const client = createWalrusClient({
|
|
91
|
+
network: deps.network,
|
|
92
|
+
wasmUrl: deps.wasmUrl,
|
|
93
|
+
uploadRelayHost: deps.relayHost,
|
|
94
|
+
uploadRelayAuthToken: deps.authToken,
|
|
95
|
+
uploadRelayMaxTipMist: deps.maxTipMist,
|
|
96
|
+
})
|
|
97
|
+
const flow = createBlobUploadFlow(client, deps.bytes)
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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)
|
|
111
|
-
|
|
112
|
-
deps.onStatus('Encoding…')
|
|
113
|
-
await flow.encode()
|
|
99
|
+
// Encoding is deterministic from the content and costs no gas, so it always runs — including on a
|
|
100
|
+
// resume, where it re-derives the slivers for the re-selected file.
|
|
101
|
+
deps.onStatus('Encoding…')
|
|
102
|
+
await flow.encode()
|
|
114
103
|
|
|
104
|
+
let registerDigest: string
|
|
105
|
+
if (deps.resumeRegisterDigest) {
|
|
106
|
+
// Resume: the blob was registered in a prior attempt. Skip the register transaction (no new
|
|
107
|
+
// WAL/gas) and upload against the existing on-chain registration.
|
|
108
|
+
registerDigest = deps.resumeRegisterDigest
|
|
109
|
+
} else {
|
|
115
110
|
deps.onStatus('Registering blob (approve in wallet)…')
|
|
116
111
|
const regTx = flow.register({ owner: deps.address, epochs: deps.epochs, deletable: false })
|
|
117
112
|
regTx.setSenderIfNotSet(deps.address)
|
|
@@ -119,12 +114,12 @@ export async function runBlobUpload(deps: RunBlobUploadDeps): Promise<UploadResu
|
|
|
119
114
|
const reg = await deps.executor.signAndExecute(regTx)
|
|
120
115
|
await deps.executor.waitForTransaction(reg.digest)
|
|
121
116
|
registerDigest = reg.digest
|
|
122
|
-
//
|
|
123
|
-
deps.onRegistered?.(
|
|
117
|
+
// Persist point: hand back the digest so the upload can be resumed after a failure/reload.
|
|
118
|
+
deps.onRegistered?.(registerDigest)
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
deps.onStatus('Uploading to the relay…')
|
|
127
|
-
await flow.upload({ digest: registerDigest })
|
|
122
|
+
await flow.upload({ digest: registerDigest, deletable: false })
|
|
128
123
|
|
|
129
124
|
deps.onStatus('Certifying (approve in wallet)…')
|
|
130
125
|
const certTx = flow.certify()
|