@cypher-zk/sdk 0.4.0 → 0.6.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 CHANGED
@@ -2,15 +2,22 @@
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-130%20passing-brightgreen)]()
5
+ [![tests](https://img.shields.io/badge/tests-150%20passing-brightgreen)]()
6
6
  [![typecheck](https://img.shields.io/badge/typecheck-clean-brightgreen)]()
7
7
  [![bundle](https://img.shields.io/badge/dist-ESM%20%2B%20.d.ts-blue)]()
8
+ [![version](https://img.shields.io/badge/version-0.5.0-blue)]()
8
9
  [![license](https://img.shields.io/badge/license-Source%20Available-orange.svg)](./LICENSE)
9
10
 
10
11
  A framework-agnostic core (Node, Bun, browser) with an optional React
11
12
  hooks subpath and end-to-end progress callbacks so frontends can render
12
13
  fine-grained loading state across every multi-step on-chain flow.
13
14
 
15
+ > **v0.2 — dispute window.** Reveal callbacks now land the market in
16
+ > `PendingResolution`, not `Resolved`. A configurable 24h–48h challenge
17
+ > window lets anyone flag a wrong outcome before claims open. See
18
+ > [§ Dispute window (v0.2)](#dispute-window-v02) for the full flow and
19
+ > three new instructions / actions / hooks.
20
+
14
21
  ---
15
22
 
16
23
  ## Why this SDK exists
@@ -35,8 +42,8 @@ const result = await client.actions.placeBet({
35
42
  payer: wallet.publicKey,
36
43
  user: wallet.publicKey,
37
44
  marketId: 7n,
38
- side: 1, // 1 = YES
39
- amountUsdc: 5_000_000n, // $5 (6 decimals)
45
+ side: 1, // 1 = YES
46
+ amountUsdc: 5_000_000n, // $5 (6 decimals)
40
47
  onProgress: (e) => updateLoaderUI(e.stage, e.message),
41
48
  });
42
49
  // Persist result.userKeypair.privateKey under the wallet's key — that's
@@ -45,34 +52,45 @@ const result = await client.actions.placeBet({
45
52
 
46
53
  ## What's in the box
47
54
 
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`,
55
+ - **All 29 program instructions** with typed builders that return raw
56
+ `TransactionInstruction`s (compose, simulate, bundle freely) — 3 admin,
57
+ 8 init comp def, 4 market lifecycle, 2 bet, 2 resolve, 4 claim, plus
58
+ **3 dispute-window** instructions added in v0.2.
59
+ - **Ten high-level action helpers** (`createMarket`,
51
60
  `createMarketMulti`, `placeBet`, `resolveMarket`, `claimPayout`,
52
- `claimRefund`, `cancelMarket`, `withdrawCreatorFunds`) that hide the
53
- "encrypt send → await MPC callback → refetch" choreography.
61
+ `claimRefund`, `cancelMarket`, `withdrawCreatorFunds`, plus
62
+ **`flagResolution`, `finalizeResolution`, `adminOverrideResolution`**)
63
+ that hide the "encrypt → send → await MPC callback → refetch"
64
+ choreography.
54
65
  - **Async progress events** on every multi-step action, so frontends can
55
66
  render `Encrypting…` → `Submitting…` → `Awaiting MPC nodes…` instead
56
67
  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.
68
+ - **Typed event surface** — **10 discriminated-union events** (7 core +
69
+ 3 dispute-window in v0.2) with `parseLogs`, `parseLogsFor`,
70
+ `subscribeAll`, `onXxx` helpers, and a WebSocket-less `pollEvents`
71
+ fallback. Decoded fields are camelCase `bigint`s, matching the typed
72
+ interfaces 1:1.
61
73
  - **Account fetch + memcmp filters** for every program account, with
62
74
  byte offsets drift-tested against the IDL.
63
75
  - **React hooks** (`@cypher-zk/sdk/react`): `CypherProvider`,
64
76
  `useGlobalState`, `useMarket`, `useMarkets`, `useUserPositions`,
65
77
  `usePlaceBet`, `useResolveMarket`, `useClaimPayout`, `useClaimRefund`,
66
- `useCreateMarket`, `useCancelMarket`, `useMarketEvents` — all built on
67
- TanStack Query with sensible cache-invalidation defaults.
78
+ `useCreateMarket`, `useCancelMarket`, **`useFlagResolution`,
79
+ `useFinalizeResolution`, `useAdminOverrideResolution`** (v0.2),
80
+ `useMarketEvents` — all built on TanStack Query with sensible
81
+ cache-invalidation defaults.
82
+ - **Phase-aware UI gating** — `marketPhase(market)` returns one of nine
83
+ literal values including `pendingResolution`, `awaitingFinalize`, and
84
+ `disputed` so buttons only render when the corresponding ix is
85
+ actually clickable.
68
86
  - **Cluster-agnostic at runtime** — reads `GlobalState.accepted_mint`
69
87
  on-chain, so the same build works against any deployment (devnet CSDC,
70
88
  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.
89
+ - **150 unit tests** (712 assertions) covering PDA derivations, fee
90
+ math, deadline phases, IDL drift, Arcium offsets, encryption
91
+ round-trip, event parser round-trip with all 10 event types, action
92
+ input validation (including dispute-window phase gating), and React
93
+ hook wiring. Plus opt-in localnet integration and devnet smoke suites.
76
94
 
77
95
  ## Install
78
96
 
@@ -84,9 +102,9 @@ npm install @cypher-zk/sdk
84
102
 
85
103
  ### Peer dependencies
86
104
 
87
- | Package | Required for |
88
- | --- | --- |
89
- | `react` ^18 \|\| ^19 | `@cypher-zk/sdk/react` subpath only |
105
+ | Package | Required for |
106
+ | -------------------------- | ----------------------------------- |
107
+ | `react` ^18 \|\| ^19 | `@cypher-zk/sdk/react` subpath only |
90
108
  | `@tanstack/react-query` ^5 | `@cypher-zk/sdk/react` subpath only |
91
109
 
92
110
  Core SDK works in any TypeScript environment with no peer requirements.
@@ -116,8 +134,8 @@ const gs = await client.globalState.fetch();
116
134
  console.log("Protocol fee:", gs.protocolFeeRate, "bps");
117
135
 
118
136
  const market = await client.markets.fetch(0n);
119
- const active = await client.markets.byState(0); // MarketState.Active
120
- const mine = await client.markets.byCreator(wallet.publicKey);
137
+ const active = await client.markets.byState(0); // MarketState.Active
138
+ const mine = await client.markets.byCreator(wallet.publicKey);
121
139
  const myBets = await client.positions.byUser(wallet.publicKey);
122
140
  ```
123
141
 
@@ -131,15 +149,20 @@ const preview = computeFees(5_000_000n, {
131
149
  protocolFeeRateBps: gs.protocolFeeRate,
132
150
  lpFeeRateBps: gs.lpFeeRate,
133
151
  });
134
- console.log("Net stake:", preview.netAmount, "after fees:", preview.protocolFee + preview.lpFee);
152
+ console.log(
153
+ "Net stake:",
154
+ preview.netAmount,
155
+ "after fees:",
156
+ preview.protocolFee + preview.lpFee,
157
+ );
135
158
 
136
159
  // Fire the end-to-end flow:
137
160
  const { signature, position, userKeypair } = await client.actions.placeBet({
138
161
  payer: wallet.publicKey,
139
162
  user: wallet.publicKey,
140
163
  marketId: 0n,
141
- side: 1, // 0 = NO, 1 = YES
142
- amountUsdc: 5_000_000n, // $5 (USDC has 6 decimals)
164
+ side: 1, // 0 = NO, 1 = YES
165
+ amountUsdc: 5_000_000n, // $5 (USDC has 6 decimals)
143
166
  onProgress: ({ stage, message, signature }) => {
144
167
  // stage ∈ "validating" | "fetching-state" | "encrypting" | "submitting"
145
168
  // | "awaiting-callback" | "refetching" | "done"
@@ -172,7 +195,11 @@ const { marketId, marketPda, signature } = await client.actions.createMarket({
172
195
  creator: wallet.publicKey,
173
196
  question: "Will ETH hit $10k by end of 2026?",
174
197
  closeTime: BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 3600),
175
- category: 0, // MarketCategory.Crypto
198
+ category: 0, // MarketCategory.Crypto
199
+ // v0.2+: optional. Defaults to MIN_CHALLENGE_PERIOD_SECS (24h).
200
+ // Must be in [MIN_CHALLENGE_PERIOD_SECS, MAX_CHALLENGE_PERIOD_SECS]
201
+ // (24h–48h). Pin shorter for prediction markets that settle fast.
202
+ challengePeriod: 24 * 3600,
176
203
  onProgress: (e) => console.log(e.stage),
177
204
  });
178
205
  ```
@@ -210,13 +237,21 @@ Each refuses pre-flight if the market isn't in the right phase
210
237
  (`marketPhase` returns `"claimable"` for payout, `"refundable"` for
211
238
  refund) so the user never burns gas on a guaranteed-to-fail tx.
212
239
 
240
+ > **v0.2 note:** `resolveMarket` no longer flips the market to
241
+ > `Resolved`. The reveal callback now sets `state = PendingResolution`
242
+ > and starts the challenge window. `claimPayout` opens only after
243
+ > `finalizeResolution` / `adminOverrideResolution` runs — see
244
+ > [§ Dispute window (v0.2)](#dispute-window-v02) below.
245
+
213
246
  ### 6. Subscribe to events
214
247
 
215
248
  ```ts
216
249
  // Real-time (WebSocket):
217
250
  const sub = client.events.onBetPlaced((data) => {
218
251
  // `data` is BetPlacedEvent — fully typed (camelCase fields, bigint amounts):
219
- console.log(`Bet placed on market ${data.market.toBase58()} — odds ${data.entryOdds}`);
252
+ console.log(
253
+ `Bet placed on market ${data.market.toBase58()} — odds ${data.entryOdds}`,
254
+ );
220
255
  });
221
256
 
222
257
  // Generic, typed by name:
@@ -224,6 +259,17 @@ const sub2 = client.events.subscribe("MarketResolvedEvent", (data) => {
224
259
  console.log("Outcome:", data.outcome, "Payout ratio:", data.payoutRatio);
225
260
  });
226
261
 
262
+ // v0.2: dispute-window events
263
+ const sub3 = client.events.onResolutionFlagged((data) => {
264
+ console.log("Market disputed by:", data.flaggedBy.toBase58());
265
+ });
266
+ const sub4 = client.events.onMarketFinalized((data) => {
267
+ console.log("Market finalized — claims now open. Outcome:", data.outcome);
268
+ });
269
+ const sub5 = client.events.onResolutionOverridden((data) => {
270
+ console.log(`Admin override: ${data.oldOutcome} → ${data.newOutcome}`);
271
+ });
272
+
227
273
  // Later
228
274
  sub.unsubscribe();
229
275
  sub2.unsubscribe();
@@ -236,9 +282,14 @@ for (const { event, signature, slot } of recent) {
236
282
 
237
283
  // Parse events out of a known transaction:
238
284
  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");
285
+ const tx = await connection.getTransaction(sig, {
286
+ maxSupportedTransactionVersion: 0,
287
+ });
288
+ const allEvents = parseLogs(tx?.meta?.logMessages ?? []);
289
+ const payoutsOnly = parseLogsFor(
290
+ tx?.meta?.logMessages ?? [],
291
+ "PayoutClaimedEvent",
292
+ );
242
293
  ```
243
294
 
244
295
  ### 7. Phase helpers
@@ -248,18 +299,196 @@ import { marketPhase, projectDeadlines } from "@cypher-zk/sdk";
248
299
 
249
300
  // Compute what action is currently available on a market:
250
301
  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;
302
+ case "betting":
303
+ /* show "Bet" button */ break;
304
+ case "awaitingResolve":
305
+ /* show "Pending resolution" */ break;
306
+ case "pendingResolution":
307
+ /* v0.2: in challenge window — show countdown + "Flag" */ break;
308
+ case "awaitingFinalize":
309
+ /* v0.2: window elapsed — show "Finalize" button */ break;
310
+ case "disputed":
311
+ /* v0.2: flagged — admin override required */ break;
312
+ case "claimable":
313
+ /* show "Claim payout" */ break;
314
+ case "refundable":
315
+ /* show "Claim refund" */ break;
316
+ case "expired":
317
+ /* show "Admin sweep eligible" */ break;
318
+ case "cancelled":
319
+ /* show "Cancelled" */ break;
257
320
  }
258
321
 
259
322
  // Preview deadlines for a draft market the user is filling in:
260
323
  const projected = projectDeadlines(BigInt(closeTimeSec));
261
- console.log("Resolution deadline:", new Date(Number(projected.resolutionDeadline) * 1000));
324
+ console.log(
325
+ "Resolution deadline:",
326
+ new Date(Number(projected.resolutionDeadline) * 1000),
327
+ );
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Dispute window (v0.2)
333
+
334
+ After the reveal callback lands, the market enters a configurable
335
+ **24h–48h** challenge window (`market.state === PendingResolution = 4`)
336
+ during which anyone can flag a wrong outcome. Only after the window
337
+ closes does the market move to `Resolved` and claims open.
338
+
262
339
  ```
340
+ Resolver calls resolveMarket
341
+
342
+ ▼ reveal_market_outcome_* (Arcium MPC)
343
+ callback writes: outcome, revealedPool*, payoutRatio
344
+ state = PendingResolution
345
+ challengeDeadline = now + challenge_period
346
+
347
+ ┌─────────────────────────────────────┐
348
+ │ Challenge window (24h–48h) │
349
+ │ Anyone may flagResolution │
350
+ │ → market.disputed = true │
351
+ └─────────────────────────────────────┘
352
+ │ │
353
+ (undisputed) (disputed)
354
+ │ │
355
+ ▼ ▼
356
+ finalizeResolution adminOverrideResolution
357
+ (anyone, post-window) (admin only, recomputes payout_ratio)
358
+ │ │
359
+ └──────────┬───────────────┘
360
+
361
+ state = Resolved
362
+ claim_deadline + refund_deadline set
363
+ claimPayout opens
364
+ ```
365
+
366
+ ### Three new actions
367
+
368
+ ```ts
369
+ // 1. ANYONE during the challenge window — flag a wrong resolution
370
+ await client.actions.flagResolution({
371
+ flagger: wallet.publicKey,
372
+ marketId,
373
+ onProgress: (e) => console.log(e.stage),
374
+ });
375
+
376
+ // 2. ANYONE after the window closes undisputed — finalize → state = Resolved
377
+ await client.actions.finalizeResolution({
378
+ caller: wallet.publicKey,
379
+ marketId,
380
+ onProgress: (e) => console.log(e.stage),
381
+ });
382
+
383
+ // 3. ADMIN ONLY when market.disputed === true — re-resolve with corrected outcome
384
+ // Recomputes payout_ratio from the already-revealed plaintext pools.
385
+ await client.actions.adminOverrideResolution({
386
+ admin: wallet.publicKey,
387
+ marketId,
388
+ outcomeValue: 1,
389
+ onProgress: (e) => console.log(e.stage),
390
+ });
391
+ ```
392
+
393
+ Each emits `validating → submitting → refetching → done` progress
394
+ stages and refuses pre-flight if the market isn't in the right phase
395
+ (SDK throws clean errors pointing at the next valid step — e.g.
396
+ "market is in 'pendingResolution' — call finalizeResolution first").
397
+
398
+ ### Phase gating
399
+
400
+ `marketPhase(market)` returns the three new values whenever
401
+ `state === PendingResolution`:
402
+
403
+ | `marketPhase` | Meaning | Clickable |
404
+ | --------------------- | ------------------------------------------ | -------------------------------------- |
405
+ | `"pendingResolution"` | inside challenge window, not flagged | `flagResolution` (any user) |
406
+ | `"awaitingFinalize"` | window elapsed, not flagged | `finalizeResolution` (any user) |
407
+ | `"disputed"` | flagged during window | `adminOverrideResolution` (admin only) |
408
+ | `"claimable"` | finalized → window closed (state=Resolved) | `claimPayout` |
409
+
410
+ `claimPayoutAction` and `useClaimPayout` reject pre-flight in the
411
+ first three phases with a hint to call `finalizeResolution` first.
412
+
413
+ ### React example
414
+
415
+ ```tsx
416
+ import {
417
+ useMarket,
418
+ useFlagResolution,
419
+ useFinalizeResolution,
420
+ } from "@cypher-zk/sdk/react";
421
+ import { marketPhase } from "@cypher-zk/sdk";
422
+
423
+ function ChallengeWindowControls({ marketId }: { marketId: bigint }) {
424
+ const { data: market } = useMarket(marketId, { refetchInterval: 5_000 });
425
+ const flag = useFlagResolution();
426
+ const finalize = useFinalizeResolution();
427
+ if (!market) return null;
428
+
429
+ const phase = marketPhase(market);
430
+ if (phase === "pendingResolution") {
431
+ return (
432
+ <>
433
+ <p>
434
+ Challenge closes at{" "}
435
+ {new Date(Number(market.challengeDeadline) * 1000).toLocaleString()}
436
+ </p>
437
+ <button
438
+ onClick={() => flag.mutate({ flagger: wallet.publicKey!, marketId })}
439
+ >
440
+ Flag this resolution
441
+ </button>
442
+ </>
443
+ );
444
+ }
445
+ if (phase === "awaitingFinalize") {
446
+ return (
447
+ <button
448
+ onClick={() => finalize.mutate({ caller: wallet.publicKey!, marketId })}
449
+ >
450
+ Finalize resolution
451
+ </button>
452
+ );
453
+ }
454
+ if (phase === "disputed") {
455
+ return <p>Awaiting admin override.</p>;
456
+ }
457
+ return null;
458
+ }
459
+ ```
460
+
461
+ ### Defaults & bounds
462
+
463
+ | Constant | Value | Notes |
464
+ | --------------------------- | ----------------- | --------------------------------- |
465
+ | `MIN_CHALLENGE_PERIOD_SECS` | `24 * 3600` (24h) | Action helpers default here |
466
+ | `MAX_CHALLENGE_PERIOD_SECS` | `48 * 3600` (48h) | Hard ceiling enforced by contract |
467
+
468
+ The high-level `client.actions.createMarket` makes `challengePeriod`
469
+ optional and defaults to `MIN_CHALLENGE_PERIOD_SECS`. The raw
470
+ `createMarketIx` builder requires it — out-of-range values throw
471
+ client-side before the tx is built.
472
+
473
+ ### Six new error codes
474
+
475
+ | Code | Name | When |
476
+ | ------ | --------------------------- | ------------------------------------- |
477
+ | `6036` | `InvalidChallengePeriod` | `challengePeriod` outside 24h–48h |
478
+ | `6037` | `NotPendingResolution` | flag/finalize/override on wrong state |
479
+ | `6038` | `ChallengePeriodNotElapsed` | finalize called too early |
480
+ | `6039` | `ChallengePeriodElapsed` | flag called after window closed |
481
+ | `6040` | `MarketDisputed` | finalize on a flagged market |
482
+ | `6041` | `MarketNotDisputed` | admin override on a clean market |
483
+
484
+ Use `parseCypherError(err)` to extract a typed `CypherErrorCode` for
485
+ branching.
486
+
487
+ ### Three new events
488
+
489
+ `ResolutionFlaggedEvent`, `MarketFinalizedEvent`,
490
+ `ResolutionOverriddenEvent` are added to the discriminated `CypherEvent`
491
+ union and have matching `client.events.on*` helpers.
263
492
 
264
493
  ---
265
494
 
@@ -300,7 +529,7 @@ function MarketView({ marketId }: { marketId: bigint }) {
300
529
  });
301
530
 
302
531
  if (isLoading) return <p>Loading market…</p>;
303
- if (!market) return <p>Market not found.</p>;
532
+ if (!market) return <p>Market not found.</p>;
304
533
 
305
534
  return (
306
535
  <div>
@@ -320,7 +549,9 @@ function MarketView({ marketId }: { marketId: bigint }) {
320
549
  >
321
550
  {placeBet.isPending && stage ? `Bet → ${stage.stage}` : "Bet $5 YES"}
322
551
  </button>
323
- {placeBet.error && <p style={{ color: "crimson" }}>{placeBet.error.message}</p>}
552
+ {placeBet.error && (
553
+ <p style={{ color: "crimson" }}>{placeBet.error.message}</p>
554
+ )}
324
555
  </div>
325
556
  );
326
557
  }
@@ -328,19 +559,22 @@ function MarketView({ marketId }: { marketId: bigint }) {
328
559
 
329
560
  ### Available hooks
330
561
 
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) |
562
+ | Hook | Kind | Description |
563
+ | --------------------------------------- | ------------ | ----------------------------------------------------------------- |
564
+ | `useGlobalState()` | Query | Protocol config (fees, mint, admin, counter) |
565
+ | `useMarket(id)` | Query | Single market by ID |
566
+ | `useMarkets(filter?)` | Query | All/filtered markets (creator, state) |
567
+ | `useUserPositions(user)` | Query | All bet positions for a user |
568
+ | `usePlaceBet()` | Mutation | End-to-end private bet |
569
+ | `useCreateMarket()` | Mutation | Create a new market |
570
+ | `useResolveMarket()` | Mutation | Submit outcome + await reveal |
571
+ | `useClaimPayout()` | Mutation | Claim winning payout |
572
+ | `useClaimRefund()` | Mutation | Claim refund on unresolved market |
573
+ | `useCancelMarket()` | Mutation | Cancel a zero-bet market |
574
+ | `useFlagResolution()` _(v0.2)_ | Mutation | Flag a pending resolution during the challenge window |
575
+ | `useFinalizeResolution()` _(v0.2)_ | Mutation | Finalize a pending resolution after the window elapses undisputed |
576
+ | `useAdminOverrideResolution()` _(v0.2)_ | Mutation | Admin re-resolves a disputed market |
577
+ | `useMarketEvents()` | Subscription | Live event stream (component-scoped) |
344
578
 
345
579
  Mutation hooks auto-invalidate the relevant query keys on success. Read
346
580
  hooks expose their `queryKey` factories (`marketKeys.one(id)`,
@@ -367,7 +601,7 @@ cypher-sdk/
367
601
  │ ├── client.ts # CypherClient — single import surface
368
602
  │ ├── accounts/ # fetch + memcmp filter helpers per account
369
603
  │ ├── arcium/ # MPC glue (cipher, queue accounts, offsets)
370
- │ ├── instructions/ # raw TransactionInstruction builders (26 ix)
604
+ │ ├── instructions/ # raw TransactionInstruction builders (29 ix, v0.2)
371
605
  │ ├── actions/ # high-level flows + progress events
372
606
  │ ├── events/ # typed event parser + WS/poll subscriptions
373
607
  │ ├── idl/ # synced Anchor IDL (source of truth)
@@ -378,7 +612,7 @@ cypher-sdk/
378
612
  │ ├── architecture.md # three-surface model + module map
379
613
  │ └── flows.md # one diagram per user flow
380
614
  └── tests/
381
- ├── unit/ # 130 tests, 660+ assertions — pure, no chain
615
+ ├── unit/ # 150 tests, 712 assertions — pure, no chain
382
616
  ├── integration/ # INTEGRATION=1 — Arcium localnet lifecycle
383
617
  └── devnet/ # DEVNET=1 — devnet read-only + opt-in writes
384
618
  ```
@@ -396,11 +630,11 @@ The SDK is **cluster-agnostic at runtime**: it reads the accepted SPL mint
396
630
  from `GlobalState.accepted_mint` on every flow rather than hard-coding
397
631
  CSDC vs USDC. The same build works against any Cypher deployment.
398
632
 
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` |
633
+ | Cluster | RPC default | Accepted mint | Arcium offset |
634
+ | ---------- | ----------------------------- | ------------------ | --------------- |
635
+ | `devnet` | `api.devnet.solana.com` | CSDC (`8AF9BABN…`) | `456` |
636
+ | `mainnet` | `api.mainnet-beta.solana.com` | USDC (`EPjFWdd5…`) | (set at deploy) |
637
+ | `localnet` | `localhost:8899` | CSDC (test build) | `1116522022` |
404
638
 
405
639
  ```ts
406
640
  // Explicit preset:
@@ -423,17 +657,17 @@ const client = new CypherClient({
423
657
 
424
658
  ## Scripts
425
659
 
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 |
660
+ | Command | Purpose |
661
+ | -------------------------- | --------------------------------------------- |
662
+ | `bun install` | Install deps |
663
+ | `bun test` | All unit suites (gates skipped) |
664
+ | `bun run test:unit` | Unit-only |
665
+ | `bun run test:integration` | `INTEGRATION=1` — Arcium localnet lifecycle |
666
+ | `bun run test:devnet` | `DEVNET=1` — devnet read-only + opt-in writes |
667
+ | `bun run typecheck` | `tsc --noEmit` (strict) |
668
+ | `bun run build` | ESM + `.d.ts` via tsup → `dist/` |
669
+ | `bun run sync:idl` | Re-copy IDL + types from `../cypher-main` |
670
+ | `bun run prepublishOnly` | sync IDL → typecheck → unit tests → build |
437
671
 
438
672
  ---
439
673
 
@@ -460,6 +694,36 @@ The SDK adds defense-in-depth client-side:
460
694
  `Uint8Array` ciphertext asserts `length === 32` before assembling the
461
695
  ix, catching the "combined ciphertext arrays" mistake from the Arcium
462
696
  skill before it hits the program.
697
+ - **v0.2 dispute-window phase gating** — `claimPayout` rejects pre-flight
698
+ if `market.state === PendingResolution` with a typed error pointing
699
+ the caller at `finalizeResolution`, so users don't pay MPC compute
700
+ fees on a guaranteed-to-fail tx. `flagResolution` /
701
+ `finalizeResolution` / `adminOverrideResolution` validate against
702
+ `market.disputed` + `challengeDeadline` client-side.
703
+
704
+ ## Changelog
705
+
706
+ ### 0.2.0 — dispute window
707
+
708
+ - **NEW**: 3 instructions (`flagResolution`, `finalizeResolution`,
709
+ `adminOverrideResolution`) + 3 actions + 3 React hooks + 3 events
710
+ - 6 error codes (6036–6041).
711
+ - **NEW**: `MarketState.PendingResolution = 4` and three new
712
+ `marketPhase` values: `pendingResolution`, `awaitingFinalize`,
713
+ `disputed`.
714
+ - **NEW**: `Market` gains `challengePeriod`, `challengeDeadline`,
715
+ `disputed` fields; layout offsets shifted.
716
+ - **NEW**: `MIN_CHALLENGE_PERIOD_SECS` / `MAX_CHALLENGE_PERIOD_SECS`
717
+ constants (24h / 48h).
718
+ - **BREAKING**: `createMarketIx` / `createMarketMultiIx` raw builders
719
+ require a new `challengePeriod` arg. The high-level
720
+ `client.actions.createMarket` makes it optional (defaults to 24h).
721
+ - **BREAKING**: `resolveMarket` now leaves the market in
722
+ `PendingResolution`; `claimPayout` opens only after
723
+ `finalizeResolution` or `adminOverrideResolution` runs.
724
+ - **DX**: `claimPayoutAction` pre-flight error now explicitly names
725
+ `finalizeResolution` as the next step when state is
726
+ `PendingResolution`.
463
727
 
464
728
  ---
465
729