@meddleware/nft-gate-client 0.0.1 → 0.0.3

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 CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.3] - 2026-08-29
9
+
10
+ ### Added
11
+
12
+ - Gate administration PTB builders (AdminCap-gated): `buildSetPriceTx`, `buildSetPaymentRecipientTx`, `buildSetPausedTx`, `buildSetDefaultUsesTx`, `buildSetSoulboundTx`, `buildSetAutoBurnAtZeroTx`, `buildSetNftNameTx`, `buildSetNftImageUrlTx`, `buildSetNftDescriptionTx`, `buildAirdropTx`, `buildMakeGateImmutableTx`
13
+ - `GateAdminContext` type (`{ packageId, gateId, adminCapId }`) for the management builders
14
+ - Gate discovery helpers: `fetchOwnedGates`, `fetchAdminCaps`, `fetchGate`, `parseAdminCap`, `parseGate`, and the `OwnedGate` type — list and read the gates an operator administers
15
+ - `@throws` annotations on the async ownership / challenge / proof functions
16
+
8
17
  ## [0.0.1] - 2026-08-27
9
18
 
10
19
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/nft-gate-client",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "Client-side helpers for the access_gate NFT access primitive: ownership queries, purchase/consume PTB builders, challenge signing, and access-proof assembly.",
6
6
  "author": "MeddleWare <meddleware@proton.me>",
@@ -9,7 +9,13 @@
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/meddleware-org/nft-gate-client.git"
11
11
  },
12
- "keywords": ["sui", "nft", "access-control", "web3", "move"],
12
+ "keywords": [
13
+ "sui",
14
+ "nft",
15
+ "access-control",
16
+ "web3",
17
+ "move"
18
+ ],
13
19
  "files": [
14
20
  "src",
15
21
  "CHANGELOG.md"
@@ -33,4 +39,4 @@
33
39
  "publishConfig": {
34
40
  "access": "public"
35
41
  }
36
- }
42
+ }
package/src/challenge.ts CHANGED
@@ -3,6 +3,9 @@ import type { Challenge } from './types.js'
3
3
  /**
4
4
  * Fetch a fresh challenge from a gateway's `GET /v1/challenge` endpoint. Tolerates both
5
5
  * `expiresAt` (camelCase) and `expires_at` (snake_case) response shapes.
6
+ *
7
+ * @throws {Error} if the network request fails or the gateway returns a non-2xx status.
8
+ * @throws {Error} if the response body is missing the required `nonce` field.
6
9
  */
