Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .claude/skills/configure-session-keys/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ encryption: { sessionKey: { signer: keypair } }
The SDK derives the address from `signer.toSuiAddress()`, creates a `SessionKey`, and certifies it automatically. Works with:

- `Keypair` directly (Node/server-side, scripts).
- `@mysten/dapp-kit`'s `CurrentAccountSigner`.
- `@mysten/dapp-kit-core`'s `CurrentAccountSigner`.
- Enoki's `EnokiSigner`.

**Use this whenever you have a `Signer` instance.** Zero ceremony.

If the wallet rejects concurrent sign requests (the dev-wallet does; dApp Kit does not queue),
subclass `CurrentAccountSigner` and serialize `signPersonalMessage` with a promise chain — worked
example: `chat-app/src/lib/queued-signer.ts`.

### Tier 2 — callback-based

```ts
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/integrate-sui-stack-messaging/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ Three tiers, pick by your auth situation:

| Tier | When | What you pass |
|---|---|---|
| **1 — signer-based** | dapp-kit-next, Keypair, Enoki, server-side Node with a key | `encryption: { sessionKey: { signer: keypair } }` |
| **2 — callback-based** | current dapp-kit without the Signer abstraction | `encryption: { sessionKey: { address, onSign: async (msg) => signPersonalMessage(msg) } }` |
| **1 — signer-based** | Keypair, Enoki, dapp-kit-core's `CurrentAccountSigner`, server-side Node with a key | `encryption: { sessionKey: { signer: keypair } }` |
| **2 — callback-based** | wallet integrations without a `Signer` abstraction | `encryption: { sessionKey: { address, onSign: async (msg) => signPersonalMessage(msg) } }` |
| **3 — manual** | you already manage `SessionKey` lifecycle externally | `encryption: { sessionKey: { getSessionKey: () => myManaged } }` |

Tier-1 is the default. Tier-2 is what most current React-wallet integrations end up needing. Tier-3 is rare — only choose if you have a strong reason to manage the key elsewhere.
Expand Down
8 changes: 5 additions & 3 deletions .claude/skills/spin-up-local-devstack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ The load-bearing, non-obvious bits (all handled in `chat-app/devstack.config.ts`
- **Seal: one server, `sealThreshold: 1`.** Two local-keygen servers collide in codegen in 0.1.1.
- **Dev-wallet: register it yourself.** `devstackVitePlugin()` only aliases `@generated` — it does NOT
inject a wallet. devstack runs the wallet *server* (funded accounts); the app builds a
`DevstackSignerAdapter` from `@generated/dapp-kit/config`, `register()`s a `DevWallet`, and
`mountDevWallet()`s the panel. Serialize sign calls (the DevWallet allows one pending sign).
`DevstackSignerAdapter` from `@generated/dapp-kit/config` and hands it to dApp Kit via
`devWalletInitializer({ mountUI: true })` in `createDAppKit({ walletInitializers })`.
Serialize sign calls (the DevWallet allows one pending sign; dApp Kit doesn't queue).

## Relayer

Expand Down Expand Up @@ -93,7 +94,8 @@ experiment now, add `walrus({ local })` + `walCoin` to `devstack.config.ts` and
- **Reset:** `devstack wipe --yes`; hard Docker reset `docker rm -f $(docker ps -aq --filter name=devstack)`
(`devstack prune` only removes idle groups). Re-emit codegen: `devstack apply`.
- **"A signing request is already pending"** = concurrent signs vs the DevWallet's one-pending model → serialize app-side.
- **Connect hangs at "Confirm connection in the wallet"** = no `mountDevWallet()`.
- **Connect hangs at "Confirm connection in the wallet"** = no approval UI — `mountUI: true` missing
in `devWalletInitializer(...)`, or `walletInitializers` not passed to `createDAppKit`.
- **Relayer "grpc-status header missing, HTTP 400"** = gRPC through Traefik → use the host-published port.

## Cross-links
Expand Down
52 changes: 32 additions & 20 deletions .claude/skills/spin-up-local-devstack/reference/NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,41 @@ two-server topology). `serverConfigs` comes from `@generated/seal/local.ts`.
(`devstack/dist/build-integrations/vite/index.mjs`). It does **not** inject a wallet. devstack's
`wallet({ accounts })` runs a server (`http://…:6173`, funded accounts, keys stay server-side; signs
over `/api/v1/devstack/*`) and emits `@generated/dapp-kit/config.ts` (`walletUrl`, `pairUrl#token`,
`chain: sui:local` — sensitive, gitignored). The browser app wires it up itself:
`chain: sui:local` — sensitive, gitignored). The browser app wires it up itself — with
`@mysten/dapp-kit-react`, via dev-wallet's first-class initializer:

