@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 +22 -0
- package/LICENSE +14 -0
- package/README.md +288 -0
- package/package.json +37 -0
- package/src/access.ts +91 -0
- package/src/client.ts +95 -0
- package/src/index.ts +43 -0
- package/src/manage.ts +109 -0
- package/src/query.ts +58 -0
- package/src/upload.ts +111 -0
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)
|
|
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
|
+
}
|