@cypher-zk/sdk 0.4.0 → 0.5.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.2.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
@@ -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
 
@@ -173,6 +191,10 @@ const { marketId, marketPda, signature } = await client.actions.createMarket({
173
191
  question: "Will ETH hit $10k by end of 2026?",
174
192
  closeTime: BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 3600),
175
193
  category: 0, // MarketCategory.Crypto
194
+ // v0.2+: optional. Defaults to MIN_CHALLENGE_PERIOD_SECS (24h).
195
+ // Must be in [MIN_CHALLENGE_PERIOD_SECS, MAX_CHALLENGE_PERIOD_SECS]
196
+ // (24h–48h). Pin shorter for prediction markets that settle fast.
197
+ challengePeriod: 24 * 3600,
176
198
  onProgress: (e) => console.log(e.stage),
177
199
  });
178
200
  ```
@@ -210,6 +232,12 @@ Each refuses pre-flight if the market isn't in the right phase
210
232
  (`marketPhase` returns `"claimable"` for payout, `"refundable"` for
211
233
  refund) so the user never burns gas on a guaranteed-to-fail tx.
212
234
 
235
+ > **v0.2 note:** `resolveMarket` no longer flips the market to
236
+ > `Resolved`. The reveal callback now sets `state = PendingResolution`
237
+ > and starts the challenge window. `claimPayout` opens only after
238
+ > `finalizeResolution` / `adminOverrideResolution` runs — see
239
+ > [§ Dispute window (v0.2)](#dispute-window-v02) below.
240
+
213
241
  ### 6. Subscribe to events
214
242
 
215
243
  ```ts
@@ -224,6 +252,17 @@ const sub2 = client.events.subscribe("MarketResolvedEvent", (data) => {
224
252
  console.log("Outcome:", data.outcome, "Payout ratio:", data.payoutRatio);
225
253
  });
226
254
 
255
+ // v0.2: dispute-window events
256
+ const sub3 = client.events.onResolutionFlagged((data) => {
257
+ console.log("Market disputed by:", data.flaggedBy.toBase58());
258
+ });
259
+ const sub4 = client.events.onMarketFinalized((data) => {
260
+ console.log("Market finalized — claims now open. Outcome:", data.outcome);
261
+ });
262
+ const sub5 = client.events.onResolutionOverridden((data) => {
263
+ console.log(`Admin override: ${data.oldOutcome} → ${data.newOutcome}`);
264
+ });
265
+
227
266
  // Later
228
267
  sub.unsubscribe();
229
268
  sub2.unsubscribe();
@@ -248,8 +287,11 @@ import { marketPhase, projectDeadlines } from "@cypher-zk/sdk";
248
287
 
249
288
  // Compute what action is currently available on a market:
