@meddleware/walrus-client 0.0.1

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 ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.0.1] - 2026-08-27
9
+
10
+ ### Added
11
+
12
+ - Initial release as `@meddleware/walrus-client` (renamed from internal `@meddleware/sui-walrus`)
13
+ - `createWalrusClient()` — configurable Walrus client factory with upload relay, WASM, and `rpcUrl` override support
14
+ - `uploadBytes` / `uploadLocalFile` — Node.js and browser blob upload (quilt)
15
+ - `uploadImageBytes` / `createBlobUploadFlow` — raw-blob variants for wallet/explorer-renderable assets
16
+ - `createUploadFlow` — multi-step browser upload flow for wallet-popup-safe signing
17
+ - `extendBlobLifetime` / `extendBlobLifetimeTransaction` — extend blob storage before expiry
18
+ - `setBlobAttributes` / `setBlobAttributesTransaction` / `readBlobAttributes` — on-chain metadata management
19
+ - `fetchOwnedWalrusBlobs` — enumerate all Walrus blobs owned by an address (dynamic type resolution, no hardcoded addresses)
20
+ - `createRelayAccessToken` / `buildAccessProofToken` / `fetchRelayChallenge` — NFT-gated relay access helpers
21
+ - Exported constants: `DEFAULT_RPC_URLS`, `PUBLIC_UPLOAD_RELAY_HOSTS`, `WALRUS_AGGREGATOR_HOSTS`
22
+ - `disableUploadRelay` option to bypass relay and write directly to Walrus storage nodes
package/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ BSD Zero Clause License
2
+
3
+ Copyright (c) 2026 MeddleWare
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # @meddleware/walrus-client
2
+
3
+ Walrus decentralised storage client and asset management utilities for Sui applications.
4
+
5
+ [![License: 0BSD](https://img.shields.io/badge/License-0BSD-blue.svg)](LICENSE)
6
+
7
+ This package provides configuration, upload helpers, and lifetime management for storing assets (images, metadata, documents) on Walrus — a decentralised storage network integrated with Sui. It works in both Node.js (scripts, deployment tools) and the browser (Vite apps with wallet signing).
8
+
9
+ ## Product semantics
10
+
11
+ **Walrus is NOT permanent storage.** All blobs expire after their epoch count runs out. A Walrus epoch is approximately 2 weeks.
12
+
13
+ - Recommended default: **200 epochs** ≈ 7.7 years (exported as `LONG_TERM_EPOCHS`)
14
+ - **Before expiry:** Use `extendBlobLifetime()` to renew the blob
15
+ - **Deletable blobs:** Set `deletable: true` only if cleanup is guaranteed before expiry; otherwise blobs cannot be recovered
16
+
17
+ > A single Walrus reservation cannot exceed `max_epochs_ahead` (53 on testnet/mainnet, exported as `MAX_SINGLE_RESERVATION_EPOCHS`). Reaching `LONG_TERM_EPOCHS` requires periodic renewal via `extendBlobLifetime()`.
18
+
19
+ ## Quick start
20
+
21
+ ### Node.js (deployment script)
22
+
23
+ ```typescript
24
+ import { createWalrusClient, uploadLocalFile, LONG_TERM_EPOCHS } from '@meddleware/walrus-client'
25
+ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
26
+ import * as fs from 'node:fs'
27
+
28
+ const keypair = Ed25519Keypair.fromSecretKey(fs.readFileSync('.secrets/keypair', 'utf-8'))
29
+ const client = createWalrusClient({ network: 'mainnet' })
30
+
31
+ const { blobId } = await uploadLocalFile(
32
+ client,
33
+ 'assets/icon.png',
34
+ 'icon.png',
35
+ keypair,
36
+ { epochs: LONG_TERM_EPOCHS, deletable: false },
37
+ )
38
+
39
+ console.log('Uploaded blob:', blobId)
40
+ ```
41
+
42
+ ### Browser (Vite app)
43
+
44
+ ```typescript
45
+ // vite.config.ts or similar
46
+ import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
47
+
48
+ // In a composable or component
49
+ import { createWalrusClient, createUploadFlow } from '@meddleware/walrus-client'
50
+
51
+ const client = createWalrusClient({ network: 'testnet', wasmUrl: walrusWasmUrl })
52
+
53
+ // Step 1: Create the flow (can happen immediately when file is selected)
54
+ const flow = createUploadFlow(client, fileBytes, 'my-asset.png')
55
+
56
+ // Step 2: Register (user clicks a button, signs with wallet)
57
+ const registerTx = flow.register({ epochs: 200, owner: address })
58
+ const result = await signAndExecuteTransaction({ transaction: registerTx })
59
+
60
+ // Step 3: Upload to storage nodes
61
+ await flow.upload({ digest: result.digest })
62
+
63
+ // Step 4: Certify on-chain (user clicks another button, signs with wallet)
64
+ const certifyTx = flow.certify()
65
+ await signAndExecuteTransaction({ transaction: certifyTx })
66
+
67
+ const files = await flow.listFiles()
68
+ console.log('Uploaded blob ID:', files[0].blobId)
69
+ ```
70
+
71
+ ## API Reference
72
+
73
+ ### Client Factory
74
+
75
+ **`createWalrusClient(options?): WalrusClient`**
76
+
77
+ Creates and configures a Walrus client. Called once per session.
78
+
79
+ ```typescript
80
+ const client = createWalrusClient({
81
+ network: 'testnet', // 'testnet' | 'mainnet' (default: 'testnet')
82
+ rpcUrl: '...', // Optional: override the Sui fullnode URL
83
+ wasmUrl: '...', // Required for browser/Vite; ignored in Node.js
84
+ uploadRelayHost: '...', // Optional upload relay URL
85
+ uploadRelayAuthToken: '...', // Optional Bearer token for NFT-gated relay access
86
+ uploadRelayMaxTipMist: 1000000, // Optional tip max in MIST (default: 1_000_000)
87
+ disableUploadRelay: false, // Bypass the relay entirely (direct to storage nodes)
88
+ })
89
+ ```
90
+
91
+ ### Upload (Node.js & Browser)
92
+
93
+ **`uploadBytes(client, contents, identifier, signer, options?): Promise<UploadResult>`**
94
+
95
+ Core upload function (stores a quilt). Works in Node.js and browser.
96
+
97
+ **`uploadLocalFile(client, filePath, identifier, signer, options?): Promise<UploadResult>`**
98
+
99
+ Node.js only. Reads a file from disk and uploads it.
100
+
101
+ **`createUploadFlow(client, contents, identifier, options?): WriteFilesFlow`**
102
+
103
+ Browser only. Returns a multi-step flow for wallet-popup-safe signing.
104
+
105
+ **`uploadImageBytes(client, contents, signer, options?): Promise<UploadResult>`** /
106
+ **`createBlobUploadFlow(client, contents): WriteBlobFlow`**
107
+
108
+ Raw-blob variants (Node.js / browser). Use these for assets that must be served directly by their blob URL (e.g. an image rendered by wallets/explorers): `GET /v1/blobs/<blobId>` returns the exact bytes. `uploadBytes`/`createUploadFlow` store a quilt instead, whose blob id does not resolve to the raw file.
109
+
110
+ **Options:**
111
+
112
+ ```typescript
113
+ {
114
+ epochs?: number // Number of epochs to store (default: MAX_SINGLE_RESERVATION_EPOCHS)
115
+ deletable?: boolean // Deletable by owner (default: false)
116
+ tags?: Record<string, string> // Optional metadata tags
117
+ }
118
+ ```
119
+
120
+ **Constants:**
121
+
122
+ ```typescript
123
+ export const MAX_SINGLE_RESERVATION_EPOCHS = 53 // Walrus `max_epochs_ahead`
124
+ export const LONG_TERM_EPOCHS = 200 // ~7.7 years target (needs renewal)
125
+ ```
126
+
127
+ ### Lifetime Extension
128
+
129
+ **`extendBlobLifetime(client, blobObjectId, signer, options): Promise<{ digest: string }>`**
130
+
131
+ Extend a blob's storage lifetime before expiry.
132
+
133
+ **`extendBlobLifetimeTransaction(client, blobObjectId, options): Transaction`**
134
+
135
+ Browser variant. Returns a transaction for wallet signing.
136
+
137
+ **Options:**
138
+
139
+ ```typescript
140
+ { epochs: number } // Add N more epochs
141
+ // OR
142
+ { endEpoch: number } // Extend to a specific Sui epoch number
143
+ ```
144
+
145
+ ### Metadata (Attributes)
146
+
147
+ **`setBlobAttributes(client, blobObjectId, signer, attributes): Promise<{ digest: string }>`**
148
+
149
+ Set or update on-chain key-value metadata.
150
+
151
+ **`setBlobAttributesTransaction(client, blobObjectId, attributes): Transaction`**
152
+
153
+ Browser variant. Returns a transaction for wallet signing.
154
+
155
+ **`readBlobAttributes(client, blobObjectId): Promise<Record<string, string> | null>`**
156
+
157
+ Read current attributes.
158
+
159
+ ### Query
160
+
161
+ **`fetchOwnedWalrusBlobs(suiClient, walrusClient, owner): Promise<OwnedBlob[]>`**
162
+
163
+ Enumerate all Walrus blobs owned by an address (resolves the Blob struct type dynamically, so no package addresses are hardcoded). Returns `{ objectId, blobId, size, endEpoch, certified }` entries.
164
+
165
+ ### NFT-gated relay access
166
+
167
+ When the upload relay is protected by an `nft-gate` auth gateway, the caller must present a wallet-signed access proof. Use `createRelayAccessToken` to produce the Bearer token:
168
+
169
+ ```typescript
170
+ import { createRelayAccessToken, createWalrusClient } from '@meddleware/walrus-client'
171
+
172
+ const token = await createRelayAccessToken({
173
+ relayHost: 'https://sui-walrus-relay-testnet.meddleware.co.uk',
174
+ address: walletAddress,
175
+ sign: (msg) => wallet.signPersonalMessage({ message: msg }),
176
+ })
177
+
178
+ const client = createWalrusClient({
179
+ network: 'testnet',
180
+ uploadRelayAuthToken: token,
181
+ })
182
+ ```
183
+
184
+ ### Re-exports
185
+
186
+ - `WalrusFile` — Construct files from `Uint8Array`, `Blob`, or `string`
187
+ - `RetryableWalrusClientError` — For retry logic during epoch transitions
188
+
189
+ ### Exported constants
190
+
191
+ | Export | Description |
192
+ |---|---|
193
+ | `DEFAULT_RPC_URLS` | Default Sui fullnode URLs per network |
194
+ | `PUBLIC_UPLOAD_RELAY_HOSTS` | Public Mysten-operated upload relay hosts (default when no `uploadRelayHost` is passed) |
195
+ | `WALRUS_AGGREGATOR_HOSTS` | Public Walrus aggregator hosts |
196
+ | `TESTNET_WALRUS_PACKAGE_CONFIG` | Walrus package config for testnet |
197
+ | `MAINNET_WALRUS_PACKAGE_CONFIG` | Walrus package config for mainnet |
198
+
199
+ ## Prerequisites
200
+
201
+ ### Sui Keypair (Node.js)
202
+
203
+ A `Signer` from `@mysten/sui/cryptography` with sufficient SUI to cover:
204
+
205
+ - Registration transaction gas
206
+ - Certification transaction gas
207
+ - WAL token balance to pay for blob storage duration (in MIST)
208
+
209
+ ### Browser Wallet
210
+
211
+ A connected Sui wallet (`@mysten/dapp-kit-core`, Sui Wallet, Movemen) to sign transactions.
212
+
213
+ ### Network
214
+
215
+ - **Testnet:** Walrus testnet storage nodes; standard Sui testnet RPC
216
+ - **Mainnet:** Walrus mainnet storage nodes; standard Sui mainnet RPC
217
+
218
+ Network endpoints are defined in `src/client.ts` and exported as `DEFAULT_RPC_URLS`.
219
+
220
+ ### Upload relay
221
+
222
+ `createWalrusClient` falls back to the MeddleWare upload relay when no `uploadRelayHost` is supplied. An upload relay is REQUIRED for browser uploads (direct-to-storage-node writes fail from browsers). To upload directly to Walrus storage nodes with **no** relay (e.g. from Node.js, or when the relay infra is unavailable), pass `disableUploadRelay: true`:
223
+
224
+ ```ts
225
+ const client = createWalrusClient({ network: 'testnet', disableUploadRelay: true })
226
+ ```
227
+
228
+ `disableUploadRelay` overrides both an explicit `uploadRelayHost` and the default fallback. To point at a public Mysten relay instead, use `PUBLIC_UPLOAD_RELAY_HOSTS[network]` as the `uploadRelayHost`.
229
+
230
+ ## Testing
231
+
232
+ ```bash
233
+ npm run type-check # tsc --noEmit (types only)
234
+ npm test # vitest run (mocked-client unit tests)
235
+ ```
236
+
237
+ The unit suite (`tests/`) uses a mocked `WalrusClient` to assert the behaviour type-checking cannot: safe-default threading (`deletable:false`, default epochs), options passthrough, the `ExtendOptions` XOR, the wallet-safe `*Transaction`/flow variants, the `disableUploadRelay` path, and owned-blob parsing. `vitest` is a **dev-only** dependency; its transitive `esbuild` advisories affect only the local test dev-server and do not reach the shipped source package (runtime deps `@mysten/sui` / `@mysten/walrus` are tilde-pinned — run `npm ci`, not `npm install`, in CI/deploy).
238
+
239
+ **Opt-in testnet integration check** (not automated — requires a funded testnet `Signer` + WAL): upload a small blob via `uploadBytes`, read it back via `readBlobAttributes`, then `extendBlobLifetime`, and confirm the digest/attributes. Run manually before relying on the package in a deploy pipeline.
240
+
241
+ ## Maintenance
242
+
243
+ ### Before blobs expire
244
+
245
+ Blobs expire after their epoch count. A maintenance script should periodically extend critical blobs:
246
+
247
+ ```typescript
248
+ const client = createWalrusClient({ network: 'mainnet' })
249
+ const keypair = loadKeypair() // Your operational keypair
250
+
251
+ for (const blobId of criticalBlobs) {
252
+ const { digest } = await extendBlobLifetime(
253
+ client,
254
+ blobId,
255
+ keypair,
256
+ { epochs: 50 }, // Extend by another 50 epochs (<= MAX_SINGLE_RESERVATION_EPOCHS)
257
+ )
258
+ console.log('Extended blob', blobId, 'in tx', digest)
259
+ }
260
+ ```
261
+
262
+ Run this task on a schedule (e.g., weekly) to prevent expiry.
263
+
264
+ ## Errors
265
+
266
+ **`RetryableWalrusClientError`** is thrown during Sui epoch transitions when the client's cached state becomes stale.
267
+
268
+ ```typescript
269
+ try {
270
+ await uploadBytes(...)
271
+ } catch (error) {
272
+ if (error instanceof RetryableWalrusClientError) {
273
+ client.walrus.reset()
274
+ // Retry the operation
275
+ }
276
+ }
277
+ ```
278
+
279
+ ## Further reading
280
+
281
+ - [Walrus SDK documentation](https://docs.walrus.space/)
282
+ - [Sui TypeScript SDK](https://sui-typescript-docs.vercel.app/)
283
+ - Package notes: [CLAUDE.md](CLAUDE.md) · agent policy: [AGENTS.md](AGENTS.md)
284
+ - Self-hosted relay: canonical location `infrastructure/k8s/walrus-relay` in the vault monorepo
285
+
286
+ ## License
287
+
288
+ [0BSD](LICENSE)
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@meddleware/walrus-client",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Walrus decentralised storage client and asset management utilities for Sui applications.",
6
+ "author": "MeddleWare <meddleware@proton.me>",
7
+ "license": "0BSD",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/meddleware-org/walrus-client.git"
11
+ },
12
+ "keywords": ["walrus", "sui", "decentralised-storage", "web3"],
13
+ "files": [
14
+ "src",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "exports": {
18
+ ".": "./src/index.ts"
19
+ },
20
+ "scripts": {
21
+ "type-check": "tsc --noEmit",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest"
24
+ },
25
+ "dependencies": {
26
+ "@mysten/sui": "~2.17.0",
27
+ "@mysten/walrus": "~1.1.7"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "~24.12.2",
31
+ "typescript": "^6.0.0",
32
+ "vitest": "~2.1.9"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
package/src/access.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * NFT-gated relay access helpers.
3
+ *
4
+ * When the MeddleWare upload relay runs behind an `nft-gate` auth gateway, an upload must
5
+ * carry a wallet-signed **access proof** proving the caller holds the required access NFT.
6
+ * These helpers build that proof; the resulting token is passed to
7
+ * `createWalrusClient({ uploadRelayAuthToken })`, which threads it as the relay
8
+ * `Authorization: Bearer` header (see `client.ts`).
9
+ *
10
+ * The wire format mirrors `@meddleware/nft-gate-client` (challenge, signed message, and
11
+ * base64-JSON proof token). It is duplicated here — rather than depended upon — so this
12
+ * package stays self-contained; the gateway (`nft-gate-gateway`) is the authority that
13
+ * verifies these proofs.
14
+ *
15
+ * TODO(unify): once `@meddleware/nft-gate-client` is published (from the `nft-gate`
16
+ * standalone workspace, canonical vault location `blockchain/sui/packages/nft-gate-client-sui/`),
17
+ * import `fetchChallenge`/`buildAccessProof`/`personalMessageForNonce` from it and delete the
18
+ * duplicated logic below (keep this module as the thin Walrus-facing wrapper).
19
+ */
20
+
21
+ /** A server-issued, time-bound challenge from the gateway's `GET /v1/challenge`. */
22
+ export interface RelayChallenge {
23
+ nonce: string
24
+ expiresAt: number
25
+ }
26
+
27
+ /** The proof payload; `consumeDigest` is present only for single-use gates. */
28
+ export interface AccessProofInput {
29
+ address: string
30
+ nonce: string
31
+ signature: string
32
+ consumeDigest?: string
33
+ }
34
+
35
+ /** A wallet personal-message signer (e.g. wallet-standard `sui:signPersonalMessage`). */
36
+ export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature: string }>
37
+
38
+ /** The exact bytes the wallet signs for a nonce. MUST match the gateway's derivation. */
39
+ export function personalMessageForNonce(nonce: string): Uint8Array {
40
+ return new TextEncoder().encode(`nft-gate:access:${nonce}`)
41
+ }
42
+
43
+ function toBase64(s: string): string {
44
+ return typeof btoa === 'function' ? btoa(s) : Buffer.from(s, 'utf-8').toString('base64')
45
+ }
46
+
47
+ /** Encode a proof as the base64(JSON) Bearer token the relay auth header carries. */
48
+ export function buildAccessProofToken(proof: AccessProofInput): string {
49
+ const payload: AccessProofInput = {
50
+ address: proof.address,
51
+ nonce: proof.nonce,
52
+ signature: proof.signature,
53
+ }
54
+ if (proof.consumeDigest) payload.consumeDigest = proof.consumeDigest
55
+ return toBase64(JSON.stringify(payload))
56
+ }
57
+
58
+ /** Fetch a fresh challenge from the gateway. Tolerates `expiresAt` or `expires_at`. */
59
+ export async function fetchRelayChallenge(
60
+ relayHost: string,
61
+ opts: { signal?: AbortSignal } = {},
62
+ ): Promise<RelayChallenge> {
63
+ const res = await fetch(`${relayHost.replace(/\/$/, '')}/v1/challenge`, { signal: opts.signal })
64
+ if (!res.ok) throw new Error(`challenge request failed: ${res.status}`)
65
+ const data = (await res.json()) as { nonce?: string; expiresAt?: number; expires_at?: number }
66
+ if (!data || typeof data.nonce !== 'string') throw new Error('challenge response missing nonce')
67
+ return { nonce: data.nonce, expiresAt: data.expiresAt ?? data.expires_at ?? 0 }
68
+ }
69
+
70
+ /**
71
+ * One-shot: fetch a challenge, sign it with the wallet, and return the token to pass as
72
+ * `createWalrusClient({ uploadRelayAuthToken })`. For single-use gates, supply the
73
+ * `consumeDigest` of the on-chain consume transaction first.
74
+ */
75
+ export async function createRelayAccessToken(opts: {
76
+ relayHost: string
77
+ address: string
78
+ sign: PersonalMessageSigner
79
+ consumeDigest?: string
80
+ signal?: AbortSignal
81
+ }): Promise<string> {
82
+ const challenge = await fetchRelayChallenge(opts.relayHost, { signal: opts.signal })
83
+ const message = personalMessageForNonce(challenge.nonce)
84
+ const { signature } = await opts.sign(message)
85
+ return buildAccessProofToken({
86
+ address: opts.address,
87
+ nonce: challenge.nonce,
88
+ signature,
89
+ consumeDigest: opts.consumeDigest,
90
+ })
91
+ }
package/src/client.ts ADDED
@@ -0,0 +1,95 @@
1
+ import { SuiGrpcClient } from '@mysten/sui/grpc'
2
+ import { walrus, TESTNET_WALRUS_PACKAGE_CONFIG, MAINNET_WALRUS_PACKAGE_CONFIG } from '@mysten/walrus'
3
+
4
+ export { TESTNET_WALRUS_PACKAGE_CONFIG, MAINNET_WALRUS_PACKAGE_CONFIG }
5
+
6
+ export type WalrusNetwork = 'testnet' | 'mainnet'
7
+
8
+ export const DEFAULT_RPC_URLS: Record<WalrusNetwork, string> = {
9
+ testnet: 'https://fullnode.testnet.sui.io:443',
10
+ mainnet: 'https://fullnode.mainnet.sui.io:443',
11
+ }
12
+
13
+ /**
14
+ * Public Mysten-operated upload relays. Used as the default when no `uploadRelayHost`
15
+ * is specified. An upload relay is REQUIRED for browser uploads — direct-to-storage-node
16
+ * writes fail from browsers and constrained networks. Operators who run their own relay
17
+ * should pass `uploadRelayHost` to `createWalrusClient` instead.
18
+ */
19
+ export const PUBLIC_UPLOAD_RELAY_HOSTS: Record<WalrusNetwork, string> = {
20
+ testnet: 'https://upload-relay.testnet.walrus.space',
21
+ mainnet: 'https://upload-relay.mainnet.walrus.space',
22
+ }
23
+
24
+ /** Public Walrus aggregators (serve raw blob bytes at `/v1/blobs/<blobId>`). */
25
+ export const WALRUS_AGGREGATOR_HOSTS: Record<WalrusNetwork, string> = {
26
+ testnet: 'https://aggregator.walrus-testnet.walrus.space',
27
+ mainnet: 'https://aggregator.walrus-mainnet.walrus.space',
28
+ }
29
+
30
+ /**
31
+ * Public URL that serves a RAW blob's bytes (e.g. a token icon). Only meaningful
32
+ * for blobs written with `uploadImageBytes` / `writeBlob` (raw), NOT quilts.
33
+ */
34
+ export function walrusBlobUrl(
35
+ network: WalrusNetwork,
36
+ blobId: string,
37
+ aggregatorHost?: string,
38
+ ): string {
39
+ const host = aggregatorHost ?? WALRUS_AGGREGATOR_HOSTS[network]
40
+ return `${host}/v1/blobs/${blobId}`
41
+ }
42
+
43
+ export function getWalrusPackageConfig(network: WalrusNetwork) {
44
+ return network === 'mainnet' ? MAINNET_WALRUS_PACKAGE_CONFIG : TESTNET_WALRUS_PACKAGE_CONFIG
45
+ }
46
+
47
+ export type CreateWalrusClientOptions = {
48
+ network?: WalrusNetwork
49
+ /** Override the Sui JSON-RPC/gRPC fullnode URL. Defaults to the public Mysten endpoint for the network. */
50
+ rpcUrl?: string
51
+ wasmUrl?: string
52
+ uploadRelayHost?: string
53
+ uploadRelayAuthToken?: string
54
+ uploadRelayMaxTipMist?: number
55
+ /**
56
+ * When true, build a client with NO upload relay (direct-to-storage-node).
57
+ * Overrides `uploadRelayHost` and the default MeddleWare relay fallback, so a
58
+ * deployer is never hard-blocked on relay infra. Uploads then talk directly to
59
+ * Walrus storage nodes.
60
+ */
61
+ disableUploadRelay?: boolean
62
+ }
63
+
64
+ export function createWalrusClient({
65
+ network = 'testnet',
66
+ rpcUrl,
67
+ wasmUrl,
68
+ uploadRelayHost,
69
+ uploadRelayAuthToken,
70
+ uploadRelayMaxTipMist = 1_000_000,
71
+ disableUploadRelay = false,
72
+ }: CreateWalrusClientOptions = {}) {
73
+ const baseUrl = rpcUrl ?? DEFAULT_RPC_URLS[network]
74
+ // disableUploadRelay wins over both an explicit host and the default fallback.
75
+ const relayHost = disableUploadRelay ? undefined : (uploadRelayHost ?? PUBLIC_UPLOAD_RELAY_HOSTS[network])
76
+ return new SuiGrpcClient({
77
+ network,
78
+ baseUrl,
79
+ }).$extend(
80
+ walrus({
81
+ ...(wasmUrl ? { wasmUrl } : {}),
82
+ ...(relayHost
83
+ ? {
84
+ uploadRelay: {
85
+ host: relayHost,
86
+ sendTip: { max: uploadRelayMaxTipMist },
87
+ ...(uploadRelayAuthToken
88
+ ? { headers: { Authorization: `Bearer ${uploadRelayAuthToken}` } }
89
+ : {}),
90
+ },
91
+ }
92
+ : {}),
93
+ }),
94
+ )
95
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ export {
2
+ createWalrusClient,
3
+ getWalrusPackageConfig,
4
+ walrusBlobUrl,
5
+ DEFAULT_RPC_URLS,
6
+ PUBLIC_UPLOAD_RELAY_HOSTS,
7
+ WALRUS_AGGREGATOR_HOSTS,
8
+ TESTNET_WALRUS_PACKAGE_CONFIG,
9
+ MAINNET_WALRUS_PACKAGE_CONFIG,
10
+ } from './client.js'
11
+ export type { CreateWalrusClientOptions, WalrusNetwork } from './client.js'
12
+
13
+ export {
14
+ LONG_TERM_EPOCHS,
15
+ uploadBytes,
16
+ uploadLocalFile,
17
+ createUploadFlow,
18
+ uploadImageBytes,
19
+ createBlobUploadFlow,
20
+ } from './upload.js'
21
+ export type { WalrusClient, UploadOptions, UploadResult } from './upload.js'
22
+
23
+ export {
24
+ extendBlobLifetime,
25
+ extendBlobLifetimeTransaction,
26
+ setBlobAttributes,
27
+ setBlobAttributesTransaction,
28
+ readBlobAttributes,
29
+ } from './manage.js'
30
+ export type { ExtendOptions } from './manage.js'
31
+
32
+ export { fetchOwnedWalrusBlobs } from './query.js'
33
+ export type { OwnedBlob } from './query.js'
34
+
35
+ export {
36
+ personalMessageForNonce,
37
+ buildAccessProofToken,
38
+ fetchRelayChallenge,
39
+ createRelayAccessToken,
40
+ } from './access.js'
41
+ export type { RelayChallenge, AccessProofInput, PersonalMessageSigner } from './access.js'
42
+
43
+ export { WalrusFile, RetryableWalrusClientError } from '@mysten/walrus'
package/src/manage.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type { Signer } from '@mysten/sui/cryptography'
2
+ import type { createWalrusClient } from './client.js'
3
+
4
+ /** A Walrus-extended Sui client, as returned by {@link createWalrusClient}. */
5
+ export type WalrusClient = ReturnType<typeof createWalrusClient>
6
+
7
+ /**
8
+ * How to extend a blob's storage: either add `epochs` more, or extend up to an
9
+ * absolute `endEpoch`. Exactly one of the two must be provided.
10
+ */
11
+ export type ExtendOptions =
12
+ | { epochs: number; endEpoch?: never }
13
+ | { endEpoch: number; epochs?: never }
14
+
15
+ /**
16
+ * Extend a blob's storage lifetime on-chain (Node.js — signs and executes).
17
+ * Since Walrus blobs expire, call this before expiry to prevent data loss.
18
+ *
19
+ * @param client A Walrus-extended client.
20
+ * @param blobObjectId The on-chain Blob object id (not the blob id).
21
+ * @param signer The keypair paying for and authorising the extension.
22
+ * @param options Add `epochs` more, or extend to an absolute `endEpoch`.
23
+ * @returns The executed transaction digest.
24
+ */
25
+ export async function extendBlobLifetime(
26
+ client: WalrusClient,
27
+ blobObjectId: string,
28
+ signer: Signer,
29
+ options: ExtendOptions,
30
+ ): Promise<{ digest: string }> {
31
+ const result = await client.walrus.executeExtendBlobTransaction({
32
+ blobObjectId,
33
+ signer,
34
+ ...options,
35
+ })
36
+ return { digest: result.digest }
37
+ }
38
+
39
+ /**
40
+ * Build (but do not sign) a blob-lifetime-extension transaction for a browser
41
+ * wallet to sign. Browser counterpart of {@link extendBlobLifetime}.
42
+ *
43
+ * @param client A Walrus-extended client.
44
+ * @param blobObjectId The on-chain Blob object id.
45
+ * @param options Add `epochs` more, or extend to an absolute `endEpoch`.
46
+ * @returns An unsigned `Transaction`.
47
+ */
48
+ export function extendBlobLifetimeTransaction(
49
+ client: WalrusClient,
50
+ blobObjectId: string,
51
+ options: ExtendOptions,
52
+ ) {
53
+ return client.walrus.extendBlobTransaction({ blobObjectId, ...options })
54
+ }
55
+
56
+ /**
57
+ * Set on-chain key/value attributes on a blob (Node.js — signs and executes). A
58
+ * `null` value deletes that attribute.
59
+ *
60
+ * @param client A Walrus-extended client.
61
+ * @param blobObjectId The on-chain Blob object id.
62
+ * @param signer The keypair authorising the write.
63
+ * @param attributes Attributes to set; `null` deletes the key.
64
+ * @returns The executed transaction digest.
65
+ */
66
+ export async function setBlobAttributes(
67
+ client: WalrusClient,
68
+ blobObjectId: string,
69
+ signer: Signer,
70
+ attributes: Record<string, string | null>,
71
+ ): Promise<{ digest: string }> {
72
+ const result = await client.walrus.executeWriteBlobAttributesTransaction({
73
+ blobObjectId,
74
+ signer,
75
+ attributes,
76
+ })
77
+ return { digest: result.digest }
78
+ }
79
+
80
+ /**
81
+ * Build (but do not sign) a set-blob-attributes transaction for a browser wallet
82
+ * to sign. Browser counterpart of {@link setBlobAttributes}.
83
+ *
84
+ * @param client A Walrus-extended client.
85
+ * @param blobObjectId The on-chain Blob object id.
86
+ * @param attributes Attributes to set; `null` deletes the key.
87
+ * @returns An unsigned `Transaction`.
88
+ */
89
+ export function setBlobAttributesTransaction(
90
+ client: WalrusClient,
91
+ blobObjectId: string,
92
+ attributes: Record<string, string | null>,
93
+ ) {
94
+ return client.walrus.writeBlobAttributesTransaction({ blobObjectId, attributes })
95
+ }
96
+
97
+ /**
98
+ * Read a blob's on-chain attributes.
99
+ *
100
+ * @param client A Walrus-extended client.
101
+ * @param blobObjectId The on-chain Blob object id.
102
+ * @returns The attribute map, or `null` if the blob has none.
103
+ */
104
+ export async function readBlobAttributes(
105
+ client: WalrusClient,
106
+ blobObjectId: string,
107
+ ): Promise<Record<string, string> | null> {
108
+ return client.walrus.readBlobAttributes({ blobObjectId })
109
+ }
package/src/query.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { blobIdFromInt } from '@mysten/walrus'
2
+ import type { SuiJsonRpcClient } from '@mysten/sui/jsonRpc'
3
+ import type { WalrusClient } from './upload.js'
4
+
5
+ /** A Walrus blob object owned by a Sui address. */
6
+ export type OwnedBlob = {
7
+ /** The on-chain Blob object id. */
8
+ objectId: string
9
+ /** Aggregator-URL-compatible blob id string (`GET /v1/blobs/<blobId>`). */
10
+ blobId: string
11
+ /** Size in bytes. */
12
+ size: number
13
+ /** Epoch at which the blob's storage expires. */
14
+ endEpoch: number
15
+ /** Whether the blob has been certified. */
16
+ certified: boolean
17
+ }
18
+
19
+ /**
20
+ * Return all Walrus blobs owned by `owner`. Resolves the on-chain Blob struct
21
+ * type dynamically via `getBlobType()`, so no package addresses are hardcoded.
22
+ *
23
+ * @param suiClient A JSON-RPC Sui client (provides `getOwnedObjects`).
24
+ * @param walrusClient A Walrus-extended client (see {@link createWalrusClient}).
25
+ * @param owner The address whose owned blobs to enumerate.
26
+ * @returns The owner's blobs; entries whose fields cannot be parsed are skipped.
27
+ */
28
+ export async function fetchOwnedWalrusBlobs(
29
+ suiClient: SuiJsonRpcClient,
30
+ walrusClient: WalrusClient,
31
+ owner: string,
32
+ ): Promise<OwnedBlob[]> {
33
+ const blobType = await walrusClient.walrus.getBlobType()
34
+ const { data } = await suiClient.getOwnedObjects({
35
+ owner,
36
+ filter: { StructType: blobType },
37
+ options: { showContent: true },
38
+ })
39
+ const blobs: OwnedBlob[] = []
40
+ for (const item of data) {
41
+ if (!item.data) continue
42
+ const fields = (item.data.content as { fields?: Record<string, unknown> } | undefined)?.fields
43
+ if (!fields) continue
44
+ const storage = fields.storage as { fields?: { end_epoch?: unknown } } | undefined
45
+ try {
46
+ blobs.push({
47
+ objectId: item.data.objectId,
48
+ blobId: blobIdFromInt(BigInt(fields.blob_id as string)),
49
+ size: Number(fields.size),
50
+ endEpoch: Number(storage?.fields?.end_epoch ?? 0),
51
+ certified: fields.certified_epoch !== null && fields.certified_epoch !== undefined,
52
+ })
53
+ } catch {
54
+ // Skip blobs whose fields cannot be parsed.
55
+ }
56
+ }
57
+ return blobs
58
+ }
package/src/upload.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { WalrusFile } from '@mysten/walrus'
3
+ import type { Signer } from '@mysten/sui/cryptography'
4
+ import type { createWalrusClient } from './client.js'
5
+
6
+ export type WalrusClient = ReturnType<typeof createWalrusClient>
7
+
8
+ /**
9
+ * Maximum epochs a SINGLE Walrus reservation accepts (`max_epochs_ahead` on the
10
+ * Walrus system object; 53 on testnet/mainnet). Passing more to `writeBlob` /
11
+ * `writeFiles` aborts on-chain (`reserve_space`, MoveAbort code 2). ~2 years at
12
+ * the ~2-week epoch cadence.
13
+ */
14
+ export const MAX_SINGLE_RESERVATION_EPOCHS = 53
15
+
16
+ /**
17
+ * Recommended TARGET lifetime for critical assets (~7.7 years at a ~2-week epoch
18
+ * cadence). NOTE: this cannot be reached in one reservation — it exceeds
19
+ * {@link MAX_SINGLE_RESERVATION_EPOCHS}. Reaching it requires periodic renewal via
20
+ * {@link extendBlobLifetime} before expiry. When passing this as an initial
21
+ * `epochs` value, clamp to `MAX_SINGLE_RESERVATION_EPOCHS` first, or pass an
22
+ * explicit `epochs <= 53`.
23
+ */
24
+ export const LONG_TERM_EPOCHS = 200
25
+
26
+ export type UploadOptions = {
27
+ epochs?: number
28
+ deletable?: boolean
29
+ tags?: Record<string, string>
30
+ }
31
+
32
+ export type UploadResult = {
33
+ blobId: string
34
+ blobObjectId: string
35
+ }
36
+
37
+ export async function uploadBytes(
38
+ client: WalrusClient,
39
+ contents: Uint8Array,
40
+ identifier: string,
41
+ signer: Signer,
42
+ options: UploadOptions = {},
43
+ ): Promise<UploadResult> {
44
+ const file = WalrusFile.from({ contents, identifier, tags: options.tags })
45
+ const [result] = await client.walrus.writeFiles({
46
+ files: [file],
47
+ // Default to the largest reservation Walrus accepts; a caller wanting the
48
+ // LONG_TERM target must renew via extendBlobLifetime after this.
49
+ epochs: options.epochs ?? MAX_SINGLE_RESERVATION_EPOCHS,
50
+ deletable: options.deletable ?? false,
51
+ signer,
52
+ })
53
+ return { blobId: result.blobId, blobObjectId: result.id }
54
+ }
55
+
56
+ // Node.js only — reads a local file before uploading
57
+ export async function uploadLocalFile(
58
+ client: WalrusClient,
59
+ filePath: string,
60
+ identifier: string,
61
+ signer: Signer,
62
+ options: UploadOptions = {},
63
+ ): Promise<UploadResult> {
64
+ const contents = await readFile(filePath)
65
+ return uploadBytes(client, new Uint8Array(contents), identifier, signer, options)
66
+ }
67
+
68
+ // Browser — returns a multi-step flow for wallet-popup-safe signing
69
+ export function createUploadFlow(
70
+ client: WalrusClient,
71
+ contents: Uint8Array,
72
+ identifier: string,
73
+ options: UploadOptions = {},
74
+ ) {
75
+ const file = WalrusFile.from({ contents, identifier, tags: options.tags })
76
+ return client.walrus.writeFilesFlow({ files: [file] })
77
+ }
78
+
79
+ // ─── Raw blobs ───────────────────────────────────────────────────────────────
80
+ // `createUploadFlow` / `uploadBytes` store a QUILT (a bundle of files); reading
81
+ // the quilt blob id back returns the encoded quilt, not the file. For assets that
82
+ // must be served directly by their blob URL (e.g. a token icon rendered by
83
+ // wallets/explorers), use a RAW blob: `GET /v1/blobs/<blobId>` returns the exact
84
+ // bytes. See `walrusBlobUrl()` in ./client.
85
+
86
+ // Node.js — one-shot raw-blob upload with a keypair `Signer`.
87
+ export async function uploadImageBytes(
88
+ client: WalrusClient,
89
+ contents: Uint8Array,
90
+ signer: Signer,
91
+ options: UploadOptions = {},
92
+ ): Promise<UploadResult> {
93
+ const res = await client.walrus.writeBlob({
94
+ blob: contents,
95
+ // Default to the largest reservation Walrus accepts (see uploadBytes).
96
+ epochs: options.epochs ?? MAX_SINGLE_RESERVATION_EPOCHS,
97
+ deletable: options.deletable ?? false,
98
+ signer,
99
+ })
100
+ return { blobId: res.blobId, blobObjectId: res.blobObject.id }
101
+ }
102
+
103
+ // Browser — multi-step raw-blob flow for wallet-popup-safe signing. Drive it as:
104
+ // await flow.encode()
105
+ // const regTx = flow.register({ owner, epochs, deletable }) // wallet signs+executes
106
+ // await flow.upload({ digest }) // digest of regTx
107
+ // const certTx = flow.certify() // wallet signs+executes
108
+ // const { blobId } = await flow.getBlob() // -> walrusBlobUrl(...)
109
+ export function createBlobUploadFlow(client: WalrusClient, contents: Uint8Array) {
110
+ return client.walrus.writeBlobFlow({ blob: contents })
111
+ }