@meddleware/walrus-client 0.0.1 → 0.0.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/walrus-client",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "description": "Walrus decentralised storage client and asset management utilities for Sui applications.",
6
6
  "author": "MeddleWare <meddleware@proton.me>",
@@ -9,7 +9,12 @@
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/meddleware-org/walrus-client.git"
11
11
  },
12
- "keywords": ["walrus", "sui", "decentralised-storage", "web3"],
12
+ "keywords": [
13
+ "walrus",
14
+ "sui",
15
+ "decentralised-storage",
16
+ "web3"
17
+ ],
13
18
  "files": [
14
19
  "src",
15
20
  "CHANGELOG.md"
@@ -24,7 +29,8 @@
24
29
  },
25
30
  "dependencies": {
26
31
  "@mysten/sui": "~2.17.0",
27
- "@mysten/walrus": "~1.1.7"
32
+ "@mysten/walrus": "~1.1.7",
33
+ "@meddleware/nft-gate-client": "~0.0.3"
28
34
  },
29
35
  "devDependencies": {
30
36
  "@types/node": "~24.12.2",
@@ -34,4 +40,4 @@
34
40
  "publishConfig": {
35
41
  "access": "public"
36
42
  }
37
- }
43
+ }
package/src/access.ts CHANGED
@@ -1,76 +1,70 @@
1
1
  /**
2
2
  * NFT-gated relay access helpers.
3
3
  *
4
- * When the MeddleWare upload relay runs behind an `nft-gate` auth gateway, an upload must
4
+ * When the Meddleware upload relay runs behind an `nft-gate` auth gateway, an upload must
5
5
  * carry a wallet-signed **access proof** proving the caller holds the required access NFT.
6
6
  * These helpers build that proof; the resulting token is passed to
7
7
  * `createWalrusClient({ uploadRelayAuthToken })`, which threads it as the relay
8
8
  * `Authorization: Bearer` header (see `client.ts`).
9
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).
10
+ * Core primitives are imported from `@meddleware/nft-gate-client` (the canonical package).
11
+ * This module re-exports them under Walrus-friendly aliases and adds the one-shot
12
+ * `createRelayAccessToken` convenience function.
19
13
  */
20
14
 
15
+ import {
16
+ type Challenge,
17
+ type AccessProof,
18
+ type PersonalMessageSigner,
19
+ personalMessageForNonce,
20
+ encodeAccessProof,
21
+ fetchChallenge,
22
+ } from '@meddleware/nft-gate-client'
23
+
24
+ // ── Re-exports under Walrus-client aliases ────────────────────────────────────
25
+
21
26
  /** A server-issued, time-bound challenge from the gateway's `GET /v1/challenge`. */
22
- export interface RelayChallenge {
23
- nonce: string
24
- expiresAt: number
25
- }
27
+ export type RelayChallenge = Challenge
26
28
 
27
29
  /** 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
- }
30
+ export type AccessProofInput = AccessProof
34
31
 
35
32
  /** A wallet personal-message signer (e.g. wallet-standard `sui:signPersonalMessage`). */
36
- export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature: string }>
33
+ export type { PersonalMessageSigner }
37
34
 
38
35
  /** 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
- }
36
+ export { personalMessageForNonce }
42
37
 
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. */
38
+ /**
39
+ * Encode a proof as the base64(JSON) Bearer token the relay auth header carries.
40
+ *
41
+ * @throws {Error} if `JSON.stringify` or `btoa` is unavailable in the environment.
42
+ */
48
43
  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))
44
+ return encodeAccessProof(proof)
56
45
  }
57
46
 
58
- /** Fetch a fresh challenge from the gateway. Tolerates `expiresAt` or `expires_at`. */
59
- export async function fetchRelayChallenge(
47
+ /**
48
+ * Fetch a fresh challenge from the gateway's `GET /v1/challenge` endpoint.
49
+ * Tolerates both `expiresAt` (camelCase) and `expires_at` (snake_case) response shapes.
50
+ *
51
+ * @throws {Error} if the network request fails or the gateway returns a non-2xx status.
52
+ * @throws {Error} if the response body is missing the required `nonce` field.
53
+ */
54
+ export function fetchRelayChallenge(
60
55
  relayHost: string,
61
56
  opts: { signal?: AbortSignal } = {},
62
57
  ): 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 }
58
+ return fetchChallenge(relayHost, opts)
68
59
  }
69
60
 
70
61
  /**
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.
62
+ * One-shot: fetch a challenge, sign it with the wallet, and return the encoded proof token
63
+ * to pass as `createWalrusClient({ uploadRelayAuthToken })`. For single-use gates, supply
64
+ * the `consumeDigest` of the on-chain consume transaction.
65
+ *
66
+ * @throws {Error} if the challenge fetch fails.
67
+ * @throws {Error} if the wallet signer rejects the message.
74
68
  */