250
289
  switch (marketPhase(market)) {
251
- case "betting": /* show "Bet" button */ break;
252
- case "awaitingResolve": /* show "Pending resolution" */ break;
290
+ case "betting": /* show "Bet" button */ break;
291
+ case "awaitingResolve": /* show "Pending resolution" */ break;
292
+ case "pendingResolution": /* v0.2: in challenge window — show countdown + "Flag" */ break;
293
+ case "awaitingFinalize": /* v0.2: window elapsed — show "Finalize" button */ break;
294
+ case "disputed": /* v0.2: flagged — admin override required */ break;
253
295
  case "claimable": /* show "Claim payout" */ break;
254
296
  case "refundable": /* show "Claim refund" */ break;
255
297
  case "expired": /* show "Admin sweep eligible" */ break;
@@ -263,6 +305,162 @@ console.log("Resolution deadline:", new Date(Number(projected.resolutionDeadline
263
305
 
264
306
  ---
265
307
 
308
+ ## Dispute window (v0.2)
309
+
310
+ After the reveal callback lands, the market enters a configurable
311
+ **24h–48h** challenge window (`market.state === PendingResolution = 4`)
312
+ during which anyone can flag a wrong outcome. Only after the window
313
+ closes does the market move to `Resolved` and claims open.
314
+
315
+ ```
316
+ Resolver calls resolveMarket
317
+
318
+ ▼ reveal_market_outcome_* (Arcium MPC)
319
+ callback writes: outcome, revealedPool*, payoutRatio
320
+ state = PendingResolution
321
+ challengeDeadline = now + challenge_period
322
+
323
+ ┌─────────────────────────────────────┐
324
+ │ Challenge window (24h–48h) │
325
+ │ Anyone may flagResolution │
326
+ │ → market.disputed = true │
327
+ └─────────────────────────────────────┘
328
+ │ │
329
+ (undisputed) (disputed)
330
+ │ │
331
+ ▼ ▼
332
+ finalizeResolution adminOverrideResolution
333
+ (anyone, post-window) (admin only, recomputes payout_ratio)
334
+ │ │
335
+ └──────────┬───────────────┘
336
+
337
+ state = Resolved
338
+ claim_deadline + refund_deadline set
339
+ claimPayout opens
340
+ ```
341
+
342
+ ### Three new actions
343
+
344
+ ```ts
345
+ // 1. ANYONE during the challenge window — flag a wrong resolution
346
+ await client.actions.flagResolution({
347
+ flagger: wallet.publicKey,
348
+ marketId,
349
+ onProgress: (e) => console.log(e.stage),
350
+ });
351
+
352
+ // 2. ANYONE after the window closes undisputed — finalize → state = Resolved
353
+ await client.actions.finalizeResolution({
354
+ caller: wallet.publicKey,
355
+ marketId,
356
+ onProgress: (e) => console.log(e.stage),
357
+ });
358
+
359
+ // 3. ADMIN ONLY when market.disputed === true — re-resolve with corrected outcome
360
+ // Recomputes payout_ratio from the already-revealed plaintext pools.
361
+ await client.actions.adminOverrideResolution({
362
+ admin: wallet.publicKey,
363
+ marketId,
364
+ outcomeValue: 1,
365
+ onProgress: (e) => console.log(e.stage),
366
+ });
367
+ ```
368
+
369
+ Each emits `validating → submitting → refetching → done` progress
370
+ stages and refuses pre-flight if the market isn't in the right phase
371
+ (SDK throws clean errors pointing at the next valid step — e.g.
372
+ "market is in 'pendingResolution' — call finalizeResolution first").
373
+
374
+ ### Phase gating
375
+
376
+ `marketPhase(market)` returns the three new values whenever
377
+ `state === PendingResolution`:
378
+
379
+ | `marketPhase` | Meaning | Clickable |
380
+ | --- | --- | --- |
381
+ | `"pendingResolution"` | inside challenge window, not flagged | `flagResolution` (any user) |
382
+ | `"awaitingFinalize"` | window elapsed, not flagged | `finalizeResolution` (any user) |
383
+ | `"disputed"` | flagged during window | `adminOverrideResolution` (admin only) |
384
+ | `"claimable"` | finalized → window closed (state=Resolved) | `claimPayout` |
385
+
386
+ `claimPayoutAction` and `useClaimPayout` reject pre-flight in the
387
+ first three phases with a hint to call `finalizeResolution` first.
388
+
389
+ ### React example
390
+
391
+ ```tsx
392
+ import {
393
+ useMarket,
394
+ useFlagResolution,
395
+ useFinalizeResolution,
396
+ } from "@cypher-zk/sdk/react";
397
+ import { marketPhase } from "@cypher-zk/sdk";
398
+
399
+ function ChallengeWindowControls({ marketId }: { marketId: bigint }) {
400
+ const { data: market } = useMarket(marketId, { refetchInterval: 5_000 });
401
+ const flag = useFlagResolution();
402
+ const finalize = useFinalizeResolution();
403
+ if (!market) return null;
404
+
405
+ const phase = marketPhase(market);
406
+ if (phase === "pendingResolution") {
407
+ return (
408
+ <>
409
+ <p>Challenge closes at {new Date(Number(market.challengeDeadline) * 1000).toLocaleString()}</p>
410
+ <button onClick={() => flag.mutate({ flagger: wallet.publicKey!, marketId })}>
411
+ Flag this resolution
412
+ </button>
413
+ </>
414
+ );
415
+ }
416
+ if (phase === "awaitingFinalize") {
417
+ return (
418
+ <button onClick={() => finalize.mutate({ caller: wallet.publicKey!, marketId })}>
419
+ Finalize resolution
420
+ </button>
421
+ );
422
+ }
423
+ if (phase === "disputed") {
424
+ return <p>Awaiting admin override.</p>;
425
+ }
426
+ return null;
427
+ }
428
+ ```
429
+
430
+ ### Defaults & bounds
431
+
432
+ | Constant | Value | Notes |
433
+ | --- | --- | --- |
434
+ | `MIN_CHALLENGE_PERIOD_SECS` | `24 * 3600` (24h) | Action helpers default here |
435
+ | `MAX_CHALLENGE_PERIOD_SECS` | `48 * 3600` (48h) | Hard ceiling enforced by contract |
436
+
437
+ The high-level `client.actions.createMarket` makes `challengePeriod`
438
+ optional and defaults to `MIN_CHALLENGE_PERIOD_SECS`. The raw
439
+ `createMarketIx` builder requires it — out-of-range values throw
440
+ client-side before the tx is built.
441
+
442
+ ### Six new error codes
443
+
444
+ | Code | Name | When |
445
+ | --- | --- | --- |
446
+ | `6036` | `InvalidChallengePeriod` | `challengePeriod` outside 24h–48h |
447
+ | `6037` | `NotPendingResolution` | flag/finalize/override on wrong state |
448
+ | `6038` | `ChallengePeriodNotElapsed` | finalize called too early |
449
+ | `6039` | `ChallengePeriodElapsed` | flag called after window closed |
450
+ | `6040` | `MarketDisputed` | finalize on a flagged market |
451
+ | `6041` | `MarketNotDisputed` | admin override on a clean market |
452
+
453
+ Use `parseCypherError(err)` to extract a typed `CypherErrorCode` for
454
+ branching.
455
+
456
+ ### Three new events
457
+
458
+ `ResolutionFlaggedEvent`, `MarketFinalizedEvent`,
459
+ `ResolutionOverriddenEvent` are added to the discriminated `CypherEvent`
460
+ union and have matching `client.events.on*` helpers.
461
+
462
+ ---
463
+
266
464
  ## React hooks
267
465
 
268
466
  ```tsx
@@ -340,6 +538,9 @@ function MarketView({ marketId }: { marketId: bigint }) {
340
538
  | `useClaimPayout()` | Mutation | Claim winning payout |
341
539
  | `useClaimRefund()` | Mutation | Claim refund on unresolved market |
342
540
  | `useCancelMarket()` | Mutation | Cancel a zero-bet market |
541
+ | `useFlagResolution()` *(v0.2)* | Mutation | Flag a pending resolution during the challenge window |
542
+ | `useFinalizeResolution()` *(v0.2)* | Mutation | Finalize a pending resolution after the window elapses undisputed |
543
+ | `useAdminOverrideResolution()` *(v0.2)* | Mutation | Admin re-resolves a disputed market |
343
544
  | `useMarketEvents()` | Subscription | Live event stream (component-scoped) |
344
545
 
345
546
  Mutation hooks auto-invalidate the relevant query keys on success. Read
@@ -367,7 +568,7 @@ cypher-sdk/
367
568
  │ ├── client.ts # CypherClient — single import surface
368
569
  │ ├── accounts/ # fetch + memcmp filter helpers per account
369
570
  │ ├── arcium/ # MPC glue (cipher, queue accounts, offsets)
370
- │ ├── instructions/ # raw TransactionInstruction builders (26 ix)
571
+ │ ├── instructions/ # raw TransactionInstruction builders (29 ix, v0.2)
371
572
  │ ├── actions/ # high-level flows + progress events
372
573
  │ ├── events/ # typed event parser + WS/poll subscriptions
373
574
  │ ├── idl/ # synced Anchor IDL (source of truth)
@@ -378,7 +579,7 @@ cypher-sdk/
378
579
  │ ├── architecture.md # three-surface model + module map
379
580
  │ └── flows.md # one diagram per user flow
380
581
  └── tests/
381
- ├── unit/ # 130 tests, 660+ assertions — pure, no chain
582
+ ├── unit/ # 150 tests, 712 assertions — pure, no chain
382
583
  ├── integration/ # INTEGRATION=1 — Arcium localnet lifecycle
383
584
  └── devnet/ # DEVNET=1 — devnet read-only + opt-in writes
384
585
  ```
@@ -460,6 +661,36 @@ The SDK adds defense-in-depth client-side:
460
661
  `Uint8Array` ciphertext asserts `length === 32` before assembling the
461
662
  ix, catching the "combined ciphertext arrays" mistake from the Arcium
462
663
  skill before it hits the program.
664
+ - **v0.2 dispute-window phase gating** — `claimPayout` rejects pre-flight
665
+ if `market.state === PendingResolution` with a typed error pointing
666
+ the caller at `finalizeResolution`, so users don't pay MPC compute
667
+ fees on a guaranteed-to-fail tx. `flagResolution` /
668
+ `finalizeResolution` / `adminOverrideResolution` validate against
669
+ `market.disputed` + `challengeDeadline` client-side.
670
+
671
+ ## Changelog
672
+
673
+ ### 0.2.0 — dispute window
674
+
675
+ - **NEW**: 3 instructions (`flagResolution`, `finalizeResolution`,
676
+ `adminOverrideResolution`) + 3 actions + 3 React hooks + 3 events
677
+ + 6 error codes (6036–6041).
678
+ - **NEW**: `MarketState.PendingResolution = 4` and three new
679
+ `marketPhase` values: `pendingResolution`, `awaitingFinalize`,
680
+ `disputed`.
681
+ - **NEW**: `Market` gains `challengePeriod`, `challengeDeadline`,
682
+ `disputed` fields; layout offsets shifted.
683
+ - **NEW**: `MIN_CHALLENGE_PERIOD_SECS` / `MAX_CHALLENGE_PERIOD_SECS`
684
+ constants (24h / 48h).
685
+ - **BREAKING**: `createMarketIx` / `createMarketMultiIx` raw builders
686
+ require a new `challengePeriod` arg. The high-level
687
+ `client.actions.createMarket` makes it optional (defaults to 24h).
688
+ - **BREAKING**: `resolveMarket` now leaves the market in
689
+ `PendingResolution`; `claimPayout` opens only after
690
+ `finalizeResolution` or `adminOverrideResolution` runs.
691
+ - **DX**: `claimPayoutAction` pre-flight error now explicitly names
692
+ `finalizeResolution` as the next step when state is
693
+ `PendingResolution`.
463
694
 
464
695
  ---
465
696