```ts
import { DevWallet } from '@mysten-incubation/dev-wallet';
import { devWalletInitializer } from '@mysten-incubation/dev-wallet';
import { DevstackSignerAdapter, parseDevstackToken } from '@mysten-incubation/dev-wallet/adapters';
import { mountDevWallet } from '@mysten-incubation/dev-wallet/ui';
const a = new DevstackSignerAdapter({ serverOrigin: dappKitConfig.walletUrl, token: parseDevstackToken(dappKitConfig.pairUrl) });
await a.initialize(); // fetch the funded accounts from the server
const w = new DevWallet({ adapters: [a], networks: { localnet: suiNetwork.rpcUrl } });
w.register(); // wallet-standard -> dapp-kit's ConnectButton lists it
mountDevWallet(w); // floating panel = where connect/sign approvals happen
const dAppKit = createDAppKit({
networks: ['localnet'],
createClient: (network) => new SuiGrpcClient({ network, baseUrl: suiNetwork.rpcUrl }),
walletInitializers: [
devWalletInitializer({
adapters: [new DevstackSignerAdapter({ serverOrigin: dappKitConfig.walletUrl, token: parseDevstackToken(dappKitConfig.pairUrl) })],
createInitialAccount: false, // accounts come from the devstack wallet server
mountUI: true, // floating panel = where connect/sign approvals happen
}),
],
});
// dApp Kit registers the wallet (ConnectButton lists it) and initializes the adapter.
```

(Pre-migration, on the deprecated `@mysten/dapp-kit`, this was manual: `new DevWallet({ adapters })` +
`wallet.register()` + `mountDevWallet(wallet)` — the initializer replaces all three.)

Gotchas hit:
- **`DevWalletClient` (popup) is the wrong client** — that's for a standalone served wallet UI; the
devstack server is an HTTP signing API, consumed via `DevstackSignerAdapter`.
- **`createDevstackAdapterFromManifest` wants the nested runtime `Manifest`**, not the flat generated
`dappKitConfig` — use `DevstackSignerAdapter` + `parseDevstackToken` directly.
- **Without `mountDevWallet`, connect hangs** at "Confirm connection in the wallet" — no UI to confirm in.
- **Without `mountUI: true` (in `devWalletInitializer`), connect hangs** at "Confirm connection in the
wallet" — no UI to confirm in.
- **The DevWallet allows only one pending sign** → concurrent session-key + tx signs throw "a signing
request is already pending" (`dev-wallet/dist/wallet/dev-wallet.mjs`). Real wallets queue; the
DevWallet doesn't. Fix: serialize sign calls app-side (a promise chain around `signPersonalMessage`
in `MessagingClientContext.tsx`). React StrictMode double-render makes the race more likely.
DevWallet doesn't. dApp Kit doesn't queue either. Fix: serialize sign calls app-side — the chat-app
subclasses `CurrentAccountSigner` with a promise chain around `signPersonalMessage`
(`src/lib/queued-signer.ts`). React StrictMode double-render makes the race more likely.

## 4. Relayer on devstack — use the host-published port, not the routed one

Expand Down Expand Up @@ -130,8 +142,8 @@ queries) may need the same schema port if exercised on localnet.
| `SealError`/`register` "object … unavailable" | two seal servers on one signer | one server, dedicated signer (§2) |
| `CodegenEmitterCollision` | two Seal package bindings | one Seal server (§2) |
| `HostServiceAcquireError`/`exit` (`ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`) | `pnpm exec vite` hit pnpm-v11 deps-purge in a non-TTY child | run vite via `node_modules/.bin/vite` in the host-service `script` |
| connect hangs "Confirm connection in the wallet" | no wallet UI mounted | `mountDevWallet(wallet)` (§3) |
| "A signing request is already pending" | DevWallet one-pending-sign + concurrent signs | serialize signs app-side (§3) |
| connect hangs "Confirm connection in the wallet" | no wallet UI mounted | `devWalletInitializer({ mountUI: true })` in `walletInitializers` (§3) |
| "A signing request is already pending" | DevWallet one-pending-sign + concurrent signs | queued `CurrentAccountSigner` subclass (§3) |
| relayer "grpc-status header missing, HTTP 400" | gRPC through Traefik | point at the host-published validator port (§4) |