7
10
  export async function fetchChallenge(
8
11
  gatewayHost: string,
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  export type {
9
9
  AccessGateConfig,
10
+ GateAdminContext,
11
+ OwnedGate,
10
12
  Challenge,
11
13
  AccessProof,
12
14
  OwnedAccessNft,
@@ -14,8 +16,33 @@ export type {
14
16
  SuiObjectClient,
15
17
  } from './types.js'
16
18
 
17
- export { fetchAccessNfts, ownsAccessNft, parseOwnedAccessNft, fetchAccessNftById } from './ownership.js'
18
- export { buildPurchaseTx, buildConsumeTx, buildCreateGateTx } from './ptb.js'
19
+ export {
20
+ fetchAccessNfts,
21
+ ownsAccessNft,
22
+ parseOwnedAccessNft,
23
+ fetchAccessNftById,
24
+ parseAdminCap,
25
+ parseGate,
26
+ fetchAdminCaps,
27
+ fetchGate,
28
+ fetchOwnedGates,
29
+ } from './ownership.js'
30
+ export {
31
+ buildPurchaseTx,
32
+ buildConsumeTx,
33
+ buildCreateGateTx,
34
+ buildSetPriceTx,
35
+ buildSetPaymentRecipientTx,
36
+ buildSetPausedTx,
37
+ buildSetDefaultUsesTx,
38
+ buildSetSoulboundTx,
39
+ buildSetAutoBurnAtZeroTx,
40
+ buildSetNftNameTx,
41
+ buildSetNftImageUrlTx,
42
+ buildSetNftDescriptionTx,
43
+ buildAirdropTx,
44
+ buildMakeGateImmutableTx,
45
+ } from './ptb.js'
19
46
  export { fetchChallenge } from './challenge.js'
20
47
  export {
21
48
  personalMessageForNonce,
package/src/ownership.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OwnedAccessNft, OwnedObjectsClient, SuiObjectClient } from './types.js'
1
+ import type { OwnedAccessNft, OwnedGate, OwnedObjectsClient, SuiObjectClient } from './types.js'
2
2
 
3
3
  /* eslint-disable @typescript-eslint/no-explicit-any */
4
4
 
@@ -50,6 +50,8 @@ export function parseOwnedAccessNft(entry: any): OwnedAccessNft | null {
50
50
  * Typed single-object read of one access NFT by id (`getObject` with `showType`+`showContent`),
51
51
  * used when a UI needs the **exact** `usesRemaining` reliably rather than the best-effort parse
52
52
  * of an owned-objects page. Returns `null` if the object is missing or not an access NFT.
53
+ *
54
+ * @throws {Error} if the RPC call fails at the network or transport layer.
53
55
  */
54
56
  export async function fetchAccessNftById(
55
57
  client: SuiObjectClient,
@@ -65,6 +67,8 @@ export async function fetchAccessNftById(
65
67
  /**
66
68
  * Fetch all access NFTs of `nftType` owned by `owner`, optionally restricted to a specific
67
69
  * `gateId`. Uses `getOwnedObjects` filtered by `StructType` (the standard owned-objects query).
70
+ *
71
+ * @throws {Error} if the RPC call fails at the network or transport layer.
68
72
  */
69
73
  export async function fetchAccessNfts(
70
74
  client: OwnedObjectsClient,
@@ -87,6 +91,8 @@ export async function fetchAccessNfts(
87
91
  * True if `owner` holds at least one access NFT of `nftType` (optionally for `gateId`).
88
92
  * This is the cheap check a frontend runs to decide whether to show a gated option, and a
89
93
  * gateway runs (server-side) as part of access verification.
94
+ *
95
+ * @throws {Error} if the underlying RPC call fails.
90
96
  */
91
97
  export async function ownsAccessNft(
92
98
  client: OwnedObjectsClient,
@@ -97,3 +103,108 @@ export async function ownsAccessNft(
97
103
  const nfts = await fetchAccessNfts(client, owner, nftType, gateId)
98
104
  return nfts.length > 0
99
105
  }
106
+
107
+ // ── Gate discovery (operator management) ─────────────────────────────────────────
108
+ // An operator holds an `AdminCap` per gate they administer. Discovery: list owned AdminCaps
109
+ // (filtered by StructType), read each cap's `gate_id`, then fetch the shared `Gate` object.
110
+
111
+ /** True if `type` names the `access_gate::AdminCap` struct. */
112
+ function isAdminCapType(type: unknown): boolean {
113
+ return typeof type === 'string' && /::access_gate::AdminCap\b/.test(type)
114
+ }
115
+
116
+ /**
117
+ * Parse a single `getOwnedObjects`/`getObject` entry into `{ adminCapId, gateId }`, or `null` if
118
+ * it is not an `AdminCap`. Validates the object **type** when present and reads `fields.gate_id`.
119
+ */
120
+ export function parseAdminCap(entry: any): { adminCapId: string; gateId: string } | null {
121
+ const obj = entry?.data ?? entry
122
+ const adminCapId: string | undefined = obj?.objectId ?? obj?.content?.fields?.id?.id
123
+ const type: unknown = obj?.type ?? obj?.content?.type
124
+ if (type !== undefined && !isAdminCapType(type)) return null
125
+ const gateId: string | undefined = obj?.content?.fields?.gate_id ?? obj?.content?.fields?.gateId
126
+ if (!adminCapId || !gateId) return null
127
+ return { adminCapId, gateId }
128
+ }
129
+
130
+ /**
131
+ * Parse a `getObject` entry for a `Gate` shared object into an {@link OwnedGate} (minus
132
+ * `adminCapId`, which comes from the owning cap). Returns `null` if the object is missing its
133
+ * expected `Gate` fields.
134
+ */
135
+ export function parseGate(entry: any): Omit<OwnedGate, 'adminCapId'> | null {
136
+ const obj = entry?.data ?? entry
137
+ const gateId: string | undefined = obj?.objectId ?? obj?.content?.fields?.id?.id
138
+ const f = obj?.content?.fields
139
+ if (!gateId || !f) return null
140
+ return {
141
+ gateId,
142
+ priceMist: BigInt(f.price_mist ?? 0),
143
+ paymentRecipient: String(f.payment_recipient ?? ''),
144
+ defaultUses: BigInt(f.default_uses ?? 0),
145
+ soulbound: Boolean(f.soulbound),
146
+ autoBurnAtZero: Boolean(f.auto_burn_at_zero),
147
+ paused: Boolean(f.paused),
148
+ frozen: Boolean(f.frozen),
149
+ nftName: String(f.nft_name ?? ''),
150
+ nftImageUrl: String(f.nft_image_url ?? ''),
151
+ nftDescription: String(f.nft_description ?? ''),
152
+ }
153
+ }
154
+
155
+ /**
156
+ * List the `{ adminCapId, gateId }` pairs for every `access_gate::AdminCap` owned by `owner`
157
+ * under `packageId`. Uses `getOwnedObjects` filtered by `StructType` (the standard query).
158
+ *
159
+ * @throws {Error} if the underlying RPC call fails.
160
+ */
161
+ export async function fetchAdminCaps(
162
+ client: OwnedObjectsClient,
163
+ owner: string,
164
+ packageId: string,
165
+ ): Promise<{ adminCapId: string; gateId: string }[]> {
166
+ const { data } = await client.getOwnedObjects({
167
+ owner,
168
+ filter: { StructType: `${packageId}::access_gate::AdminCap` },
169
+ options: { showContent: true, showType: true },
170
+ })
171
+ return (data ?? [])
172
+ .map(parseAdminCap)
173
+ .filter((c): c is { adminCapId: string; gateId: string } => c !== null)
174
+ }
175
+
176
+ /**
177
+ * Typed single-object read of one `Gate` by id, returning its parsed state (without `adminCapId`).
178
+ * Returns `null` if the object is missing or not a `Gate`.
179
+ *
180
+ * @throws {Error} if the RPC call fails at the network or transport layer.
181
+ */
182
+ export async function fetchGate(
183
+ client: SuiObjectClient,
184
+ gateId: string,
185
+ ): Promise<Omit<OwnedGate, 'adminCapId'> | null> {
186
+ const res = await client.getObject({ id: gateId, options: { showType: true, showContent: true } })
187
+ return parseGate(res)
188
+ }
189
+
190
+ /**
191
+ * Fetch every gate `owner` administers: list their owned `AdminCap`s, then fetch each referenced
192
+ * `Gate` shared object and merge in the owning `adminCapId`. Gates whose object can no longer be
193
+ * read (e.g. deleted) are skipped.
194
+ *
195
+ * @throws {Error} if an underlying RPC call fails at the network or transport layer.
196
+ */
197
+ export async function fetchOwnedGates(
198
+ client: OwnedObjectsClient & SuiObjectClient,
199
+ owner: string,
200
+ packageId: string,
201
+ ): Promise<OwnedGate[]> {
202
+ const caps = await fetchAdminCaps(client, owner, packageId)
203
+ const gates = await Promise.all(
204
+ caps.map(async ({ adminCapId, gateId }) => {
205
+ const gate = await fetchGate(client, gateId)
206
+ return gate ? { ...gate, adminCapId } : null
207
+ }),
208
+ )
209
+ return gates.filter((g): g is OwnedGate => g !== null)
210
+ }
package/src/proof.ts CHANGED
@@ -9,14 +9,11 @@ export function personalMessageForNonce(nonce: string): Uint8Array {
9
9
  }
10
10
 
11
11
  function toBase64(s: string): string {
12
- // Works in browsers and modern Node (globalThis.btoa is available on Node >= 16).
13
- if (typeof btoa === 'function') return btoa(s)
14
- return Buffer.from(s, 'utf-8').toString('base64')
12
+ return btoa(s)
15
13
  }
16
14
 
17
15
  function fromBase64(s: string): string {
18
- if (typeof atob === 'function') return atob(s)
19
- return Buffer.from(s, 'base64').toString('utf-8')
16
+ return atob(s)
20
17
  }
21
18
 
22
19
  /** Encode a proof as the compact Bearer token carried in the relay auth header. */
@@ -41,6 +38,8 @@ export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature
41
38
  /**
42
39
  * Sign a challenge and assemble the encoded access-proof token to hand to any gateway as its
43
40
  * auth bearer (e.g. an upload-relay client's auth-token option, an `Authorization` header).
41
+ *
42
+ * @throws {Error} if the wallet signer rejects or fails to sign the message.
44
43
  */
45
44
  export async function buildAccessProof(opts: {
46
45
  address: string
package/src/ptb.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Transaction } from '@mysten/sui/transactions'
2
- import type { AccessGateConfig } from './types.js'
2
+ import type { AccessGateConfig, GateAdminContext } from './types.js'
3
3
 
4
4
  /**
5
5
  * Build a PTB that purchases access: split `priceMist` from the gas coin and call
@@ -11,7 +11,7 @@ export function buildPurchaseTx(cfg: AccessGateConfig, priceMist: bigint | numbe
11
11
  const [payment] = tx.splitCoins(tx.gas, [tx.pure.u64(priceMist)])
12
12
  tx.moveCall({
13
13
  target: `${cfg.packageId}::access_gate::purchase`,
14
- arguments: [tx.object(cfg.gateId), payment],
14
+ arguments: [tx.object(cfg.gateId), tx.object(cfg.platformConfigId), payment],
15
15
  })
16
16
  return tx
17
17
  }
@@ -48,6 +48,9 @@ export function buildCreateGateTx(
48
48
  defaultUses: bigint | number
49
49
  soulbound: boolean
50
50
  autoBurnAtZero: boolean
51
+ nftName: string
52
+ nftImageUrl: string
53
+ nftDescription: string
51
54
  },
52
55
  ): Transaction {
53
56
  const tx = new Transaction()
@@ -59,7 +62,98 @@ export function buildCreateGateTx(
59
62
  tx.pure.u64(opts.defaultUses),
60
63
  tx.pure.bool(opts.soulbound),
61
64
  tx.pure.bool(opts.autoBurnAtZero),
65
+ tx.pure.string(opts.nftName),
66
+ tx.pure.string(opts.nftImageUrl),
67
+ tx.pure.string(opts.nftDescription),
62
68
  ],
63
69
  })
64
70
  return tx
65
71
  }
72
+
73
+ // ── Gate administration (AdminCap-gated) ─────────────────────────────────────────
74
+ // Builders for the operator management surface. Each calls an `assert_admin`-gated entry
75
+ // point with `[adminCap, gate, <value>]`; the operator signs + executes with their wallet.
76
+ // The on-chain call aborts (`E_WRONG_GATE` / `E_FROZEN`) if the cap/gate mismatch or the
77
+ // gate is frozen, so these never need to pre-check.
78
+
79
+ /** The element type accepted by a `moveCall`'s `arguments` array. */
80
+ type MoveCallArg = NonNullable<Parameters<Transaction['moveCall']>[0]['arguments']>[number]
81
+
82
+ /** Build a single-`moveCall` admin PTB: `<fn>(adminCap, gate, ...extraArgs)`. */
83
+ function buildGateAdminCall(
84
+ ctx: GateAdminContext,
85
+ fn: string,
86
+ extraArgs: (tx: Transaction) => MoveCallArg[],
87
+ ): Transaction {
88
+ const tx = new Transaction()
89
+ tx.moveCall({
90
+ target: `${ctx.packageId}::access_gate::${fn}`,
91
+ arguments: [tx.object(ctx.adminCapId), tx.object(ctx.gateId), ...extraArgs(tx)],
92
+ })
93
+ return tx
94
+ }
95
+
96
+ /** Set the gate price (in MIST) charged by future `purchase` calls (0 = free). */
97
+ export function buildSetPriceTx(ctx: GateAdminContext, priceMist: bigint | number): Transaction {
98
+ return buildGateAdminCall(ctx, 'set_price', (tx) => [tx.pure.u64(priceMist)])
99
+ }
100
+
101
+ /** Redirect future purchase payments to a new recipient address. */
102
+ export function buildSetPaymentRecipientTx(ctx: GateAdminContext, recipient: string): Transaction {
103
+ return buildGateAdminCall(ctx, 'set_payment_recipient', (tx) => [tx.pure.address(recipient)])
104
+ }
105
+
106
+ /** Pause or unpause `purchase` (paused ⇒ `purchase` aborts with `E_PAUSED`). */
107
+ export function buildSetPausedTx(ctx: GateAdminContext, paused: boolean): Transaction {
108
+ return buildGateAdminCall(ctx, 'set_paused', (tx) => [tx.pure.bool(paused)])
109
+ }
110
+
111
+ /** Change the default uses for future mints (0 ⇒ unlimited pass; N ⇒ single-use with N). */
112
+ export function buildSetDefaultUsesTx(ctx: GateAdminContext, defaultUses: bigint | number): Transaction {
113
+ return buildGateAdminCall(ctx, 'set_default_uses', (tx) => [tx.pure.u64(defaultUses)])
114
+ }
115
+
116
+ /** Switch the soulbound flag for future mints (does not affect already-minted NFTs). */
117
+ export function buildSetSoulboundTx(ctx: GateAdminContext, soulbound: boolean): Transaction {
118
+ return buildGateAdminCall(ctx, 'set_soulbound', (tx) => [tx.pure.bool(soulbound)])
119
+ }
120
+
121
+ /** Toggle the auto-burn-at-zero policy for future mints. */
122
+ export function buildSetAutoBurnAtZeroTx(ctx: GateAdminContext, autoBurn: boolean): Transaction {
123
+ return buildGateAdminCall(ctx, 'set_auto_burn_at_zero', (tx) => [tx.pure.bool(autoBurn)])
124
+ }
125
+
126
+ /** Update the default NFT display name for future mints. */
127
+ export function buildSetNftNameTx(ctx: GateAdminContext, name: string): Transaction {
128
+ return buildGateAdminCall(ctx, 'set_nft_name', (tx) => [tx.pure.string(name)])
129
+ }
130
+
131
+ /** Update the default NFT image URL for future mints. */
132
+ export function buildSetNftImageUrlTx(ctx: GateAdminContext, url: string): Transaction {
133
+ return buildGateAdminCall(ctx, 'set_nft_image_url', (tx) => [tx.pure.string(url)])
134
+ }
135
+
136
+ /** Update the default NFT description for future mints. */
137
+ export function buildSetNftDescriptionTx(ctx: GateAdminContext, description: string): Transaction {
138
+ return buildGateAdminCall(ctx, 'set_nft_description', (tx) => [tx.pure.string(description)])
139
+ }
140
+
141
+ /** AdminCap-gated free grant (airdrop) of the gate's NFT flavour to `recipient`. */
142
+ export function buildAirdropTx(ctx: GateAdminContext, recipient: string): Transaction {
143
+ return buildGateAdminCall(ctx, 'airdrop', (tx) => [tx.pure.address(recipient)])
144
+ }
145
+
146
+ /**
147
+ * Make the gate immutable — **irreversible**. Consumes the `AdminCap` (passed by value) and sets
148
+ * `Gate.frozen = true`, permanently ending all setters and `airdrop`. `purchase`/`consume` remain
149
+ * permissionless. Grant everything first, then freeze.
150
+ */
151
+ export function buildMakeGateImmutableTx(ctx: GateAdminContext): Transaction {
152
+ const tx = new Transaction()
153
+ tx.moveCall({
154
+ target: `${ctx.packageId}::access_gate::make_gate_immutable`,
155
+ // cap is consumed by value; gate is &mut.
156
+ arguments: [tx.object(ctx.adminCapId), tx.object(ctx.gateId)],
157
+ })
158
+ return tx
159
+ }
package/src/types.ts CHANGED
@@ -11,6 +11,8 @@ export interface AccessGateConfig {
11
11
  packageId: string
12
12
  /** The shared `Gate` object ID. */
13
13
  gateId: string
14
+ /** The shared `PlatformConfig` object ID. Required for `buildPurchaseTx`. */
15
+ platformConfigId: string
14
16
  /**
15
17
  * Fully-qualified NFT type string to filter ownership by, e.g.
16
18
  * `<pkg>::access_gate::AccessNFT` or `<pkg>::access_gate::SoulboundAccessNFT`.
@@ -21,6 +23,48 @@ export interface AccessGateConfig {
21
23
  soulbound?: boolean
22
24
  }
23
25
 
26
+ /**
27
+ * Identifies a gate an operator administers, for the AdminCap-gated management PTB builders
28
+ * (setters, airdrop, freeze). The three ids together authorise a call: `adminCapId` must be the
29
+ * `AdminCap` whose `gate_id` matches `gateId`, under the published `packageId`.
30
+ */
31
+ export interface GateAdminContext {
32
+ /** Published `access_gate` package ID. */
33
+ packageId: string
34
+ /** The shared `Gate` object ID being administered. */
35
+ gateId: string
36
+ /** The `AdminCap` object ID authorised over `gateId` (held by the operator). */
37
+ adminCapId: string
38
+ }
39
+
40
+ /** A gate an operator administers, parsed from its on-chain `Gate` object + owning `AdminCap`. */
41
+ export interface OwnedGate {
42
+ /** The shared `Gate` object ID. */
43
+ gateId: string
44
+ /** The `AdminCap` object ID that authorises administering this gate. */
45
+ adminCapId: string
46
+ /** Price in MIST charged by `purchase` (0 = free). */
47
+ priceMist: bigint
48
+ /** Address that receives the operator share of each paid `purchase`. */
49
+ paymentRecipient: string
50
+ /** 0 ⇒ unlimited passes; N ⇒ single-use NFTs with N uses. */
51
+ defaultUses: bigint
52
+ /** Whether newly-minted NFTs are soulbound. */
53
+ soulbound: boolean
54
+ /** Whether a single-use NFT is deleted (vs. kept as a receipt) at zero uses. */
55
+ autoBurnAtZero: boolean
56
+ /** Whether `purchase` is currently disabled. */
57
+ paused: boolean
58
+ /** Whether the gate has been made immutable (all admin/airdrop permanently disabled). */
59
+ frozen: boolean
60
+ /** Default NFT display name minted into future NFTs. */
61
+ nftName: string
62
+ /** Default NFT image URL minted into future NFTs. */
63
+ nftImageUrl: string
64
+ /** Default NFT description minted into future NFTs. */
65
+ nftDescription: string
66
+ }
67
+
24
68
  /** A server-issued, time-bound challenge the wallet signs to prove control of an address. */
25
69
  export interface Challenge {
26
70
  /** Opaque nonce (as issued by the gateway; treated as a UTF-8 string end-to-end). */