@meddleware/walrus-ui 0.1.23 → 0.1.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/walrus-ui",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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",
@@ -45,7 +45,7 @@
45
45
  "@meddleware/ui": "^0.1.10",
46
46
  "@meddleware/wallet-adapter": "^0.0.5",
47
47
  "@meddleware/walrus-client": "^0.0.9",
48
- "@meddleware/walrus-relay": "^0.1.8",
48
+ "@meddleware/walrus-relay": "^0.1.10",
49
49
  "@mysten/sui": "^2.30.0",
50
50
  "@mysten/wallet-standard": "^0.20.0",
51
51
  "@mysten/walrus": "~1.2.24",
@@ -10,7 +10,7 @@ import {
10
10
  useAccessGate,
11
11
  MAX_SINGLE_RESERVATION_EPOCHS,
12
12
  } from '@meddleware/walrus-relay'
13
- import type { UploadResult } from '@meddleware/walrus-relay'
13
+ import type { UploadResult, UploadProgress } from '@meddleware/walrus-relay'
14
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'
@@ -19,6 +19,7 @@ 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
21
  import { runBlobUpload } from '../upload-flow.js'
22
+ import { getCertifyRetry } from '@meddleware/walrus-relay'
22
23
  import {
23
24
  consumeStorageKey,
24
25
  isRedeemedConflict,
@@ -67,7 +68,7 @@ async function onPurchase(): Promise<void> {
67
68
  // upload-flow.ts). Reusing a prior registration is what produced "the received transaction is too old".
68
69
  async function performUpload(
69
70
  bytes: Uint8Array,
70
- opts: { relayHost: string; onStatus: (s: string) => void },
71
+ opts: { relayHost: string; onStatus: (s: string | UploadProgress) => void },
71
72
  ): Promise<UploadResult> {
72
73
  if (!account.value) throw new Error('Connect your wallet first.')
73
74
  const executor = await buildExecutor()
@@ -77,6 +78,10 @@ async function performUpload(
77
78
  const gated = !!(gate && gateState.hasAccess.value === true && gateState.nftId.value)
78
79
  const consumeKey = gate ? consumeStorageKey(NETWORK, gate.gateId, address) : null
79
80
 
81
+ // Gated uploads spend one NFT use on-chain before the core flow — surface it as the leading
82
+ // "Access" step so the stepper reflects the extra wallet approval (a reused consume is instant).
83
+ if (gated) opts.onStatus({ step: 'access', detail: 'Confirming access…' })
84
+
80
85
  // Resolve this attempt's relay token (gated only); reuses a stored consume, fresh challenge each time.
81
86
  const token = (forceFresh: boolean): Promise<string | undefined> =>
82
87
  !gated
@@ -97,22 +102,29 @@ async function performUpload(
97
102
  })
98
103
 
99
104
  const runOnce = async (authToken: string | undefined): Promise<UploadResult> => {
100
- const r = await runBlobUpload({
101
- bytes,
102
- network: NETWORK,
103
- relayHost: opts.relayHost,
104
- address,
105
- wasmUrl: walrusWasmUrl,
106
- maxTipMist: uploadRelayMaxTipMist(),
107
- epochs: MAX_SINGLE_RESERVATION_EPOCHS,
108
- executor,
109
- suiClient: getSuiClient(),
110
- authToken,
111
- onStatus: opts.onStatus,
112
- })
113
- // Success → clear the consume layer (the use is now genuinely spent for an upload).
114
- if (consumeKey) storage.removeItem(consumeKey)
115
- return r
105
+ try {
106
+ const r = await runBlobUpload({
107
+ bytes,
108
+ network: NETWORK,
109
+ relayHost: opts.relayHost,
110
+ address,
111
+ wasmUrl: walrusWasmUrl,
112
+ maxTipMist: uploadRelayMaxTipMist(),
113
+ epochs: MAX_SINGLE_RESERVATION_EPOCHS,
114
+ executor,
115
+ suiClient: getSuiClient(),
116
+ authToken,
117
+ onStatus: opts.onStatus,
118
+ })
119
+ // Success → clear the consume layer (the use is now genuinely spent for an upload).
120
+ if (consumeKey) storage.removeItem(consumeKey)
121
+ return r
122
+ } catch (e) {
123
+ // A certify-only failure means the upload already landed (relay access was used), so clear the
124
+ // consume too — the certify retry is a plain Sui tx and must not trigger a fresh NFT consume.
125
+ if (getCertifyRetry(e) && consumeKey) storage.removeItem(consumeKey)
126
+ throw e
127
+ }
116
128
  }
117
129
 
118
130
  try {
@@ -11,7 +11,8 @@
11
11
  // can't be reused across attempts — reusing a prior/discovered registration (localStorage or on-chain
12
12
  // discovery) hands the relay an old tx with a non-matching nonce. Each attempt re-encodes (free) and
13
13
  // registers fresh so the tip+nonce the relay verifies is always recent.
14
- import type { UploadResult } from '@meddleware/walrus-relay'
14
+ import type { UploadResult, UploadProgress } from '@meddleware/walrus-relay'
15
+ import { attachCertifyRetry } from '@meddleware/walrus-relay'
15
16
 
16
17
  /** Minimal transaction executor — the structural subset App.vue's wallet executor already provides. */
17
18
  export interface UploadExecutor {
@@ -62,7 +63,8 @@ export interface RunBlobUploadDeps {
62
63
  * upload presents a fresh challenge signature (see `@meddleware/walrus-client`).
63
64
  */
64
65
  authToken?: string | (() => string | undefined)
65
- onStatus: (s: string) => void
66
+ /** Structured step progress; drives the stepped indicator in the WalrusUpload widget. */
67
+ onStatus: (p: UploadProgress) => void
66
68
  /** Lazy loader for the Walrus client module (keeps wasm out of the eager bundle). */
67
69
  loadWalrusClient?: () => Promise<WalrusClientModule>
68
70
  }
@@ -90,26 +92,40 @@ export async function runBlobUpload(deps: RunBlobUploadDeps): Promise<UploadResu
90
92
  })
91
93
  const flow = createBlobUploadFlow(client, deps.bytes)
92
94
 
93
- deps.onStatus('Encoding…')
95
+ deps.onStatus({ step: 'encode', detail: 'Encoding…' })
94
96
  await flow.encode()
95
97
 
96
- deps.onStatus('Registering blob (approve in wallet)…')
98
+ deps.onStatus({ step: 'register', detail: 'Registering blob (approve in wallet)…' })
97
99
  const regTx = flow.register({ owner: deps.address, epochs: deps.epochs, deletable: false })
98
100
  regTx.setSenderIfNotSet(deps.address)
99
101
  await regTx.build({ client: deps.suiClient })
100
102
  const reg = await deps.executor.signAndExecute(regTx)
101
103
  await deps.executor.waitForTransaction(reg.digest)
102
104
 
103
- deps.onStatus('Uploading to the relay…')
105
+ deps.onStatus({ step: 'upload', detail: 'Uploading to the relay…' })
104
106
  await flow.upload({ digest: reg.digest, deletable: false })
105
107
 
106
- deps.onStatus('Certifying (approve in wallet)…')
107
- const certTx = flow.certify()
108
- certTx.setSenderIfNotSet(deps.address)
109
- await certTx.build({ client: deps.suiClient })
110
- const cert = await deps.executor.signAndExecute(certTx)
111
- await deps.executor.waitForTransaction(cert.digest)
108
+ // Certify is a plain owner tx built from the storage-node certificate the live `flow` now holds
109
+ // (no relay, no tip). If it fails (e.g. the user rejects the prompt) the blob is already registered
110
+ // + stored + paid, so we never want to redo the upload — we expose this closure to retry certify
111
+ // alone. Re-calling `flow.certify()` just rebuilds the same tx from the in-memory certificate.
112
+ const runCertify = async (): Promise<UploadResult> => {
113
+ deps.onStatus({ step: 'certify', detail: 'Certifying (approve in wallet)…' })
114
+ const certTx = flow.certify()
115
+ certTx.setSenderIfNotSet(deps.address)
116
+ await certTx.build({ client: deps.suiClient })
117
+ const cert = await deps.executor.signAndExecute(certTx)
118
+ await deps.executor.waitForTransaction(cert.digest)
119
+ const blob = await flow.getBlob()
120
+ return { blobId: blob.blobId, url: walrusBlobUrl(deps.network, blob.blobId), digest: cert.digest }
121
+ }
112
122
 
113
- const blob = await flow.getBlob()
114
- return { blobId: blob.blobId, url: walrusBlobUrl(deps.network, blob.blobId), digest: cert.digest }
123
+ try {
124
+ return await runCertify()
125
+ } catch (e) {
126
+ // Upload landed but certify did not: attach a retry so the UI can offer a "Certify" button
127
+ // rather than discarding the paid upload. The closure keeps the live flow (and its certificate).
128
+ attachCertifyRetry<UploadResult>(e, runCertify)
129
+ throw e
130
+ }
115
131
  }