75
69
  export async function createRelayAccessToken(opts: {
76
70
  relayHost: string
@@ -79,13 +73,13 @@ export async function createRelayAccessToken(opts: {
79
73
  consumeDigest?: string
80
74
  signal?: AbortSignal
81
75
  }): Promise<string> {
82
- const challenge = await fetchRelayChallenge(opts.relayHost, { signal: opts.signal })
76
+ const challenge = await fetchChallenge(opts.relayHost, { signal: opts.signal })
83
77
  const message = personalMessageForNonce(challenge.nonce)
84
78
  const { signature } = await opts.sign(message)
85
- return buildAccessProofToken({
79
+ return encodeAccessProof({
86
80
  address: opts.address,
87
81
  nonce: challenge.nonce,
88
82
  signature,
89
- consumeDigest: opts.consumeDigest,
83
+ ...(opts.consumeDigest ? { consumeDigest: opts.consumeDigest } : {}),
90
84
  })
91
85
  }
package/src/client.ts CHANGED
@@ -3,8 +3,10 @@ import { walrus, TESTNET_WALRUS_PACKAGE_CONFIG, MAINNET_WALRUS_PACKAGE_CONFIG }
3
3
 
4
4
  export { TESTNET_WALRUS_PACKAGE_CONFIG, MAINNET_WALRUS_PACKAGE_CONFIG }
5
5
 
6
+ /** Supported Walrus network environments. */
6
7
  export type WalrusNetwork = 'testnet' | 'mainnet'
7
8
 
9
+ /** Default Sui full-node RPC URLs, keyed by network. */
8
10
  export const DEFAULT_RPC_URLS: Record<WalrusNetwork, string> = {
9
11
  testnet: 'https://fullnode.testnet.sui.io:443',
10
12
  mainnet: 'https://fullnode.mainnet.sui.io:443',
@@ -40,27 +42,51 @@ export function walrusBlobUrl(
40
42
  return `${host}/v1/blobs/${blobId}`
41
43
  }
42
44
 
45
+ /** Return the Walrus on-chain package config for the given network. */
43
46
  export function getWalrusPackageConfig(network: WalrusNetwork) {
44
47
  return network === 'mainnet' ? MAINNET_WALRUS_PACKAGE_CONFIG : TESTNET_WALRUS_PACKAGE_CONFIG
45
48
  }
46
49
 
50
+ /** Options accepted by {@link createWalrusClient}. */
47
51
  export type CreateWalrusClientOptions = {
52
+ /** Target network (default `'testnet'`). */
48
53
  network?: WalrusNetwork
49
54
  /** Override the Sui JSON-RPC/gRPC fullnode URL. Defaults to the public Mysten endpoint for the network. */
50
55
  rpcUrl?: string
56
+ /** Optional WASM bundle URL for the Walrus WASM client. */
51
57
  wasmUrl?: string
58
+ /**
59
+ * Upload relay host URL. Defaults to the public Mysten relay for the network.
60
+ * Operators running their own relay (e.g. Meddleware's NFT-gated relay) should
61
+ * pass their relay URL here alongside `uploadRelayAuthToken`.
62
+ */
52
63
  uploadRelayHost?: string
64
+ /**
65
+ * Bearer token for the upload relay `Authorization` header. Obtain via
66
+ * `createRelayAccessToken` from the `access` module when the relay is NFT-gated.
67
+ */
53
68
  uploadRelayAuthToken?: string
69
+ /**
70
+ * Maximum tip payment to the upload relay in MIST (default 1,000,000 = 0.001 SUI).
71
+ * The relay may request less; this cap prevents the client from overpaying.
72
+ */
54
73
  uploadRelayMaxTipMist?: number
55
74
  /**
56
75
  * 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.
76
+ * Overrides `uploadRelayHost` and the default relay fallback. Useful for
77
+ * server-side Node.js scripts where relay infra is unnecessary.
60
78
  */
61
79
  disableUploadRelay?: boolean
62
80
  }
63
81
 
82
+ /**
83
+ * Create a Walrus client pre-configured for the given network and relay options.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * const client = createWalrusClient({ network: 'testnet' })
88
+ * ```
89
+ */
64
90
  export function createWalrusClient({
65
91
  network = 'testnet',
66
92
  rpcUrl,
package/src/upload.ts CHANGED
@@ -3,6 +3,7 @@ import { WalrusFile } from '@mysten/walrus'
3
3
  import type { Signer } from '@mysten/sui/cryptography'
4
4
  import type { createWalrusClient } from './client.js'
5
5
 
6
+ /** The return type of {@link createWalrusClient}. */
6
7
  export type WalrusClient = ReturnType<typeof createWalrusClient>
7
8
 
8
9
  /**
@@ -23,17 +24,30 @@ export const MAX_SINGLE_RESERVATION_EPOCHS = 53
23
24
  */
24
25
  export const LONG_TERM_EPOCHS = 200
25
26
 
27
+ /** Options shared by all upload helpers. */
26
28
  export type UploadOptions = {
29
+ /** Storage epochs to reserve (default: {@link MAX_SINGLE_RESERVATION_EPOCHS}). */
27
30
  epochs?: number
31
+ /** Whether the blob can be deleted by its owner (default false). */
28
32
  deletable?: boolean
33
+ /** Arbitrary key-value metadata tags stored with the blob. */
29
34
  tags?: Record<string, string>
30
35
  }
31
36
 
37
+ /** The object IDs returned after a successful upload. */
32
38
  export type UploadResult = {
39
+ /** Walrus content-addressed blob ID. */
33
40
  blobId: string
41
+ /** On-chain Walrus blob object ID. */
34
42
  blobObjectId: string
35
43
  }
36
44
 
45
+ /**
46
+ * Upload raw bytes as a Walrus quilt (file bundle). Use this from Node.js with a keypair
47
+ * signer; use {@link createUploadFlow} in the browser for wallet-popup-safe signing.
48
+ *
49
+ * @throws {Error} if the Walrus write transaction fails or the signer rejects.
50
+ */
37
51
  export async function uploadBytes(
38
52
  client: WalrusClient,
39
53
  contents: Uint8Array,
@@ -53,7 +67,11 @@ export async function uploadBytes(
53
67
  return { blobId: result.blobId, blobObjectId: result.id }
54
68
  }
55
69
 
56
- // Node.js only — reads a local file before uploading
70
+ /**
71
+ * Read a local file and upload it as a Walrus quilt. Node.js only.
72
+ *
73
+ * @throws {Error} if the file cannot be read or the upload fails.
74
+ */
57
75
  export async function uploadLocalFile(
58
76
  client: WalrusClient,
59
77
  filePath: string,
@@ -65,7 +83,10 @@ export async function uploadLocalFile(
65
83
  return uploadBytes(client, new Uint8Array(contents), identifier, signer, options)
66
84
  }
67
85
 
68
- // Browser — returns a multi-step flow for wallet-popup-safe signing
86
+ /**
87
+ * Begin a multi-step quilt upload flow suitable for browser wallet signing.
88
+ * The caller drives the flow: encode → register (wallet signs) → upload → certify (wallet signs).
89
+ */
69
90
  export function createUploadFlow(
70
91
  client: WalrusClient,
71
92
  contents: Uint8Array,
@@ -83,7 +104,12 @@ export function createUploadFlow(
83
104
  // wallets/explorers), use a RAW blob: `GET /v1/blobs/<blobId>` returns the exact
84
105
  // bytes. See `walrusBlobUrl()` in ./client.
85
106
 
86
- // Node.js — one-shot raw-blob upload with a keypair `Signer`.
107
+ /**
108
+ * Upload raw bytes as a Walrus raw blob (served directly at `/v1/blobs/<blobId>`).
109
+ * Use this for assets that must be URL-addressable (e.g. token icons). Node.js only.
110
+ *
111
+ * @throws {Error} if the Walrus write transaction fails or the signer rejects.
112
+ */
87
113
  export async function uploadImageBytes(
88
114
  client: WalrusClient,
89
115
  contents: Uint8Array,
@@ -100,12 +126,18 @@ export async function uploadImageBytes(
100
126
  return { blobId: res.blobId, blobObjectId: res.blobObject.id }
101
127
  }
102
128
 
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(...)
129
+ /**
130
+ * Begin a multi-step raw-blob upload flow suitable for browser wallet signing.
131
+ *
132
+ * Drive the returned flow:
133
+ * ```ts
134
+ * await flow.encode()
135
+ * const regTx = flow.register({ owner, epochs, deletable }) // wallet signs + executes
136
+ * await flow.upload({ digest }) // digest of regTx
137
+ * const certTx = flow.certify() // wallet signs + executes
138
+ * const { blobId } = await flow.getBlob() // use walrusBlobUrl(network, blobId)
139
+ * ```
140
+ */
109
141
  export function createBlobUploadFlow(client: WalrusClient, contents: Uint8Array) {
110
142
  return client.walrus.writeBlobFlow({ blob: contents })
111
143
  }