## 7. Operational
Expand All @@ -145,11 +157,11 @@ queries) may need the same schema port if exercised on localnet.
- Logs: `devstack up --renderer plain`. Reset: `devstack wipe --yes`. `devstack prune --yes` only
removes *idle* groups (a live validator isn't idle). Re-emit codegen: `devstack apply`.

## 8. Follow-up: migrate the chat-app to `@mysten/dapp-kit-react`
## 8. dapp-kit migration (done)

The chat-app is on the **deprecated** `@mysten/dapp-kit` (JSON-RPC-only, never gets gRPC/GraphQL).
`@mysten/dapp-kit-react` (sui-2.0) is the path forward and is gRPC-native. Browser-gRPC-on-localnet is
**proven working** here (the messaging base client is already `SuiGrpcClient`), so the migration's main
unknown is retired. The dev-wallet wiring is identical on both stacks (same `DevstackSignerAdapter`),
so migration is a separate, deliberate PR — see the cost-benefit (workaround-now / migrate-next).
Guide: <https://sdk.mystenlabs.com/sui/migrations/sui-2.0/llms.txt>.
The chat-app now runs on `@mysten/dapp-kit-react` (sui-2.0, gRPC-native): `createDAppKit` +
`DAppKitProvider`, `useCurrentClient()` as the base client source, imperative
`dAppKit.signAndExecuteTransaction` / `signPersonalMessage` (no mutation hooks), and the dev-wallet
registered through `walletInitializers` (section 3). The sign-request serialization stays — dApp Kit
does not queue wallet requests and the DevWallet still allows only one pending sign.
Guide used: <https://sdk.mystenlabs.com/sui/migrations/sui-2.0/llms.txt>.
1 change: 1 addition & 0 deletions chat-app/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Sui Network
VITE_SUI_NETWORK=testnet
# gRPC-Web endpoint (the public fullnodes serve gRPC and JSON-RPC on the same host)
VITE_SUI_RPC_URL=https://fullnode.testnet.sui.io:443
VITE_SUI_GRAPHQL_URL=https://sui-testnet.mystenlabs.com/graphql

Expand Down
19 changes: 10 additions & 9 deletions chat-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ A fully functional chat application built on the Sui Groups SDK ecosystem, showc
- **On-chain permission management** — Group membership and fine-grained permissions (send, read, edit, delete, admin) are enforced on-chain via `@mysten/sui-groups`, with the relayer and Seal key servers independently verifying permissions.
- **Atomic multi-step transactions** — The SDK's `call` layer composes multiple on-chain operations (e.g., remove member + rotate encryption key) into a single Programmable Transaction Block (PTB), guaranteeing atomicity.
- **Encrypted file attachments via Walrus** — Files are encrypted with the group's DEK and stored on [Walrus](https://docs.wal.app) decentralized storage. Metadata (filename, MIME type, size) is encrypted separately.
- **Wallet-based authentication** — No usernames or passwords. Users authenticate with their Sui wallet via `@mysten/dapp-kit`.
- **Wallet-based authentication** — No usernames or passwords. Users authenticate with their Sui wallet via `@mysten/dapp-kit-react`.
- **Real-time message delivery** — New messages appear automatically via HTTP polling with the SDK's `subscribe()` API.

**Tech stack:** React 19 · Vite · Tailwind CSS · @mysten/dapp-kit
**Tech stack:** React 19 · Vite · Tailwind CSS · @mysten/dapp-kit-react

---

Expand All @@ -45,7 +45,7 @@ The app provides working code for common integration patterns: wallet setup, ses

| Feature | Description | SDK Method |
|---------------------------|-------------------------------------------------------|---------------------------------|
| Wallet connection | Connect/disconnect via @mysten/dapp-kit ConnectButton | `useCurrentAccount()` |
| Wallet connection | Connect/disconnect via @mysten/dapp-kit-react ConnectButton | `useCurrentAccount()` |
| SDK client initialization | Create SuiStackMessagingClient from wallet signer | `createSuiStackMessagingClient()` |

### Group Management
Expand Down Expand Up @@ -117,7 +117,8 @@ The app follows a 3-layer architecture:
### Layer 1 — Browser (React SPA)

- React 19 UI with Tailwind CSS styling
- @mysten/dapp-kit for Sui wallet integration (ConnectButton, useCurrentAccount, useSignPersonalMessage)
- @mysten/dapp-kit-react for Sui wallet integration (ConnectButton, useCurrentAccount, `dAppKit.signPersonalMessage`)
- Sui access is gRPC: `VITE_SUI_RPC_URL` must point at a gRPC-Web-capable endpoint (the public fullnodes serve gRPC and JSON-RPC on the same host)
- Custom `MessagingClientProvider` context that creates and memoizes the SDK client

### Layer 2 — SDK (in-browser, client-side)
Expand All @@ -139,7 +140,7 @@ The app follows a 3-layer architecture:
### Key Architectural Decisions

- **Group discovery via Sui GraphQL** — query `MemberAdded`/`MemberRemoved` events from the indexer, cached in localStorage for instant sidebar rendering
- **Tier 2 session keys** — dapp-kit's `signPersonalMessage` feeds the SDK callback config
- **Tier 1 session keys** — a queued `CurrentAccountSigner` subclass feeds the SDK's signer-based config
- **Atomic PTBs via SDK `call` layer** — composed admin operations in single transactions
- **Distributed state** — React component state + localStorage caching (no centralized store needed)

Expand All @@ -153,8 +154,9 @@ The app follows a 3-layer architecture:
|-------------------------------|-----------|-------------------------|
| `@mysten/sui-stack-messaging` | workspace | E2E encrypted messaging |
| `@mysten/sui-groups` | workspace | Permission management |
| `@mysten/dapp-kit` | ^0.x | Wallet adapter |
| `@mysten/sui` | ^2.6 | Sui RPC client |
| `@mysten/dapp-kit-react` | ^2.0 | Wallet adapter (React) |
| `@mysten/dapp-kit-core` | ^1.3 | Wallet adapter core |
| `@mysten/sui` | ^2.17 | Sui gRPC client |
| `@mysten/seal` | ^1.1 | Threshold encryption |

### Application Dependencies
Expand All @@ -164,7 +166,6 @@ The app follows a 3-layer architecture:
| React | ^19 | UI framework |
| Vite | ^6 | Build tool |
| Tailwind CSS | ^4 | Styling |
| TanStack Query | ^5 | Server state management |

### Infrastructure

Expand All @@ -183,7 +184,7 @@ The app follows a 3-layer architecture:
|----------------------|---------------------------------------------------------------------------------------------------------------------------|
| Groups SDK source | [permissioned-groups](../ts-sdks/packages/permissioned-groups), [messaging-groups](../ts-sdks/packages/messaging-groups/) |
| System Design doc | [SYSTEM_DESIGN.md](./docs/SYSTEM_DESIGN.md) |
| @mysten/dapp-kit | https://sdk.mystenlabs.com/dapp-kit |
| @mysten/dapp-kit-react | https://sdk.mystenlabs.com/dapp-kit |
| Sui TypeScript SDK | https://sdk.mystenlabs.com/typescript |
| Walrus Documentation | https://docs.wal.app |
| Seal Documentation | https://docs.seal.mystenlabs.com |
5 changes: 3 additions & 2 deletions chat-app/devstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,9 @@ export default defineDevstack({
// @generated/packages (packages.sui_stack_messaging.packageId) -> packageConfig.messaging
// @generated/sui/network (suiNetwork.{rpcUrl,graphqlUrl}) -> base gRPC client + GraphQL
// @generated/dapp-kit/config (walletUrl,pairUrl,chain) -> dev-wallet registration + network
// The shim also REGISTERS the dev-wallet (DevstackSignerAdapter + DevWallet + mountDevWallet) —
// devstack runs the wallet server, but the app must register it. MessagingNamespace + Version +
// The shim also builds the dev-wallet `walletInitializers` entry (DevstackSignerAdapter wrapped in
// devWalletInitializer) — devstack runs the wallet server, but the app hands the initializer to
// createDAppKit (src/lib/dapp-kit.ts), which registers the wallet. MessagingNamespace + Version +
// the merged sui_groups id are recovered from the publish tx at bootstrap (not surfaced by codegen);
// mvr overrides `@local-pkg/sui-stack-messaging` / `@local-pkg/sui-groups` are set on the base client.
//
Expand Down
Loading
Loading