@meddleware/dev 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.
@@ -0,0 +1,83 @@
1
+ # Toolchain
2
+
3
+ ## Node.js
4
+
5
+ The workspace targets **Node.js 22.18.0 or ≥ 24.12.0**. Use [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm) to manage versions:
6
+
7
+ ```bash
8
+ # fnm (recommended — fast, written in Rust)
9
+ curl -fsSL https://fnm.vercel.app/install | bash
10
+ fnm install 24
11
+ fnm use 24
12
+ node --version # v24.x.x
13
+ ```
14
+
15
+ ## Sui and Walrus CLIs — suiup
16
+
17
+ All Sui and Walrus binaries are managed by **suiup**, a version manager analogous to rustup:
18
+
19
+ ```bash
20
+ # Install suiup
21
+ curl -sSfL https://raw.githubusercontent.com/MystenLabs/suiup/main/install.sh | sh
22
+ # suiup installs to ~/.local/bin/suiup; symlinks at /usr/local/bin/sui and /usr/local/bin/walrus
23
+
24
+ # Install the testnet-channel binaries
25
+ suiup install sui@testnet
26
+ suiup install walrus@testnet
27
+
28
+ # Pin to the exact versions used in this workspace
29
+ suiup install sui@testnet-v1.76.1
30
+ suiup switch sui@testnet-v1.76.1
31
+
32
+ suiup install walrus@testnet-v1.53.0
33
+ suiup switch walrus@testnet-v1.53.0
34
+
35
+ # Verify
36
+ sui --version # sui testnet-v1.76.1
37
+ walrus --version # walrus testnet-v1.53.0
38
+ ```
39
+
40
+ ### Checking for updates
41
+
42
+ ```bash
43
+ suiup show # installed and active versions
44
+ suiup status # check whether newer testnet releases are available
45
+ ```
46
+
47
+ When a new testnet release ships, install and switch:
48
+
49
+ ```bash
50
+ suiup install sui@testnet-vX.Y.Z
51
+ suiup switch sui@testnet-vX.Y.Z
52
+ suiup install walrus@testnet-vX.Y.Z
53
+ suiup switch walrus@testnet-vX.Y.Z
54
+ ```
55
+
56
+ ## Sui wallet
57
+
58
+ Install any Sui-compatible wallet browser extension. [Slush](https://slush.app/) is recommended for development.
59
+
60
+ Create a new address for testnet work:
61
+
62
+ ```bash
63
+ sui client new-address ed25519
64
+ # Copy the address shown and fund it from the testnet faucet
65
+ ```
66
+
67
+ Fund the address from the [Sui testnet faucet](https://faucet.sui.io/).
68
+
69
+ ## npm workspaces
70
+
71
+ The monorepo uses npm workspaces. Install all dependencies from the workspace root:
72
+
73
+ ```bash
74
+ cd workspace/
75
+ npm install
76
+ ```
77
+
78
+ Individual packages can be built or tested independently:
79
+
80
+ ```bash
81
+ cd repos/walrus-ui && npm run build
82
+ cd repos/seal-ui && npm run type-check
83
+ ```
package/docs/index.md ADDED
@@ -0,0 +1,44 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: "Meddleware Dev"
6
+ text: "Build on the platform"
7
+ tagline: Integration guides, design-system usage, and Sui development patterns.
8
+ actions:
9
+ - theme: brand
10
+ text: Getting started
11
+ link: /getting-started/
12
+ - theme: alt
13
+ text: Sui development
14
+ link: /sui/
15
+ - theme: alt
16
+ text: User docs →
17
+ link: https://docs.meddleware.co.uk
18
+
19
+ features:
20
+ - title: Design system
21
+ details: Consume @meddleware/design-tokens and @meddleware/ui in a Vue 3 app — sacred-geometry scales, seasonal theming, focus/noise utilities.
22
+ link: /design-system/
23
+ linkText: Design system
24
+ - title: Walrus Storage
25
+ details: Upload, read, and extend blobs with the @meddleware/walrus-client SDK. Self-host your own Walrus relay.
26
+ link: /sui/walrus-storage/
27
+ linkText: Walrus Storage
28
+ - title: Sealed Storage
29
+ details: Encrypt data on Walrus with on-chain access-control policies. Write Move policies and integrate the @meddleware/seal-client SDK.
30
+ link: /sui/sealed-storage/
31
+ linkText: Sealed Storage
32
+ - title: Access Gate
33
+ details: Create NFT access-gate passes, verify them in your app, and deploy the nft-gate gateway.
34
+ link: /sui/access-gate/
35
+ linkText: Access Gate
36
+ - title: DAO
37
+ details: Interact with the Meddleware DAO from code — governance PTBs, on-chain objects, and treasury reads.
38
+ link: /sui/dao/
39
+ linkText: DAO
40
+ - title: API reference
41
+ details: Full TypeDoc SDK reference lives in the user docs — every exported function, type, and class.
42
+ link: https://docs.meddleware.co.uk/blockchain/sui/
43
+ linkText: API reference →
44
+ ---
@@ -0,0 +1,159 @@
1
+ # Deploy the NFT Gate gateway
2
+
3
+ The NFT Gate gateway handles the off-chain challenge/proof protocol. It issues nonces, verifies NFT ownership on-chain, and returns short-lived JWTs. Two implementations are available: a Rust/Axum service and a Cloudflare Workers implementation.
4
+
5
+ ## Implementations
6
+
7
+ | Implementation | Location | Best for |
8
+ | --- | --- | --- |
9
+ | Rust/Axum | `services/nft-gate/gateway/` | Self-hosted, k8s, low latency |
10
+ | Cloudflare Worker | `services/nft-gate/worker/` | Edge deployment, zero-infra |
11
+
12
+ ---
13
+
14
+ ## Rust/Axum gateway
15
+
16
+ ### Build
17
+
18
+ ```bash
19
+ cd services/nft-gate/gateway
20
+ cargo build --release
21
+ ```
22
+
23
+ ### Configuration
24
+
25
+ The gateway reads environment variables:
26
+
27
+ | Variable | Description | Default |
28
+ | --- | --- | --- |
29
+ | `SUI_NETWORK` | `testnet` or `mainnet` | `testnet` |
30
+ | `SUI_RPC_URL` | Sui gRPC endpoint | testnet default |
31
+ | `JWT_SECRET` | HMAC-SHA256 signing secret (≥32 bytes) | — required |
32
+ | `CHALLENGE_TTL_SECS` | Nonce expiry | `120` |
33
+ | `TOKEN_TTL_SECS` | JWT lifetime | `3600` |
34
+ | `LISTEN_ADDR` | Bind address | `0.0.0.0:3000` |
35
+ | `LOG_LEVEL` | `debug`/`info`/`warn`/`error` | `info` |
36
+
37
+ Create `.env`:
38
+
39
+ ```bash
40
+ SUI_NETWORK=testnet
41
+ JWT_SECRET=<at-least-32-random-bytes-base64>
42
+ CHALLENGE_TTL_SECS=120
43
+ TOKEN_TTL_SECS=3600
44
+ ```
45
+
46
+ ### Run
47
+
48
+ ```bash
49
+ cargo run --release
50
+ # or with .env file
51
+ env $(cat .env | xargs) cargo run --release
52
+ ```
53
+
54
+ ### k8s deployment
55
+
56
+ A `Deployment` and `Service` manifest live in `post-bootstrap/nft-gate/`. The gateway is exposed via nginx-ingress at `https://nft-gate.meddleware.co.uk`. Secrets are mounted from a k8s `Secret` object:
57
+
58
+ ```yaml
59
+ env:
60
+ - name: JWT_SECRET
61
+ valueFrom:
62
+ secretKeyRef:
63
+ name: nft-gate-secrets
64
+ key: jwt-secret
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Cloudflare Workers implementation
70
+
71
+ The Worker implementation lives in `services/nft-gate/worker/`. It is functionally equivalent to the Rust gateway but runs at the Cloudflare edge.
72
+
73
+ ### Prerequisites
74
+
75
+ - Cloudflare account with Workers enabled.
76
+ - Wrangler CLI: `npm install -g wrangler`.
77
+
78
+ ### Configuration
79
+
80
+ ```toml
81
+ # services/nft-gate/worker/wrangler.toml
82
+ name = "nft-gate"
83
+ main = "src/index.ts"
84
+ compatibility_date = "2025-01-01"
85
+
86
+ [vars]
87
+ SUI_NETWORK = "testnet"
88
+ CHALLENGE_TTL_SECS = "120"
89
+ TOKEN_TTL_SECS = "3600"
90
+ ```
91
+
92
+ Store secrets:
93
+
94
+ ```bash
95
+ wrangler secret put JWT_SECRET
96
+ # Paste a random ≥32-byte base64-encoded value
97
+ ```
98
+
99
+ ### Deploy
100
+
101
+ ```bash
102
+ cd services/nft-gate/worker
103
+ npm install
104
+ wrangler deploy
105
+ ```
106
+
107
+ ---
108
+
109
+ ## API endpoints
110
+
111
+ Both implementations expose the same HTTP API:
112
+
113
+ ### `POST /challenge`
114
+
115
+ Request a challenge nonce for a pass.
116
+
117
+ **Body:**
118
+ ```json
119
+ { "gateId": "0x...", "passId": "0x..." }
120
+ ```
121
+
122
+ **Response:**
123
+ ```json
124
+ { "challenge": "meddleware-access:v1:<nonce>", "nonce": "<hex>" }
125
+ ```
126
+
127
+ ### `POST /verify`
128
+
129
+ Submit the signed challenge and receive a JWT.
130
+
131
+ **Body:**
132
+ ```json
133
+ {
134
+ "gateId": "0x...",
135
+ "passId": "0x...",
136
+ "nonce": "<hex>",
137
+ "address": "0x...",
138
+ "signature": "<base64>"
139
+ }
140
+ ```
141
+
142
+ **Response:**
143
+ ```json
144
+ { "token": "<jwt>", "expiresAt": 1234567890 }
145
+ ```
146
+
147
+ ### `GET /health`
148
+
149
+ Returns `{ "status": "ok" }`. Used by the relay registry ping check.
150
+
151
+ ---
152
+
153
+ ## JSON-RPC deprecation notice
154
+
155
+ ::: warning Public fullnodes killed JSON-RPC in September 2026
156
+ The gateway uses `@mysten/sui` v2.x which routes through gRPC/GraphQL automatically. Do not configure an RPC URL ending in `/json-rpc` — it will fail. If you are running your own fullnode, ensure the gRPC port is reachable.
157
+ :::
158
+
159
+ <!-- white-label: operator customization guide (custom JWT claims, RBAC, rate limits, enterprise SSO) — planned -->
@@ -0,0 +1,62 @@
1
+ # Access Gate — SDK setup
2
+
3
+ [User docs →](https://docs.meddleware.co.uk/blockchain/sui/access-gate/) | [API reference →](https://docs.meddleware.co.uk/blockchain/sui/access-gate/reference)
4
+
5
+ The Access Gate lets operators create on-chain NFT pass systems. Each gate can mint unlimited-use or single-use passes, optionally soulbound to the buyer's address. Operators and the platform earn commission on each sale.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @meddleware/nft-gate-client @mysten/sui
11
+ ```
12
+
13
+ ## Initialise the client
14
+
15
+ ```ts
16
+ import { NftGateClient } from '@meddleware/nft-gate-client'
17
+ import { SuiClient, getFullnodeUrl } from '@mysten/sui/client'
18
+
19
+ const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') })
20
+ const gateClient = new NftGateClient({
21
+ suiClient,
22
+ network: 'testnet',
23
+ gatewayUrl: import.meta.env.VITE_NFT_GATE_URL ?? 'https://nft-gate.meddleware.co.uk',
24
+ })
25
+ ```
26
+
27
+ ## Key concepts
28
+
29
+ | Term | Description |
30
+ | --- | --- |
31
+ | `Gate` | On-chain object representing an access system (price, supply, commission config) |
32
+ | `AdminCap` | Capability object held by the gate operator; required for privileged operations |
33
+ | `AccessPass` | NFT minted when a user buys access |
34
+ | Soulbound | A pass bound to one address — non-transferable |
35
+ | Challenge/proof | Off-chain protocol: gateway issues a nonce; client signs it with the pass |
36
+
37
+ ## Quick start
38
+
39
+ Check whether an address holds a valid pass for a gate:
40
+
41
+ ```ts
42
+ const hasAccess = await gateClient.hasValidPass(gateId, walletAddress)
43
+ ```
44
+
45
+ Buy a pass:
46
+
47
+ ```ts
48
+ import { Transaction } from '@mysten/sui/transactions'
49
+
50
+ const tx = new Transaction()
51
+ const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(priceInMist)])
52
+ const [pass] = tx.moveCall({
53
+ target: `${ACCESS_GATE_PACKAGE}::access_gate::buy`,
54
+ arguments: [tx.object(gateId), coin, tx.object(PLATFORM_CONFIG_ID)],
55
+ })
56
+ tx.transferObjects([pass], tx.pure.address(buyerAddress))
57
+ await signAndExecute({ transaction: tx })
58
+ ```
59
+
60
+ See [Integration guide](./integration) for the full purchase → verify flow and [Deploy the gateway](./gateway) for self-hosting the challenge/proof gateway.
61
+
62
+ <!-- white-label: operator guide (custom gate config, commission setup, branded pass metadata) — planned -->
@@ -0,0 +1,168 @@
1
+ # Access Gate — Integration guide
2
+
3
+ ## Full purchase → verify flow
4
+
5
+ ### Purchase a pass
6
+
7
+ ```ts
8
+ import { Transaction } from '@mysten/sui/transactions'
9
+ import { SuiClient, getFullnodeUrl } from '@mysten/sui/client'
10
+
11
+ const PACKAGE = import.meta.env.VITE_ACCESS_GATE_PACKAGE
12
+ const GATE_ID = import.meta.env.VITE_GATE_ID
13
+ const PLATFORM = import.meta.env.VITE_PLATFORM_CONFIG_ID
14
+
15
+ async function buyPass(
16
+ suiClient: SuiClient,
17
+ signer: { address: string; signAndExecute: (tx: Transaction) => Promise<string> },
18
+ priceInMist: bigint,
19
+ ): Promise<string> {
20
+ const tx = new Transaction()
21
+ const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(priceInMist)])
22
+ const [pass] = tx.moveCall({
23
+ target: `${PACKAGE}::access_gate::buy`,
24
+ arguments: [tx.object(GATE_ID), coin, tx.object(PLATFORM)],
25
+ })
26
+ tx.transferObjects([pass], tx.pure.address(signer.address))
27
+
28
+ const digest = await signer.signAndExecute(tx)
29
+ return digest
30
+ }
31
+ ```
32
+
33
+ ### Verify a pass off-chain (challenge/proof)
34
+
35
+ The gateway issues a one-time nonce; the client must sign it with the pass object to prove ownership.
36
+
37
+ ```ts
38
+ import { NftGateClient } from '@meddleware/nft-gate-client'
39
+
40
+ async function getAccessToken(
41
+ gateClient: NftGateClient,
42
+ signer: { address: string; sign: (msg: Uint8Array) => Promise<Uint8Array> },
43
+ gateId: string,
44
+ passId: string,
45
+ ): Promise<string> {
46
+ // 1. Request a challenge from the gateway
47
+ const { challenge, nonce } = await gateClient.requestChallenge({ gateId, passId })
48
+
49
+ // 2. Sign the challenge with the user's wallet
50
+ const signature = await signer.sign(new TextEncoder().encode(challenge))
51
+
52
+ // 3. Submit the proof; receive a short-lived JWT
53
+ const { token } = await gateClient.submitProof({
54
+ gateId,
55
+ passId,
56
+ nonce,
57
+ signature,
58
+ address: signer.address,
59
+ })
60
+
61
+ return token
62
+ }
63
+ ```
64
+
65
+ The `token` is a signed JWT issued by the gateway. Present it in the `Authorization: Bearer <token>` header when calling any gated API endpoint.
66
+
67
+ ### Nonce requirements
68
+
69
+ The `access_gate` Move package requires a minimum nonce length of 8 bytes. The gateway enforces this automatically. If you are calling the contract directly (not via the gateway), generate at least 8 bytes of entropy:
70
+
71
+ ```ts
72
+ const nonce = crypto.getRandomValues(new Uint8Array(8))
73
+ ```
74
+
75
+ ### Check pass validity before gating
76
+
77
+ ```ts
78
+ const hasAccess = await gateClient.hasValidPass(gateId, walletAddress)
79
+ if (!hasAccess) {
80
+ // Prompt user to purchase
81
+ }
82
+ ```
83
+
84
+ ## Using with dapp-kit
85
+
86
+ Wire the wallet from `@mysten/dapp-kit`:
87
+
88
+ ```ts
89
+ import { useCurrentAccount, useSignPersonalMessage, useSignAndExecuteTransaction } from '@mysten/dapp-kit'
90
+ import { Transaction } from '@mysten/sui/transactions'
91
+
92
+ const account = useCurrentAccount()
93
+ const { mutateAsync: signPersonalMessage } = useSignPersonalMessage()
94
+ const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction()
95
+
96
+ const signer = {
97
+ address: account.value!.address,
98
+ sign: async (msg: Uint8Array) => {
99
+ const { signature } = await signPersonalMessage({ message: msg })
100
+ return signature
101
+ },
102
+ signAndExecute: async (tx: Transaction) => {
103
+ const { digest } = await signAndExecuteTransaction({ transaction: tx })
104
+ return digest
105
+ },
106
+ }
107
+ ```
108
+
109
+ ## Error handling
110
+
111
+ ```ts
112
+ import { NftGateError, GateNotFoundError, PassExpiredError } from '@meddleware/nft-gate-client'
113
+
114
+ try {
115
+ const token = await getAccessToken(gateClient, signer, gateId, passId)
116
+ } catch (err) {
117
+ if (err instanceof GateNotFoundError) {
118
+ console.error('Gate does not exist:', gateId)
119
+ } else if (err instanceof PassExpiredError) {
120
+ console.error('Pass is expired or consumed')
121
+ } else if (err instanceof NftGateError) {
122
+ console.error('Gateway error:', err.code, err.message)
123
+ }
124
+ throw err
125
+ }
126
+ ```
127
+
128
+ ## Vue composable
129
+
130
+ ```ts
131
+ // composables/useAccessGate.ts
132
+ import { ref } from 'vue'
133
+ import { NftGateClient } from '@meddleware/nft-gate-client'
134
+
135
+ export function useAccessGate(gateClient: NftGateClient) {
136
+ const checking = ref(false)
137
+ const hasAccess = ref<boolean | null>(null)
138
+ const token = ref<string | null>(null)
139
+ const error = ref<Error | null>(null)
140
+
141
+ async function check(gateId: string, address: string) {
142
+ checking.value = true
143
+ error.value = null
144
+ try {
145
+ hasAccess.value = await gateClient.hasValidPass(gateId, address)
146
+ } catch (e) {
147
+ error.value = e as Error
148
+ } finally {
149
+ checking.value = false
150
+ }
151
+ }
152
+
153
+ async function authenticate(
154
+ gateId: string,
155
+ passId: string,
156
+ signer: { address: string; sign: (msg: Uint8Array) => Promise<Uint8Array> },
157
+ ) {
158
+ error.value = null
159
+ try {
160
+ token.value = await getAccessToken(gateClient, signer, gateId, passId)
161
+ } catch (e) {
162
+ error.value = e as Error
163
+ }
164
+ }
165
+
166
+ return { checking, hasAccess, token, error, check, authenticate }
167
+ }
168
+ ```
@@ -0,0 +1,154 @@
1
+ # DAO — Governance integration
2
+
3
+ [User docs →](https://docs.meddleware.co.uk/blockchain/sui/dao/) | [API reference →](https://docs.meddleware.co.uk/blockchain/sui/dao/reference)
4
+
5
+ The vault DAO governs privileged parameters — fee rates, strategy allocations, skim thresholds, fee recipients, and access gate policy. The `vault_dao` package issues `DaoAdminCap` to the multisig governor; `vault_governor` gates privileged operations behind it.
6
+
7
+ ## Key objects
8
+
9
+ | Object | Description |
10
+ | --- | --- |
11
+ | `DaoAdminCap` | Owned object; required as a witness for all DAO-gated calls |
12
+ | `ConfigState` | Shared object; holds all DAO-governed parameters |
13
+ | `vault_governor` | Package that executes privileged vault operations given `DaoAdminCap` |
14
+
15
+ ## Reading current config
16
+
17
+ ```ts
18
+ import { SuiClient, getFullnodeUrl } from '@mysten/sui/client'
19
+
20
+ const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') })
21
+
22
+ // Fetch ConfigState fields
23
+ const config = await suiClient.getObject({
24
+ id: CONFIG_STATE_ID,
25
+ options: { showContent: true },
26
+ })
27
+
28
+ const fields = (config.data?.content as any)?.fields
29
+ console.log({
30
+ skimFeeBps: fields.skim_fee_bps,
31
+ skimThresholdMist: fields.skim_threshold_mist,
32
+ buybackBps: fields.buyback_bps,
33
+ feeRecipients: fields.fee_recipients,
34
+ })
35
+ ```
36
+
37
+ ## Composing a governance PTB
38
+
39
+ DAO operations are PTBs that pass the `DaoAdminCap` to `vault_governor` functions. The `DaoAdminCap` must be an owned object held by the signer.
40
+
41
+ ### Update `skim_fee_bps`
42
+
43
+ ```ts
44
+ import { Transaction } from '@mysten/sui/transactions'
45
+
46
+ function buildSetSkimFeeBpsTx(
47
+ daoAdminCapId: string,
48
+ configStateId: string,
49
+ newBps: number, // e.g. 1000 = 10%
50
+ ): Transaction {
51
+ const tx = new Transaction()
52
+ tx.moveCall({
53
+ target: `${VAULT_GOVERNOR_PACKAGE}::vault_governor::set_skim_fee_bps`,
54
+ arguments: [
55
+ tx.object(daoAdminCapId),
56
+ tx.object(configStateId),
57
+ tx.pure.u64(newBps),
58
+ ],
59
+ })
60
+ return tx
61
+ }
62
+
63
+ // Execute
64
+ const tx = buildSetSkimFeeBpsTx(daoAdminCapId, CONFIG_STATE_ID, 1000)
65
+ await signAndExecute({ transaction: tx })
66
+ ```
67
+
68
+ ### Update fee recipients
69
+
70
+ `fee_recipients` is a vector of `(address, weight_bps)` tuples. The weights must sum to 10 000.
71
+
72
+ ```ts
73
+ function buildSetFeeRecipientsTx(
74
+ daoAdminCapId: string,
75
+ configStateId: string,
76
+ recipients: Array<{ address: string; weightBps: number }>,
77
+ ): Transaction {
78
+ const tx = new Transaction()
79
+
80
+ const addrs = recipients.map(r => r.address)
81
+ const weights = recipients.map(r => r.weightBps)
82
+
83
+ tx.moveCall({
84
+ target: `${VAULT_GOVERNOR_PACKAGE}::vault_governor::set_fee_recipients`,
85
+ arguments: [
86
+ tx.object(daoAdminCapId),
87
+ tx.object(configStateId),
88
+ tx.pure(addrs, 'vector<address>'),
89
+ tx.pure(weights, 'vector<u64>'),
90
+ ],
91
+ })
92
+ return tx
93
+ }
94
+ ```
95
+
96
+ ### Pause / unpause the vault
97
+
98
+ ```ts
99
+ function buildPauseVaultTx(daoAdminCapId: string, vaultId: string): Transaction {
100
+ const tx = new Transaction()
101
+ tx.moveCall({
102
+ target: `${VAULT_GOVERNOR_PACKAGE}::vault_governor::pause`,
103
+ arguments: [tx.object(daoAdminCapId), tx.object(vaultId)],
104
+ })
105
+ return tx
106
+ }
107
+ ```
108
+
109
+ ## Dry-run before executing
110
+
111
+ Always dry-run DAO transactions before live execution:
112
+
113
+ ```ts
114
+ const dryRun = await suiClient.dryRunTransactionBlock({
115
+ transactionBlock: await tx.build({ client: suiClient }),
116
+ })
117
+
118
+ if (dryRun.effects.status.status !== 'success') {
119
+ console.error('Dry-run failed:', dryRun.effects.status.error)
120
+ } else {
121
+ console.log('Gas estimate:', dryRun.effects.gasUsed)
122
+ }
123
+ ```
124
+
125
+ ## Event monitoring
126
+
127
+ Subscribe to governance events to observe when parameters change:
128
+
129
+ ```ts
130
+ const unsubscribe = await suiClient.subscribeEvent({
131
+ filter: { Package: VAULT_GOVERNOR_PACKAGE },
132
+ onMessage: (event) => {
133
+ console.log('Governance event:', event.type, event.parsedJson)
134
+ },
135
+ })
136
+ ```
137
+
138
+ Key event types emitted by `vault_governor`:
139
+
140
+ | Event | Emitted when |
141
+ | --- | --- |
142
+ | `SkimFeeBpsUpdated` | `skim_fee_bps` changed |
143
+ | `FeeRecipientsUpdated` | `fee_recipients` changed |
144
+ | `VaultPaused` | Vault paused |
145
+ | `VaultResumed` | Vault unpaused |
146
+ | `StrategyAllocationUpdated` | Strategy allocation cap changed |
147
+
148
+ ## Trust assumptions
149
+
150
+ - The `DaoAdminCap` holder has full governance authority. Guard the signer with a multisig policy or hardware key.
151
+ - `ConfigState` is a shared object; any party can read it on-chain.
152
+ - There is no timelock on governance operations at testnet. A timelock module is planned for mainnet — monitor `docs/DEFERRED_WORK.md`.
153
+
154
+ <!-- white-label: operator guide (multisig setup, custom governance modules, off-chain voting integration) — planned -->