@cypher-zk/sdk 0.3.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.
- package/README.md +341 -141
- 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
|
-
[]()
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
8
|
+
[](./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
|
-
##
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
32
|
-
|
|
|
33
|
-
| `react` ^18
|
|
34
|
-
| `@tanstack/react-query` ^5 | `@cypher-zk/sdk/react`
|
|
35
|
-
|
|
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
|
-
##
|
|
96
|
+
## Quickstart
|
|
38
97
|
|
|
39
|
-
###
|
|
98
|
+
### 1. Construct a client
|
|
40
99
|
|
|
41
100
|
```ts
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
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
|
-
|
|
114
|
+
```ts
|
|
51
115
|
const gs = await client.globalState.fetch();
|
|
52
|
-
console.log("
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
124
|
+
### 3. Place a private bet — with live progress
|
|
63
125
|
|
|
64
126
|
```ts
|
|
65
|
-
import {
|
|
127
|
+
import { computeFees } from "@cypher-zk/sdk";
|
|
66
128
|
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
227
|
+
// Later
|
|
228
|
+
sub.unsubscribe();
|
|
229
|
+
sub2.unsubscribe();
|
|
103
230
|
|
|
104
|
-
// Poll-based (no WS
|
|
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,
|
|
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
|
-
###
|
|
244
|
+
### 7. Phase helpers
|
|
112
245
|
|
|
113
246
|
```ts
|
|
114
|
-
import {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## React hooks
|
|
127
267
|
|
|
128
268
|
```tsx
|
|
129
|
-
import {
|
|
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(
|
|
146
|
-
const
|
|
294
|
+
function MarketView({ marketId }: { marketId: bigint }) {
|
|
295
|
+
const { data: market, isLoading } = useMarket(marketId);
|
|
296
|
+
const [stage, setStage] = useState<ActionProgressEvent | null>(null);
|
|
147
297
|
|
|
148
|
-
|
|
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
|
-
<
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
314
|
+
marketId,
|
|
315
|
+
side: 1,
|
|
316
|
+
amountUsdc: 5_000_000n,
|
|
317
|
+
onProgress: setStage,
|
|
163
318
|
})
|
|
164
319
|
}
|
|
165
320
|
>
|
|
166
|
-
Bet
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
356
|
+
## Layout
|
|
190
357
|
|
|
191
358
|
```
|
|
192
359
|
cypher-sdk/
|
|
193
|
-
├── src/ #
|
|
194
|
-
│ ├── config.ts #
|
|
195
|
-
│ ├── pda.ts # PDA
|
|
196
|
-
│ ├── wallet.ts # Wallet adapter
|
|
197
|
-
│ ├── fees.ts #
|
|
198
|
-
│ ├── deadlines.ts #
|
|
199
|
-
│ ├── errors.ts #
|
|
200
|
-
│ ├── client.ts # CypherClient —
|
|
201
|
-
│ ├── accounts/ #
|
|
202
|
-
│ ├── arcium/ # MPC glue (cipher,
|
|
203
|
-
│ ├── instructions/ #
|
|
204
|
-
│ ├── actions/ #
|
|
205
|
-
│ ├── events/ #
|
|
206
|
-
│ ├── idl/ #
|
|
207
|
-
│ └── node/ #
|
|
208
|
-
├── react/src/ #
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
│ ├──
|
|
212
|
-
│
|
|
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/ #
|
|
217
|
-
├── integration/ #
|
|
218
|
-
└── devnet/ #
|
|
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
|
-
|
|
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
|
-
|
|
393
|
+
## Cluster strategy
|
|
224
394
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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-
|
|
415
|
+
rpc: "https://my-helius-endpoint.example",
|
|
244
416
|
arciumClusterOffset: 456,
|
|
245
|
-
expectedMint:
|
|
417
|
+
expectedMint: KNOWN_MINTS.devnetCSDC,
|
|
246
418
|
},
|
|
247
419
|
});
|
|
248
420
|
```
|
|
249
421
|
|
|
422
|
+
---
|
|
423
|
+
|
|
250
424
|
## Scripts
|
|
251
425
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
bun
|
|
255
|
-
bun
|
|
256
|
-
bun run test:unit
|
|
257
|
-
bun run test:integration
|
|
258
|
-
bun run test:devnet
|
|
259
|
-
bun run
|
|
260
|
-
bun run
|
|
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
|
|
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
|
|
468
|
+
Source Available — use is permitted, redistribution is not. See
|
|
469
|
+
[LICENSE](./LICENSE) for full terms.
|