@meddleware/walrus-ui 0.1.0 → 0.1.2

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,8 +1,8 @@
1
1
  {
2
2
  "name": "@meddleware/walrus-ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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
- "author": "MeddleWare <meddleware@proton.me>",
5
+ "author": "Meddleware <dev@meddleware.co.uk>",
6
6
  "license": "0BSD",
7
7
  "repository": {
8
8
  "type": "git",
@@ -20,16 +20,20 @@
20
20
  "dev": "vite",
21
21
  "build": "vue-tsc --noEmit && vite build",
22
22
  "preview": "vite preview",
23
- "type-check": "vue-tsc --noEmit"
23
+ "type-check": "vue-tsc --noEmit",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:e2e": "playwright test"
24
27
  },
25
28
  "publishConfig": {
26
29
  "access": "public"
27
30
  },
28
31
  "dependencies": {
29
- "@meddleware/design-tokens": "^0.1.0",
32
+ "@meddleware/design-tokens": "^0.1.2",
30
33
  "@meddleware/nft-gate-client": "^0.0.3",
31
- "@meddleware/walrus-client": "^0.0.2",
32
- "@meddleware/walrus-relay": "^0.1.0",
34
+ "@meddleware/ui": "^0.1.6",
35
+ "@meddleware/walrus-client": "^0.0.3",
36
+ "@meddleware/walrus-relay": "^0.1.1",
33
37
  "@mysten/sui": "~2.17.0",
34
38
  "@mysten/wallet-standard": "~0.19.9",
35
39
  "@mysten/walrus": "~1.1.7",
@@ -37,10 +41,12 @@
37
41
  "vue": "^3.5.40"
38
42
  },
39
43
  "devDependencies": {
44
+ "@playwright/test": "~1.49.1",
40
45
  "@types/node": "~24.12.2",
41
46
  "@vitejs/plugin-vue": "^6.0.8",
42
47
  "typescript": "~6.0.0",
43
48
  "vite": "^8.1.5",
49
+ "vitest": "~2.1.9",
44
50
  "vue-tsc": "~3.3.0"
45
51
  },
46
52
  "engines": {
package/src/App.vue CHANGED
@@ -10,10 +10,15 @@ import {
10
10
  import type { UploadResult } from '@meddleware/walrus-relay'
11
11
  // Lightweight URL import — just the wasm asset URL (does not pull the walrus client).
12
12
  import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
13
+ import { AppHeader, AppFooter, ColorModeControl, useColorMode } from '@meddleware/ui'
13
14
  import { useWallet, getSuiClient } from './wallet.js'
14
- import { NETWORK, OPERATOR_RELAY_HOSTS, relayHosts, accessGate } from './config.js'
15
+ import { NETWORK, OPERATOR_RELAY_HOSTS, relayHosts, accessGate, uploadRelayMaxTipMist } from './config.js'
16
+ import { runBlobUpload } from './upload-flow.js'
15
17
  import MyBlobs from './components/MyBlobs.vue'
16
18
 
19
+ const isEmbedded = new URLSearchParams(window.location.search).has('embedded')
20
+ const { mode, set } = useColorMode('dark')
21
+
17
22
  type Tab = 'upload' | 'blobs'
18
23
  const activeTab = ref<Tab>('upload')
19
24
 
@@ -43,15 +48,15 @@ async function onPurchase(): Promise<void> {
43
48
  }
44
49
  }
45
50
 
46
- // Wire the shared WalrusUpload widget to @meddleware/walrus-client + the wallet.
51
+ // Wire the shared WalrusUpload widget to the extracted upload orchestration + the wallet. The
52
+ // register/upload/certify sequence lives in src/upload-flow.ts (unit-tested); this closure only
53
+ // gathers the wallet-bound inputs (executor, sui client, gated proof token) and delegates.
47
54
  async function performUpload(
48
55
  bytes: Uint8Array,
49
56
  opts: { relayHost: string; onStatus: (s: string) => void },
50
57
  ): Promise<UploadResult> {
51
58
  if (!account.value) throw new Error('Connect your wallet first.')
52
- const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await import('@meddleware/walrus-client')
53
59
  const executor = await buildExecutor(NETWORK)
54
- const suiClient = getSuiClient(NETWORK)
55
60
 
56
61
  // If the relay is NFT-gated and we hold access, attach a signed proof token.
57
62
  let authToken: string | undefined
@@ -63,41 +68,19 @@ async function performUpload(
63
68
  })
64
69
  }
65
70
 
66
- const client = createWalrusClient({
71
+ return runBlobUpload({
72
+ bytes,
67
73
  network: NETWORK,
74
+ relayHost: opts.relayHost,
75
+ address: account.value.address,
68
76
  wasmUrl: walrusWasmUrl,
69
- uploadRelayHost: opts.relayHost,
70
- uploadRelayAuthToken: authToken,
71
- uploadRelayMaxTipMist: Number(import.meta.env.VITE_UPLOAD_RELAY_MAX_TIP_MIST) || 50_000_000,
72
- })
73
- const flow = createBlobUploadFlow(client, bytes)
74
-
75
- opts.onStatus('Encoding…')
76
- await flow.encode()
77
-
78
- opts.onStatus('Registering blob (approve in wallet)…')
79
- const regTx = flow.register({
80
- owner: account.value.address,
77
+ maxTipMist: uploadRelayMaxTipMist(),
81
78
  epochs: MAX_SINGLE_RESERVATION_EPOCHS,
82
- deletable: false,
79
+ executor,
80
+ suiClient: getSuiClient(NETWORK),
81
+ authToken,
82
+ onStatus: opts.onStatus,
83
83
  })
84
- regTx.setSenderIfNotSet(account.value.address)
85
- await regTx.build({ client: suiClient })
86
- const reg = await executor.signAndExecute(regTx)
87
- await executor.waitForTransaction(reg.digest)
88
-
89
- opts.onStatus('Uploading to the relay…')
90
- await flow.upload({ digest: reg.digest })
91
-
92
- opts.onStatus('Certifying (approve in wallet)…')
93
- const certTx = flow.certify()
94
- certTx.setSenderIfNotSet(account.value.address)
95
- await certTx.build({ client: suiClient })
96
- const cert = await executor.signAndExecute(certTx)
97
- await executor.waitForTransaction(cert.digest)
98
-
99
- const blob = await flow.getBlob()
100
- return { blobId: blob.blobId, url: walrusBlobUrl(NETWORK, blob.blobId), digest: cert.digest }
101
84
  }
102
85
 
103
86
  function onUploaded(r: UploadResult): void {
@@ -106,133 +89,149 @@ function onUploaded(r: UploadResult): void {
106
89
  </script>
107
90
 
108
91
  <template>
109
- <div class="page">
110
- <header class="head">
111
- <div>
112
- <h1>Walrus Assets</h1>
113
- <p class="sub">Upload and manage blobs on Walrus decentralised storage ({{ NETWORK }}).</p>
114
- </div>
115
- <TipConfigBadge :host="OPERATOR_RELAY_HOSTS[NETWORK]" />
116
- </header>
117
-
118
- <nav class="tabs" aria-label="Feature tabs">
119
- <button
120
- type="button"
121
- class="tab"
122
- :class="{ active: activeTab === 'upload' }"
123
- @click="activeTab = 'upload'"
124
- >
125
- Upload
126
- </button>
127
- <button
128
- type="button"
129
- class="tab"
130
- :class="{ active: activeTab === 'blobs' }"
131
- @click="activeTab = 'blobs'"
132
- >
133
- My Blobs
134
- </button>
135
- </nav>
136
-
137
- <section class="wallet">
138
- <template v-if="account">
139
- <span class="addr">{{ account.address.slice(0, 8) }}…{{ account.address.slice(-4) }}</span>
140
- <button type="button" @click="disconnect">Disconnect</button>
92
+ <div class="app" :class="{ 'app--embedded': isEmbedded }">
93
+ <AppHeader v-if="!isEmbedded" variant="dark">
94
+ <template #brand>
95
+ <span>Walrus Assets</span>
141
96
  </template>
142
- <template v-else>
143
- <button type="button" :disabled="!wallets.length" @click="onConnectFirst">
144
- {{ wallets.length ? 'Connect wallet' : 'No wallet detected' }}
145
- </button>
97
+ <template #actions>
98
+ <TipConfigBadge :host="OPERATOR_RELAY_HOSTS[NETWORK]" />
99
+ <ColorModeControl :model-value="mode" @update:model-value="set" />
146
100
  </template>
147
- </section>
101
+ </AppHeader>
102
+
103
+ <div class="page">
104
+ <p class="sub">Upload and manage blobs on Walrus decentralised storage ({{ NETWORK }}).</p>
105
+
106
+ <nav class="tabs" aria-label="Feature tabs">
107
+ <button
108
+ type="button"
109
+ class="tab"
110
+ :class="{ active: activeTab === 'upload' }"
111
+ @click="activeTab = 'upload'"
112
+ >
113
+ Upload
114
+ </button>
115
+ <button
116
+ type="button"
117
+ class="tab"
118
+ :class="{ active: activeTab === 'blobs' }"
119
+ @click="activeTab = 'blobs'"
120
+ >
121
+ My Blobs
122
+ </button>
123
+ </nav>
124
+
125
+ <section class="wallet">
126
+ <template v-if="account">
127
+ <span class="addr">{{ account.address.slice(0, 8) }}…{{ account.address.slice(-4) }}</span>
128
+ <button type="button" @click="disconnect">Disconnect</button>
129
+ </template>
130
+ <template v-else>
131
+ <button type="button" :disabled="!wallets.length" @click="onConnectFirst">
132
+ {{ wallets.length ? 'Connect wallet' : 'No wallet detected' }}
133
+ </button>
134
+ </template>
135
+ </section>
148
136
 
149
- <template v-if="activeTab === 'upload'">
150
- <AccessGateCta
151
- :gate-configured="gateState.gateConfigured"
152
- :has-access="gateState.hasAccess.value"
153
- :busy="purchasing"
154
- :price-mist="gate?.priceMist ?? null"
155
- @purchase="onPurchase"
156
- />
137
+ <template v-if="activeTab === 'upload'">
138
+ <AccessGateCta
139
+ :gate-configured="gateState.gateConfigured"
140
+ :has-access="gateState.hasAccess.value"
141
+ :busy="purchasing"
142
+ :price-mist="gate?.priceMist ?? null"
143
+ @purchase="onPurchase"
144
+ />
145
+
146
+ <WalrusUpload
147
+ :hosts="relayHosts(NETWORK)"
148
+ :connected="!!account"
149
+ :access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess }"
150
+ :perform-upload="performUpload"
151
+ @uploaded="onUploaded"
152
+ />
153
+
154
+ <section v-if="result" class="result">
155
+ <h2>Uploaded ✓</h2>
156
+ <p><strong>Blob ID:</strong> <code>{{ result.blobId }}</code></p>
157
+ <p>
158
+ <strong>URL:</strong>
159
+ <a :href="result.url" target="_blank" rel="noopener">{{ result.url }}</a>
160
+ </p>
161
+ <p v-if="result.digest"><strong>Certify tx:</strong> <code>{{ result.digest }}</code></p>
162
+ </section>
163
+ </template>
157
164
 
158
- <WalrusUpload
159
- :hosts="relayHosts(NETWORK)"
160
- :connected="!!account"
161
- :access="{ gateConfigured: gateState.gateConfigured, hasAccess: gateState.hasAccess }"
162
- :perform-upload="performUpload"
163
- @uploaded="onUploaded"
165
+ <MyBlobs
166
+ v-if="activeTab === 'blobs'"
167
+ :address="account?.address ?? null"
168
+ :build-executor="() => buildExecutor(NETWORK)"
164
169
  />
170
+ </div>
165
171
 
166
- <section v-if="result" class="result">
167
- <h2>Uploaded ✓</h2>
168
- <p><strong>Blob ID:</strong> <code>{{ result.blobId }}</code></p>
169
- <p>
170
- <strong>URL:</strong>
171
- <a :href="result.url" target="_blank" rel="noopener">{{ result.url }}</a>
172
- </p>
173
- <p v-if="result.digest"><strong>Certify tx:</strong> <code>{{ result.digest }}</code></p>
174
- </section>
175
- </template>
176
-
177
- <MyBlobs
178
- v-if="activeTab === 'blobs'"
179
- :address="account?.address ?? null"
180
- :build-executor="() => buildExecutor(NETWORK)"
181
- />
182
-
183
- <footer class="foot">
184
- <p>© MeddleWare · <a href="https://sui.meddleware.co.uk">more SUI tools</a></p>
185
- </footer>
172
+ <AppFooter v-if="!isEmbedded" />
186
173
  </div>
187
174
  </template>
188
175
 
189
176
  <style scoped>
177
+ .app {
178
+ min-height: 100vh;
179
+ display: flex;
180
+ flex-direction: column;
181
+ }
182
+
183
+ .app--embedded {
184
+ min-height: 100%;
185
+ }
186
+
190
187
  .page {
191
188
  max-width: 640px;
192
189
  margin: 0 auto;
193
190
  padding: 2rem 1.25rem 4rem;
191
+ flex: 1;
194
192
  }
195
- .head {
196
- display: flex;
197
- justify-content: space-between;
198
- align-items: flex-start;
199
- gap: 1rem;
200
- flex-wrap: wrap;
201
- }
202
- .head h1 {
203
- margin: 0;
204
- font-size: 1.8rem;
193
+
194
+ .app--embedded .page {
195
+ padding-top: 1rem;
196
+ padding-bottom: 1rem;
205
197
  }
198
+
206
199
  .sub {
207
- color: var(--mw-color-text-muted, #666);
200
+ color: var(--muted);
208
201
  margin: 0.25rem 0 0;
209
202
  }
203
+
210
204
  .wallet {
211
205
  display: flex;
212
206
  align-items: center;
213
207
  gap: 0.75rem;
214
208
  margin: 1.25rem 0;
215
209
  }
210
+
216
211
  .addr {
217
212
  font-family: monospace;
218
213
  font-size: 0.9rem;
219
214
  }
215
+
220
216
  .result {
221
217
  margin-top: 1.5rem;
222
218
  padding: 1rem;
223
- border: 1px solid var(--mw-color-border, #ddd);
219
+ border: 1px solid var(--border);
224
220
  border-radius: 10px;
225
221
  word-break: break-all;
226
222
  }
223
+
227
224
  .result code {
228
225
  font-size: 0.85rem;
229
226
  }
227
+
230
228
  .tabs {
231
229
  display: flex;
232
230
  gap: 0.25rem;
233
231
  margin: 1rem 0 0.5rem;
234
- border-bottom: 2px solid var(--mw-color-border, #ddd);
232
+ border-bottom: 2px solid var(--border);
235
233
  }
234
+
236
235
  .tab {
237
236
  background: none;
238
237
  border: none;
@@ -241,16 +240,12 @@ function onUploaded(r: UploadResult): void {
241
240
  margin-bottom: -2px;
242
241
  cursor: pointer;
243
242
  font-size: 0.95rem;
244
- color: var(--mw-color-text-muted, #666);
243
+ color: var(--muted);
245
244
  }
245
+
246
246
  .tab.active {
247
- border-bottom-color: var(--mw-color-primary, #6c3);
248
- color: var(--mw-color-text, #111);
247
+ border-bottom-color: var(--accent);
248
+ color: var(--text);
249
249
  font-weight: 600;
250
250
  }
251
- .foot {
252
- margin-top: 3rem;
253
- font-size: 0.85rem;
254
- color: var(--mw-color-text-muted, #666);
255
- }
256
251
  </style>
package/src/config.ts CHANGED
@@ -8,7 +8,11 @@ import {
8
8
  accessGateNftType,
9
9
  } from '@meddleware/walrus-relay'
10
10
 
11
- const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
11
+ /** Build-time env bag. Injectable on the env-reading helpers below purely so unit tests can exercise
12
+ * each branch without depending on Vite's (frozen) `import.meta.env` inlining. */
13
+ export type EnvSource = Record<string, string | undefined>
14
+
15
+ const env: EnvSource = (import.meta as unknown as { env?: EnvSource }).env ?? {}
12
16
 
13
17
  /** Active Walrus network, from `VITE_NETWORK` (default `testnet`). */
14
18
  export const NETWORK: WalrusNetwork = (env.VITE_NETWORK as WalrusNetwork) || 'testnet'
@@ -30,6 +34,19 @@ export function relayHosts(network: WalrusNetwork): { operator: string; public:
30
34
  return { operator: OPERATOR_RELAY_HOSTS[network], public: PUBLIC_WALRUS_RELAY_HOSTS[network] }
31
35
  }
32
36
 
37
+ /** Default relay tip ceiling (MIST) when `VITE_UPLOAD_RELAY_MAX_TIP_MIST` is unset (0.05 SUI). */
38
+ export const DEFAULT_UPLOAD_RELAY_MAX_TIP_MIST = 50_000_000
39
+
40
+ /**
41
+ * Cap on the relay tip payment in MIST, from `VITE_UPLOAD_RELAY_MAX_TIP_MIST`. This is a ceiling to
42
+ * prevent overpayment (the relay asks for the minimum), NOT a fixed charge. A non-positive or
43
+ * unparseable value falls back to {@link DEFAULT_UPLOAD_RELAY_MAX_TIP_MIST}.
44
+ */
45
+ export function uploadRelayMaxTipMist(envSource: EnvSource = env): number {
46
+ const parsed = Number(envSource.VITE_UPLOAD_RELAY_MAX_TIP_MIST)
47
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_UPLOAD_RELAY_MAX_TIP_MIST
48
+ }
49
+
33
50
  /**
34
51
  * JSON-RPC endpoint used to build + execute the register/certify transactions.
35
52
  * NOTE: this is JSON-RPC (the public testnet fullnode serves gRPC only for JSON-RPC,
@@ -45,19 +62,19 @@ export const RPC_URLS: Record<WalrusNetwork, string> = {
45
62
  * packageId and platformConfigId are hardcoded in the library — operators only
46
63
  * need to supply their Gate object ID, soulbound flag, and purchase price.
47
64
  */
48
- export function accessGate(network: WalrusNetwork): RelayGateConfig | null {
65
+ export function accessGate(network: WalrusNetwork, envSource: EnvSource = env): RelayGateConfig | null {
49
66
  const NET = network.toUpperCase()
50
67
  const packageId = ACCESS_GATE_PACKAGE_ID[network]
51
68
  const platformConfigId = ACCESS_GATE_PLATFORM_CONFIG_ID[network]
52
- const gateId = env[`VITE_ACCESS_GATE_ID_${NET}`]
69
+ const gateId = envSource[`VITE_ACCESS_GATE_ID_${NET}`]
53
70
  if (!packageId || !platformConfigId || !gateId) return null
54
- const soulbound = env[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] === 'true'
71
+ const soulbound = envSource[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] === 'true'
55
72
  return {
56
73
  packageId,
57
74
  gateId,
58
75
  platformConfigId,
59
76
  nftType: accessGateNftType(network, soulbound),
60
77
  soulbound,
61
- priceMist: BigInt(env[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
78
+ priceMist: BigInt(envSource[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
62
79
  }
63
80
  }
package/src/main.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import '@meddleware/design-tokens/tokens.css'
2
+ import '@meddleware/ui/base.css'
2
3
  import { createApp } from 'vue'
3
4
  import App from './App.vue'
4
5
 
@@ -0,0 +1,100 @@
1
+ // The multi-step Walrus blob upload orchestration (encode → register → upload → certify → resolve),
2
+ // extracted from App.vue so it can be unit-tested and reused by the e2e harness.
3
+ //
4
+ // The `@mysten/walrus` wasm client must NOT be pulled into the eager module graph (see CLAUDE.md),
5
+ // so this module never imports @meddleware/walrus-client at top level — it loads it lazily through
6
+ // the injectable `loadWalrusClient` (default: a dynamic import). Tests inject a fake loader.
7
+ import type { UploadResult } from '@meddleware/walrus-relay'
8
+
9
+ /** Minimal transaction executor — the structural subset App.vue's wallet executor already provides. */
10
+ export interface UploadExecutor {
11
+ signAndExecute(tx: unknown): Promise<{ digest: string }>
12
+ waitForTransaction(digest: string): Promise<unknown>
13
+ }
14
+
15
+ /** The slice of `@meddleware/walrus-client` this flow needs. */
16
+ export interface WalrusClientModule {
17
+ createWalrusClient: (opts: Record<string, unknown>) => unknown
18
+ createBlobUploadFlow: (client: unknown, bytes: Uint8Array) => BlobUploadFlow
19
+ walrusBlobUrl: (network: string, blobId: string) => string
20
+ }
21
+
22
+ interface BuiltTx {
23
+ setSenderIfNotSet(address: string): void
24
+ build(opts: { client: unknown }): Promise<unknown>
25
+ }
26
+
27
+ interface BlobUploadFlow {
28
+ encode(): Promise<void>
29
+ register(opts: { owner: string; epochs: number; deletable: boolean }): BuiltTx
30
+ upload(opts: { digest: string }): Promise<void>
31
+ certify(): BuiltTx
32
+ getBlob(): Promise<{ blobId: string }>
33
+ }
34
+
35
+ export interface RunBlobUploadDeps {
36
+ bytes: Uint8Array
37
+ network: string
38
+ relayHost: string
39
+ /** Owner/sender address (already-connected wallet account). */
40
+ address: string
41
+ /** Wasm bundle URL for the Walrus client. */
42
+ wasmUrl: string
43
+ /** Cap on the relay tip payment in MIST. */
44
+ maxTipMist: number
45
+ /** Blob storage reservation length in epochs. */
46
+ epochs: number
47
+ executor: UploadExecutor
48
+ /** A Sui client used to `build()` the register/certify transactions. */
49
+ suiClient: unknown
50
+ /** Optional Bearer proof token for an NFT-gated relay. */
51
+ authToken?: string
52
+ onStatus: (s: string) => void
53
+ /** Lazy loader for the Walrus client module (keeps wasm out of the eager bundle). */
54
+ loadWalrusClient?: () => Promise<WalrusClientModule>
55
+ }
56
+
57
+ /**
58
+ * Register → upload → certify a blob and resolve its id + public URL. Two wallet approvals
59
+ * (register, certify) are requested via `executor`; `onStatus` narrates each step.
60
+ */
61
+ export async function runBlobUpload(deps: RunBlobUploadDeps): Promise<UploadResult> {
62
+ // The real module's flow types are richer than the narrow structural subset we use here, so the
63
+ // default loader casts through `unknown`. Tests inject an exact-shape fake instead.
64
+ const load =
65
+ deps.loadWalrusClient ??
66
+ (async () => (await import('@meddleware/walrus-client')) as unknown as WalrusClientModule)
67
+ const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await load()
68
+
69
+ const client = createWalrusClient({
70
+ network: deps.network,
71
+ wasmUrl: deps.wasmUrl,
72
+ uploadRelayHost: deps.relayHost,
73
+ uploadRelayAuthToken: deps.authToken,
74
+ uploadRelayMaxTipMist: deps.maxTipMist,
75
+ })
76
+ const flow = createBlobUploadFlow(client, deps.bytes)
77
+
78
+ deps.onStatus('Encoding…')
79
+ await flow.encode()
80
+
81
+ deps.onStatus('Registering blob (approve in wallet)…')
82
+ const regTx = flow.register({ owner: deps.address, epochs: deps.epochs, deletable: false })
83
+ regTx.setSenderIfNotSet(deps.address)
84
+ await regTx.build({ client: deps.suiClient })
85
+ const reg = await deps.executor.signAndExecute(regTx)
86
+ await deps.executor.waitForTransaction(reg.digest)
87
+
88
+ deps.onStatus('Uploading to the relay…')
89
+ await flow.upload({ digest: reg.digest })
90
+
91
+ deps.onStatus('Certifying (approve in wallet)…')
92
+ const certTx = flow.certify()
93
+ certTx.setSenderIfNotSet(deps.address)
94
+ await certTx.build({ client: deps.suiClient })
95
+ const cert = await deps.executor.signAndExecute(certTx)
96
+ await deps.executor.waitForTransaction(cert.digest)
97
+
98
+ const blob = await flow.getBlob()
99
+ return { blobId: blob.blobId, url: walrusBlobUrl(deps.network, blob.blobId), digest: cert.digest }
100
+ }