@cypher-zk/sdk 0.2.0 → 0.4.0

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.
Files changed (2) hide show
  1. package/README.md +341 -141
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,268 +2,468 @@
2
2
 
3
3
  > **TypeScript SDK for the [Cypher](https://cyphers.live) privacy-preserving prediction market on Solana**, powered by [Arcium](https://arcium.com) MPC.
4
4
 
5
- [![Tests](https://img.shields.io/badge/tests-107%20passing-brightgreen)]()
6
- [![License: Source Available](https://img.shields.io/badge/license-Source%20Available-orange.svg)]()
5
+ [![tests](https://img.shields.io/badge/tests-130%20passing-brightgreen)]()
6
+ [![typecheck](https://img.shields.io/badge/typecheck-clean-brightgreen)]()
7
+ [![bundle](https://img.shields.io/badge/dist-ESM%20%2B%20.d.ts-blue)]()
8
+ [![license](https://img.shields.io/badge/license-Source%20Available-orange.svg)](./LICENSE)
9
+
10
+ A framework-agnostic core (Node, Bun, browser) with an optional React
11
+ hooks subpath and end-to-end progress callbacks so frontends can render
12
+ fine-grained loading state across every multi-step on-chain flow.
7
13
 
8
14
  ---
9
15
 
10
- ## Features
16
+ ## Why this SDK exists
17
+
18
+ Cypher is a Solana program that wraps prediction-market state in
19
+ [Arcium MPC](https://docs.arcium.com) so user bets stay encrypted on
20
+ chain. Talking to it directly involves wiring:
21
+
22
+ - A typed Anchor `Program` against the program's IDL
23
+ - Per-flow Arcium queue accounts (cluster offset, mempool, comp def, …)
24
+ - `x25519` keypair generation + Rescue cipher encryption for each bet
25
+ - Polling the computation account until the MPC nodes finalize the
26
+ callback
27
+ - Refetching the position/market after the callback updates state
11
28
 
12
- - **Full instruction coverage** builders for all 26 Anchor instructions
13
- - **High-level action helpers** encrypt send await MPC callback → refetch, in one call
14
- - **Typed event parsing** — 7 on-chain events decoded and narrowed via discriminated unions
15
- - **Real-time & poll-based subscriptions** — WebSocket `onLogs` + HTTP fallback
16
- - **Framework-agnostic core** — works in any TS environment (Node, Deno, Bun, browser)
17
- - **React hooks layer** — optional `@cypher/sdk/react` with React Query integration
18
- - **Cluster-aware** — reads `GlobalState.accepted_mint` on-chain (devnet=CSDC, mainnet=USDC)
19
- - **Deterministic test suite** — 107 unit tests, 581 assertions
29
+ This SDK gives you a single `client.actions.placeBet({...})` call that
30
+ does all of that, with progress events for the UI and discriminated-
31
+ union typed events for the indexer.
20
32
 
21
- ## Installation
33
+ ```ts
34
+ const result = await client.actions.placeBet({
35
+ payer: wallet.publicKey,
36
+ user: wallet.publicKey,
37
+ marketId: 7n,
38
+ side: 1, // 1 = YES
39
+ amountUsdc: 5_000_000n, // $5 (6 decimals)
40
+ onProgress: (e) => updateLoaderUI(e.stage, e.message),
41
+ });
42
+ // Persist result.userKeypair.privateKey under the wallet's key — that's
43
+ // the only way to later decrypt this position to claim a payout.
44
+ ```
45
+
46
+ ## What's in the box
47
+
48
+ - **All 26 program instructions** with typed builders that return raw
49
+ `TransactionInstruction`s (compose, simulate, bundle freely).
50
+ - **Seven high-level action helpers** (`createMarket`,
51
+ `createMarketMulti`, `placeBet`, `resolveMarket`, `claimPayout`,
52
+ `claimRefund`, `cancelMarket`, `withdrawCreatorFunds`) that hide the
53
+ "encrypt → send → await MPC callback → refetch" choreography.
54
+ - **Async progress events** on every multi-step action, so frontends can
55
+ render `Encrypting…` → `Submitting…` → `Awaiting MPC nodes…` instead
56
+ of one generic spinner.
57
+ - **Typed event surface** — 7 discriminated-union events with
58
+ `parseLogs`, `parseLogsFor`, `subscribeAll`, 7 `onXxx` helpers, and a
59
+ WebSocket-less `pollEvents` fallback. Decoded fields are camelCase
60
+ `bigint`s, matching the typed interfaces 1:1.
61
+ - **Account fetch + memcmp filters** for every program account, with
62
+ byte offsets drift-tested against the IDL.
63
+ - **React hooks** (`@cypher-zk/sdk/react`): `CypherProvider`,
64
+ `useGlobalState`, `useMarket`, `useMarkets`, `useUserPositions`,
65
+ `usePlaceBet`, `useResolveMarket`, `useClaimPayout`, `useClaimRefund`,
66
+ `useCreateMarket`, `useCancelMarket`, `useMarketEvents` — all built on
67
+ TanStack Query with sensible cache-invalidation defaults.
68
+ - **Cluster-agnostic at runtime** — reads `GlobalState.accepted_mint`
69
+ on-chain, so the same build works against any deployment (devnet CSDC,
70
+ mainnet USDC, localnet test mint).
71
+ - **130 unit tests** covering PDA derivations, fee math, deadline
72
+ phases, IDL drift, Arcium offsets, encryption round-trip, event
73
+ parser round-trip with all 7 event types, action input validation,
74
+ and React hook wiring. Plus opt-in localnet integration and devnet
75
+ smoke suites.
76
+
77
+ ## Install
22
78
 
23
79
  ```bash
24
- npm install @cypher-zk/sdk
25
- # or
26
80
  bun add @cypher-zk/sdk
81
+ # or
82
+ npm install @cypher-zk/sdk
27
83
  ```
28
84
 
29
85
  ### Peer dependencies
30
86
 
31
- | Package | Required for |
32
- | -------------------------- | --------------------------------- |
33
- | `react` ^18/^19 | `@cypher-zk/sdk/react` hooks only |
34
- | `@tanstack/react-query` ^5 | `@cypher-zk/sdk/react` hooks only |
35
- | `typescript` ^5 | Type checking |
87
+ | Package | Required for |
88
+ | --- | --- |
89
+ | `react` ^18 \|\| ^19 | `@cypher-zk/sdk/react` subpath only |
90
+ | `@tanstack/react-query` ^5 | `@cypher-zk/sdk/react` subpath only |
91
+
92
+ Core SDK works in any TypeScript environment with no peer requirements.
93
+
94
+ ---
36
95
 
37
- ## Quick Start
96
+ ## Quickstart
38
97
 
39
- ### Core SDK
98
+ ### 1. Construct a client
40
99
 
41
100
  ```ts
42
- import { CypherClient, keypairToWallet } from "@cypher-zk/sdk";
43
- import { Connection, Keypair } from "@solana/web3.js";
101
+ import { Connection } from "@solana/web3.js";
102
+ import { CypherClient } from "@cypher-zk/sdk";
44
103
 
45
- const connection = new Connection("https://api.devnet.solana.com");
46
- const wallet = keypairToWallet(Keypair.generate());
104
+ const connection = new Connection("https://api.devnet.solana.com", "confirmed");
47
105
 
106
+ // Wallet can be:
107
+ // - any @solana/wallet-adapter wallet (browser)
108
+ // - `keypairToWallet(Keypair)` (Node / scripts / tests)
48
109
  const client = new CypherClient({ connection, wallet, cluster: "devnet" });
110
+ ```
111
+
112
+ ### 2. Read protocol state
49
113
 
50
- // Fetch protocol state
114
+ ```ts
51
115
  const gs = await client.globalState.fetch();
52
- console.log("Market counter:", gs.marketCounter);
116
+ console.log("Protocol fee:", gs.protocolFeeRate, "bps");
53
117
 
54
- // Fetch a specific market
55
118
  const market = await client.markets.fetch(0n);
56
- console.log("Question:", market?.question);
57
-
58
- // Fetch all active markets
59
- const active = await client.markets.byState(0); // MarketState.Active
119
+ const active = await client.markets.byState(0); // MarketState.Active
120
+ const mine = await client.markets.byCreator(wallet.publicKey);
121
+ const myBets = await client.positions.byUser(wallet.publicKey);
60
122
  ```
61
123
 
62
- ### Place a Private Bet (end-to-end)
124
+ ### 3. Place a private bet — with live progress
63
125
 
64
126
  ```ts
65
- import { MarketType } from "@cypher-zk/sdk";
127
+ import { computeFees } from "@cypher-zk/sdk";
66
128
 
67
- const result = await client.actions.placeBet({
129
+ // Preview the fee split before showing a confirm modal:
130
+ const preview = computeFees(5_000_000n, {
131
+ protocolFeeRateBps: gs.protocolFeeRate,
132
+ lpFeeRateBps: gs.lpFeeRate,
133
+ });
134
+ console.log("Net stake:", preview.netAmount, "after fees:", preview.protocolFee + preview.lpFee);
135
+
136
+ // Fire the end-to-end flow:
137
+ const { signature, position, userKeypair } = await client.actions.placeBet({
68
138
  payer: wallet.publicKey,
69
139
  user: wallet.publicKey,
70
140
  marketId: 0n,
71
- marketType: MarketType.YesNo,
72
- side: 0, // 0=YES, 1=NO
73
- amountUsdc: 5_000_000n, // $5 USDC (6 decimals)
141
+ side: 1, // 0 = NO, 1 = YES
142
+ amountUsdc: 5_000_000n, // $5 (USDC has 6 decimals)
143
+ onProgress: ({ stage, message, signature }) => {
144
+ // stage ∈ "validating" | "fetching-state" | "encrypting" | "submitting"
145
+ // | "awaiting-callback" | "refetching" | "done"
146
+ console.log(stage, message ?? "", signature ?? "");
147
+ },
74
148
  });
75
149
 
76
- console.log("Tx:", result.signature);
77
- console.log("Entry odds:", result.position?.entryOdds);
150
+ // IMPORTANT: persist userKeypair.privateKey somewhere the user controls
151
+ // (e.g. localStorage encrypted under the wallet's signature) — without
152
+ // it, the user cannot decrypt this position later when claiming.
153
+ saveSecretForLater(position!.market, userKeypair.privateKey);
78
154
  ```
79
155
 
80
- ### Create a Market
156
+ The progress callback lets you drive multi-step UI:
157
+
158
+ ```
159
+ [ Validating … ] ◄ instant, client-side
160
+ [ Fetching protocol state … ]
161
+ [ Encrypting your bet … ]
162
+ [ Submitting transaction … ] ◄ tx signature available here
163
+ [ Awaiting MPC nodes (~10s) … ]
164
+ [ Updating position … ]
165
+ [ Done! ]
166
+ ```
167
+
168
+ ### 4. Create a market
81
169
 
82
170
  ```ts
83
- const result = await client.actions.createMarket({
171
+ const { marketId, marketPda, signature } = await client.actions.createMarket({
84
172
  creator: wallet.publicKey,
85
173
  question: "Will ETH hit $10k by end of 2026?",
86
174
  closeTime: BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 3600),
87
- category: 0, // Crypto
175
+ category: 0, // MarketCategory.Crypto
176
+ onProgress: (e) => console.log(e.stage),
88
177
  });
178
+ ```
179
+
180
+ Multi-outcome variant — `createMarketMulti` with `numOutcomes: 2 | 3 | 4`.
89
181
 
90
- console.log("Market ID:", result.marketId);
91
- console.log("Market PDA:", result.marketPda.toBase58());
182
+ ### 5. Resolve, claim payout, claim refund
183
+
184
+ ```ts
185
+ // Resolver (oracle / DAO):
186
+ await client.actions.resolveMarket({
187
+ payer: wallet.publicKey,
188
+ resolver: wallet.publicKey,
189
+ marketId,
190
+ outcomeValue: 1,
191
+ onProgress: (e) => console.log(e.stage),
192
+ });
193
+
194
+ // Winning bettor:
195
+ await client.actions.claimPayout({
196
+ payer: wallet.publicKey,
197
+ user: wallet.publicKey,
198
+ marketId,
199
+ });
200
+
201
+ // Bettor on an unresolved market (past resolution_deadline):
202
+ await client.actions.claimRefund({
203
+ payer: wallet.publicKey,
204
+ user: wallet.publicKey,
205
+ marketId,
206
+ });
92
207
  ```
93
208
 
94
- ### Subscribe to Events
209
+ Each refuses pre-flight if the market isn't in the right phase
210
+ (`marketPhase` returns `"claimable"` for payout, `"refundable"` for
211
+ refund) so the user never burns gas on a guaranteed-to-fail tx.
212
+
213
+ ### 6. Subscribe to events
95
214
 
96
215
  ```ts
97
- // Real-time (WebSocket)
216
+ // Real-time (WebSocket):
98
217
  const sub = client.events.onBetPlaced((data) => {
99
- console.log("Bet placed! Odds:", data.entryOdds);
218
+ // `data` is BetPlacedEvent — fully typed (camelCase fields, bigint amounts):
219
+ console.log(`Bet placed on market ${data.market.toBase58()} — odds ${data.entryOdds}`);
220
+ });
221
+
222
+ // Generic, typed by name:
223
+ const sub2 = client.events.subscribe("MarketResolvedEvent", (data) => {
224
+ console.log("Outcome:", data.outcome, "Payout ratio:", data.payoutRatio);
100
225
  });
101
226
 
102
- // Later: sub.unsubscribe();
227
+ // Later
228
+ sub.unsubscribe();
229
+ sub2.unsubscribe();
103
230
 
104
- // Poll-based (no WS needed)
231
+ // Poll-based fallback (no WS required):
105
232
  const recent = await client.events.pollEvents({ limit: 20 });
106
- for (const { event } of recent) {
107
- console.log(event.name, event.data);
233
+ for (const { event, signature, slot } of recent) {
234
+ console.log(event.name, "in tx", signature, "at slot", slot);
108
235
  }
236
+
237
+ // Parse events out of a known transaction:
238
+ import { parseLogs, parseLogsFor } from "@cypher-zk/sdk";
239
+ const tx = await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0 });
240
+ const allEvents = parseLogs(tx?.meta?.logMessages ?? []);
241
+ const payoutsOnly = parseLogsFor(tx?.meta?.logMessages ?? [], "PayoutClaimedEvent");
109
242
  ```
110
243
 
111
- ### Parse Transaction Logs
244
+ ### 7. Phase helpers
112
245
 
113
246
  ```ts
114
- import { parseLogs } from "@cypher-zk/sdk";
115
-
116
- const tx = await connection.getTransaction(sig, { commitment: "confirmed" });
117
- const events = parseLogs(tx.meta?.logMessages ?? []);
118
-
119
- for (const evt of events) {
120
- if (evt.name === "MarketResolvedEvent") {
121
- console.log("Outcome:", evt.data.outcome);
122
- }
247
+ import { marketPhase, projectDeadlines } from "@cypher-zk/sdk";
248
+
249
+ // Compute what action is currently available on a market:
250
+ switch (marketPhase(market)) {
251
+ case "betting": /* show "Bet" button */ break;
252
+ case "awaitingResolve": /* show "Pending resolution" */ break;
253
+ case "claimable": /* show "Claim payout" */ break;
254
+ case "refundable": /* show "Claim refund" */ break;
255
+ case "expired": /* show "Admin sweep eligible" */ break;
256
+ case "cancelled": /* show "Cancelled" */ break;
123
257
  }
258
+
259
+ // Preview deadlines for a draft market the user is filling in:
260
+ const projected = projectDeadlines(BigInt(closeTimeSec));
261
+ console.log("Resolution deadline:", new Date(Number(projected.resolutionDeadline) * 1000));
124
262
  ```
125
263
 
126
- ## React Hooks
264
+ ---
265
+
266
+ ## React hooks
127
267
 
128
268
  ```tsx
129
- import { CypherProvider, useMarket, usePlaceBet } from "@cypher-zk/sdk/react";
269
+ import {
270
+ CypherProvider,
271
+ useGlobalState,
272
+ useMarket,
273
+ useMarkets,
274
+ usePlaceBet,
275
+ } from "@cypher-zk/sdk/react";
130
276
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
277
+ import { useState } from "react";
278
+ import { CypherClient, keypairToWallet, MarketState } from "@cypher-zk/sdk";
279
+ import type { ActionProgressEvent } from "@cypher-zk/sdk";
131
280
 
132
281
  const queryClient = new QueryClient();
282
+ const client = new CypherClient({ connection, wallet, cluster: "devnet" });
133
283
 
134
284
  function App() {
135
285
  return (
136
286
  <QueryClientProvider client={queryClient}>
137
287
  <CypherProvider client={client}>
138
- <MarketView />
288
+ <MarketView marketId={0n} />
139
289
  </CypherProvider>
140
290
  </QueryClientProvider>
141
291
  );
142
292
  }
143
293
 
144
- function MarketView() {
145
- const { data: market, isLoading } = useMarket(0n);
146
- const { mutateAsync: placeBet, isPending } = usePlaceBet();
294
+ function MarketView({ marketId }: { marketId: bigint }) {
295
+ const { data: market, isLoading } = useMarket(marketId);
296
+ const [stage, setStage] = useState<ActionProgressEvent | null>(null);
147
297
 
148
- if (isLoading) return <p>Loading…</p>;
298
+ const placeBet = usePlaceBet({
299
+ onSuccess: ({ userKeypair }) => persistUserSecret(userKeypair.privateKey),
300
+ });
301
+
302
+ if (isLoading) return <p>Loading market…</p>;
303
+ if (!market) return <p>Market not found.</p>;
149
304
 
150
305
  return (
151
306
  <div>
152
- <h1>{market?.question}</h1>
307
+ <h2>{market.question}</h2>
153
308
  <button
154
- disabled={isPending}
309
+ disabled={placeBet.isPending}
155
310
  onClick={() =>
156
- placeBet({
311
+ placeBet.mutate({
157
312
  payer: wallet.publicKey,
158
313
  user: wallet.publicKey,
159
- marketId: 0n,
160
- marketType: 0,
161
- side: 0,
162
- amountUsdc: 1_000_000n,
314
+ marketId,
315
+ side: 1,
316
+ amountUsdc: 5_000_000n,
317
+ onProgress: setStage,
163
318
  })
164
319
  }
165
320
  >
166
- Bet YES ($1)
321
+ {placeBet.isPending && stage ? `Bet ${stage.stage}` : "Bet $5 YES"}
167
322
  </button>
323
+ {placeBet.error && <p style={{ color: "crimson" }}>{placeBet.error.message}</p>}
168
324
  </div>
169
325
  );
170
326
  }
171
327
  ```
172
328
 
173
- ### Available Hooks
329
+ ### Available hooks
330
+
331
+ | Hook | Kind | Description |
332
+ | --- | --- | --- |
333
+ | `useGlobalState()` | Query | Protocol config (fees, mint, admin, counter) |
334
+ | `useMarket(id)` | Query | Single market by ID |
335
+ | `useMarkets(filter?)` | Query | All/filtered markets (creator, state) |
336
+ | `useUserPositions(user)` | Query | All bet positions for a user |
337
+ | `usePlaceBet()` | Mutation | End-to-end private bet |
338
+ | `useCreateMarket()` | Mutation | Create a new market |
339
+ | `useResolveMarket()` | Mutation | Submit outcome + await reveal |
340
+ | `useClaimPayout()` | Mutation | Claim winning payout |
341
+ | `useClaimRefund()` | Mutation | Claim refund on unresolved market |
342
+ | `useCancelMarket()` | Mutation | Cancel a zero-bet market |
343
+ | `useMarketEvents()` | Subscription | Live event stream (component-scoped) |
344
+
345
+ Mutation hooks auto-invalidate the relevant query keys on success. Read
346
+ hooks expose their `queryKey` factories (`marketKeys.one(id)`,
347
+ `positionKeys.byUser(user)`, `globalStateKeys.all`) for manual
348
+ invalidation.
349
+
350
+ Live example under [`examples/react-vite/`](./examples/react-vite/) — a
351
+ small Vite app that renders the protocol state, lists active markets,
352
+ and drives `usePlaceBet` with live progress.
174
353
 
175
- | Hook | Type | Description |
176
- | ------------------------ | ------------ | ----------------------------------- |
177
- | `useGlobalState()` | Query | Protocol config (fees, mint, admin) |
178
- | `useMarket(id)` | Query | Single market by ID |
179
- | `useMarkets(filter?)` | Query | All/filtered markets |
180
- | `useUserPositions(user)` | Query | User's bet positions |
181
- | `usePlaceBet()` | Mutation | End-to-end private bet |
182
- | `useResolveMarket()` | Mutation | Resolve a market |
183
- | `useClaimPayout()` | Mutation | Claim winning payout |
184
- | `useClaimRefund()` | Mutation | Claim refund on unresolved market |
185
- | `useCreateMarket()` | Mutation | Create a new market |
186
- | `useCancelMarket()` | Mutation | Cancel a zero-bet market |
187
- | `useMarketEvents()` | Subscription | Live event stream |
354
+ ---
188
355
 
189
- ## Architecture
356
+ ## Layout
190
357
 
191
358
  ```
192
359
  cypher-sdk/
193
- ├── src/ # Framework-agnostic core
194
- │ ├── config.ts # Program ID, cluster presets, on-chain constants
195
- │ ├── pda.ts # PDA derivation helpers
196
- │ ├── wallet.ts # Wallet adapter interface
197
- │ ├── fees.ts # Fee calculation (mirrors on-chain math)
198
- │ ├── deadlines.ts # Market lifecycle phase computation
199
- │ ├── errors.ts # Typed error codes + parsing
200
- │ ├── client.ts # CypherClient — unified entry point
201
- │ ├── accounts/ # Account fetchers (GlobalState, Market, Position, LP)
202
- │ ├── arcium/ # MPC glue (cipher, encoding, queue accounts, offsets)
203
- │ ├── instructions/ # Raw instruction builders (26 instructions)
204
- │ ├── actions/ # High-level flows (encrypt send → callback → refetch)
205
- │ ├── events/ # Event parser + WebSocket/poll subscriptions
206
- │ ├── idl/ # Synced Anchor IDL (source of truth)
207
- │ └── node/ # Node-only admin helpers
208
- ├── react/src/ # Optional React hooks layer
209
- ├── CypherProvider.tsx # Context + provider
210
- ├── useGlobalState.ts # Read hook
211
- │ ├── useMarket.ts # Read hooks (single + filtered)
212
- ├── useUserPositions.ts # Read hook
213
- │ ├── mutations.ts # Write hooks (all actions)
214
- │ └── useMarketEvents.ts # Live event subscription hook
360
+ ├── src/ # framework-agnostic core (ESM)
361
+ │ ├── config.ts # program ID, cluster presets, constants
362
+ │ ├── pda.ts # PDA derivations
363
+ │ ├── wallet.ts # Wallet interface + Keypair adapter
364
+ │ ├── fees.ts # mirrors on-chain fee math
365
+ │ ├── deadlines.ts # market phase computation
366
+ │ ├── errors.ts # CypherErrorCode + Anchor error walker
367
+ │ ├── client.ts # CypherClient — single import surface
368
+ │ ├── accounts/ # fetch + memcmp filter helpers per account
369
+ │ ├── arcium/ # MPC glue (cipher, queue accounts, offsets)
370
+ │ ├── instructions/ # raw TransactionInstruction builders (26 ix)
371
+ │ ├── actions/ # high-level flows + progress events
372
+ │ ├── events/ # typed event parser + WS/poll subscriptions
373
+ │ ├── idl/ # synced Anchor IDL (source of truth)
374
+ │ └── node/ # node-only admin helpers
375
+ ├── react/src/ # @cypher-zk/sdk/react hooks subpath
376
+ ├── examples/react-vite/ # minimal Vite app smoke
377
+ ├── docs/
378
+ │ ├── architecture.md # three-surface model + module map
379
+ └── flows.md # one diagram per user flow
215
380
  └── tests/
216
- ├── unit/ # 107 tests, 581 assertions
217
- ├── integration/ # Localnet lifecycle (INTEGRATION=1)
218
- └── devnet/ # Devnet smoke test (DEVNET=1)
381
+ ├── unit/ # 130 tests, 660+ assertions — pure, no chain
382
+ ├── integration/ # INTEGRATION=1 — Arcium localnet lifecycle
383
+ └── devnet/ # DEVNET=1 devnet read-only + opt-in writes
219
384
  ```
220
385
 
221
- ## Cluster Configuration
386
+ See [`docs/architecture.md`](./docs/architecture.md) for the three-surface
387
+ model (circuit / program / client), the account topology, and the full
388
+ runtime flow of a private bet. See [`docs/flows.md`](./docs/flows.md) for
389
+ per-flow ASCII diagrams.
390
+
391
+ ---
222
392
 
223
- The SDK is **cluster-agnostic** — it reads `GlobalState.accepted_mint` on-chain at runtime:
393
+ ## Cluster strategy
224
394
 
225
- | Cluster | Accepted Mint | Arcium Cluster Offset |
226
- | ---------- | --------------------- | --------------------- |
227
- | `devnet` | CSDC (`8AF9BABN…`) | `456` |
228
- | `mainnet` | USDC (`EPjFWdd5…`) | TBD |
229
- | `localnet` | CSDC (same as devnet) | `1116522022` |
395
+ The SDK is **cluster-agnostic at runtime**: it reads the accepted SPL mint
396
+ from `GlobalState.accepted_mint` on every flow rather than hard-coding
397
+ CSDC vs USDC. The same build works against any Cypher deployment.
230
398
 
231
- Override cluster detection:
399
+ | Cluster | RPC default | Accepted mint | Arcium offset |
400
+ | --- | --- | --- | --- |
401
+ | `devnet` | `api.devnet.solana.com` | CSDC (`8AF9BABN…`) | `456` |
402
+ | `mainnet` | `api.mainnet-beta.solana.com` | USDC (`EPjFWdd5…`) | (set at deploy) |
403
+ | `localnet` | `localhost:8899` | CSDC (test build) | `1116522022` |
232
404
 
233
405
  ```ts
234
- // Explicit preset
406
+ // Explicit preset:
235
407
  const client = new CypherClient({ connection, wallet, cluster: "devnet" });
236
408
 
237
- // Fully custom
409
+ // Custom config — override RPC and/or Arcium cluster offset:
238
410
  const client = new CypherClient({
239
411
  connection,
240
412
  wallet,
241
413
  cluster: {
242
414
  name: "devnet",
243
- rpc: "https://my-helius-rpc.com",
415
+ rpc: "https://my-helius-endpoint.example",
244
416
  arciumClusterOffset: 456,
245
- expectedMint: new PublicKey("8AF9BABNWwEhipRxtXPYoWSZW24SKjUn6YqbKd9ZqhwB"),
417
+ expectedMint: KNOWN_MINTS.devnetCSDC,
246
418
  },
247
419
  });
248
420
  ```
249
421
 
422
+ ---
423
+
250
424
  ## Scripts
251
425
 
252
- ```bash
253
- bun run build # Build dist/ with tsup
254
- bun run typecheck # Type-check without emitting
255
- bun run test # All unit tests
256
- bun run test:unit # Unit tests only
257
- bun run test:integration # Integration (requires localnet)
258
- bun run test:devnet # Devnet smoke (requires DEVNET_KEYPAIR)
259
- bun run sync:idl # Copy IDL from ../cypher-main
260
- bun run clean # Remove dist/
261
- ```
426
+ | Command | Purpose |
427
+ | --- | --- |
428
+ | `bun install` | Install deps |
429
+ | `bun test` | All unit suites (gates skipped) |
430
+ | `bun run test:unit` | Unit-only |
431
+ | `bun run test:integration` | `INTEGRATION=1` Arcium localnet lifecycle |
432
+ | `bun run test:devnet` | `DEVNET=1` devnet read-only + opt-in writes |
433
+ | `bun run typecheck` | `tsc --noEmit` (strict) |
434
+ | `bun run build` | ESM + `.d.ts` via tsup → `dist/` |
435
+ | `bun run sync:idl` | Re-copy IDL + types from `../cypher-main` |
436
+ | `bun run prepublishOnly` | sync IDL → typecheck → unit tests → build |
437
+
438
+ ---
262
439
 
263
440
  ## Security
264
441
 
265
- The Cypher protocol has undergone a comprehensive security audit (June 2026). All 9 findings — including 3 critical, 2 high, and 4 medium/low — have been fully remediated and verified in a second-round audit. See `cypher-main/audit_report.md` for the full report.
442
+ The Cypher protocol underwent a two-round security audit. All 9
443
+ findings — 3 critical, 2 high, 4 medium/low — have been remediated and
444
+ verified. See `cypher-main/audit_report.md` for the full report.
445
+
446
+ The SDK adds defense-in-depth client-side:
447
+
448
+ - **`computeFees` mirrors the on-chain math exactly** — `placeBet`
449
+ pre-asserts the encrypted `net_amount` will match what the contract
450
+ computes from the gross. The contract's circuit also verifies this
451
+ on-chain (audit fix H-1) — the SDK guard surfaces the failure as a
452
+ clean rejection instead of a wasted MPC computation.
453
+ - **`marketPhase` blocks known-bad claims** — `claimPayout` /
454
+ `claimRefund` refuse to send if the market isn't in the right phase,
455
+ saving users from on-chain `MarketStillOpen` / `ClaimPeriodExpired`
456
+ rejections.
457
+ - **Position double-claim guard** — `claimPayout` reads `position.claimed`
458
+ before submitting (the contract enforces it again as audit fix H-2).
459
+ - **Strict ciphertext-length enforcement** — every builder that takes a
460
+ `Uint8Array` ciphertext asserts `length === 32` before assembling the
461
+ ix, catching the "combined ciphertext arrays" mistake from the Arcium
462
+ skill before it hits the program.
463
+
464
+ ---
266
465
 
267
466
  ## License
268
467
 
269
- Source Available — use is permitted, redistribution is not. See [LICENSE](./LICENSE) for full terms.
468
+ Source Available — use is permitted, redistribution is not. See
469
+ [LICENSE](./LICENSE) for full terms.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cypher-zk/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript SDK for the Cypher prediction market (Arcium + Anchor on Solana)",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",