@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.
- package/CHANGELOG.md +10 -0
- package/docs/.vitepress/config.ts +111 -0
- package/docs/.vitepress/env.d.ts +6 -0
- package/docs/.vitepress/theme/custom.css +63 -0
- package/docs/.vitepress/theme/index.ts +19 -0
- package/docs/design-system/components.md +111 -0
- package/docs/design-system/index.md +64 -0
- package/docs/design-system/tokens.md +136 -0
- package/docs/getting-started/index.md +52 -0
- package/docs/getting-started/local-dev.md +74 -0
- package/docs/getting-started/toolchain.md +83 -0
- package/docs/index.md +44 -0
- package/docs/sui/access-gate/gateway.md +159 -0
- package/docs/sui/access-gate/index.md +62 -0
- package/docs/sui/access-gate/integration.md +168 -0
- package/docs/sui/dao/index.md +154 -0
- package/docs/sui/environment.md +101 -0
- package/docs/sui/index.md +46 -0
- package/docs/sui/ptb-patterns.md +136 -0
- package/docs/sui/sealed-storage/index.md +49 -0
- package/docs/sui/sealed-storage/integration.md +125 -0
- package/docs/sui/sealed-storage/policies.md +81 -0
- package/docs/sui/walrus-storage/index.md +67 -0
- package/docs/sui/walrus-storage/integration.md +127 -0
- package/docs/sui/walrus-storage/relay-self-host.md +82 -0
- package/package.json +39 -0
- package/tsconfig.json +13 -0
|
@@ -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 -->
|