@cypher-zk/sdk 0.3.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 +569 -138
- package/dist/{client-hOLBWshA.d.ts → client-B0EueahJ.d.ts} +587 -21
- package/dist/{cypher-WAYH63ZA.json → cypher-M5PH6UM5.json} +440 -30
- package/dist/index.d.ts +23 -4
- package/dist/index.js +191 -12
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +6 -2
- package/dist/react/index.js +34 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/idl/cypher.json +440 -30
- package/src/idl/cypher.ts +440 -30
package/README.md
CHANGED
|
@@ -2,268 +2,699 @@
|
|
|
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
|
+
[]()
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
|
|
11
|
+
A framework-agnostic core (Node, Bun, browser) with an optional React
|
|
12
|
+
hooks subpath and end-to-end progress callbacks so frontends can render
|
|
13
|
+
fine-grained loading state across every multi-step on-chain flow.
|
|
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.
|
|
7
20
|
|
|
8
21
|
---
|
|
9
22
|
|
|
10
|
-
##
|
|
23
|
+
## Why this SDK exists
|
|
11
24
|
|
|
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
|
|
25
|
+
Cypher is a Solana program that wraps prediction-market state in
|
|
26
|
+
[Arcium MPC](https://docs.arcium.com) so user bets stay encrypted on
|
|
27
|
+
chain. Talking to it directly involves wiring:
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
- A typed Anchor `Program` against the program's IDL
|
|
30
|
+
- Per-flow Arcium queue accounts (cluster offset, mempool, comp def, …)
|
|
31
|
+
- `x25519` keypair generation + Rescue cipher encryption for each bet
|
|
32
|
+
- Polling the computation account until the MPC nodes finalize the
|
|
33
|
+
callback
|
|
34
|
+
- Refetching the position/market after the callback updates state
|
|
35
|
+
|
|
36
|
+
This SDK gives you a single `client.actions.placeBet({...})` call that
|
|
37
|
+
does all of that, with progress events for the UI and discriminated-
|
|
38
|
+
union typed events for the indexer.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const result = await client.actions.placeBet({
|
|
42
|
+
payer: wallet.publicKey,
|
|
43
|
+
user: wallet.publicKey,
|
|
44
|
+
marketId: 7n,
|
|
45
|
+
side: 1, // 1 = YES
|
|
46
|
+
amountUsdc: 5_000_000n, // $5 (6 decimals)
|
|
47
|
+
onProgress: (e) => updateLoaderUI(e.stage, e.message),
|
|
48
|
+
});
|
|
49
|
+
// Persist result.userKeypair.privateKey under the wallet's key — that's
|
|
50
|
+
// the only way to later decrypt this position to claim a payout.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## What's in the box
|
|
54
|
+
|
|
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`,
|
|
60
|
+
`createMarketMulti`, `placeBet`, `resolveMarket`, `claimPayout`,
|
|
61
|
+
`claimRefund`, `cancelMarket`, `withdrawCreatorFunds`, plus
|
|
62
|
+
**`flagResolution`, `finalizeResolution`, `adminOverrideResolution`**)
|
|
63
|
+
that hide the "encrypt → send → await MPC callback → refetch"
|
|
64
|
+
choreography.
|
|
65
|
+
- **Async progress events** on every multi-step action, so frontends can
|
|
66
|
+
render `Encrypting…` → `Submitting…` → `Awaiting MPC nodes…` instead
|
|
67
|
+
of one generic spinner.
|
|
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.
|
|
73
|
+
- **Account fetch + memcmp filters** for every program account, with
|
|
74
|
+
byte offsets drift-tested against the IDL.
|
|
75
|
+
- **React hooks** (`@cypher-zk/sdk/react`): `CypherProvider`,
|
|
76
|
+
`useGlobalState`, `useMarket`, `useMarkets`, `useUserPositions`,
|
|
77
|
+
`usePlaceBet`, `useResolveMarket`, `useClaimPayout`, `useClaimRefund`,
|
|
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.
|
|
86
|
+
- **Cluster-agnostic at runtime** — reads `GlobalState.accepted_mint`
|
|
87
|
+
on-chain, so the same build works against any deployment (devnet CSDC,
|
|
88
|
+
mainnet USDC, localnet test mint).
|
|
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.
|
|
94
|
+
|
|
95
|
+
## Install
|
|
22
96
|
|
|
23
97
|
```bash
|
|
24
|
-
npm install @cypher-zk/sdk
|
|
25
|
-
# or
|
|
26
98
|
bun add @cypher-zk/sdk
|
|
99
|
+
# or
|
|
100
|
+
npm install @cypher-zk/sdk
|
|
27
101
|
```
|
|
28
102
|
|
|
29
103
|
### Peer dependencies
|
|
30
104
|
|
|
31
|
-
| Package
|
|
32
|
-
|
|
|
33
|
-
| `react` ^18
|
|
34
|
-
| `@tanstack/react-query` ^5 | `@cypher-zk/sdk/react`
|
|
35
|
-
| `typescript` ^5 | Type checking |
|
|
105
|
+
| Package | Required for |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `react` ^18 \|\| ^19 | `@cypher-zk/sdk/react` subpath only |
|
|
108
|
+
| `@tanstack/react-query` ^5 | `@cypher-zk/sdk/react` subpath only |
|
|
36
109
|
|
|
37
|
-
|
|
110
|
+
Core SDK works in any TypeScript environment with no peer requirements.
|
|
38
111
|
|
|
39
|
-
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Quickstart
|
|
115
|
+
|
|
116
|
+
### 1. Construct a client
|
|
40
117
|
|
|
41
118
|
```ts
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
119
|
+
import { Connection } from "@solana/web3.js";
|
|
120
|
+
import { CypherClient } from "@cypher-zk/sdk";
|
|
44
121
|
|
|
45
|
-
const connection = new Connection("https://api.devnet.solana.com");
|
|
46
|
-
const wallet = keypairToWallet(Keypair.generate());
|
|
122
|
+
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
|
|
47
123
|
|
|
124
|
+
// Wallet can be:
|
|
125
|
+
// - any @solana/wallet-adapter wallet (browser)
|
|
126
|
+
// - `keypairToWallet(Keypair)` (Node / scripts / tests)
|
|
48
127
|
const client = new CypherClient({ connection, wallet, cluster: "devnet" });
|
|
128
|
+
```
|
|
49
129
|
|
|
50
|
-
|
|
130
|
+
### 2. Read protocol state
|
|
131
|
+
|
|
132
|
+
```ts
|
|
51
133
|
const gs = await client.globalState.fetch();
|
|
52
|
-
console.log("
|
|
134
|
+
console.log("Protocol fee:", gs.protocolFeeRate, "bps");
|
|
53
135
|
|
|
54
|
-
// Fetch a specific market
|
|
55
136
|
const market = await client.markets.fetch(0n);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const active = await client.markets.byState(0); // MarketState.Active
|
|
137
|
+
const active = await client.markets.byState(0); // MarketState.Active
|
|
138
|
+
const mine = await client.markets.byCreator(wallet.publicKey);
|
|
139
|
+
const myBets = await client.positions.byUser(wallet.publicKey);
|
|
60
140
|
```
|
|
61
141
|
|
|
62
|
-
### Place a
|
|
142
|
+
### 3. Place a private bet — with live progress
|
|
63
143
|
|
|
64
144
|
```ts
|
|
65
|
-
import {
|
|
145
|
+
import { computeFees } from "@cypher-zk/sdk";
|
|
66
146
|
|
|
67
|
-
|
|
147
|
+
// Preview the fee split before showing a confirm modal:
|
|
148
|
+
const preview = computeFees(5_000_000n, {
|
|
149
|
+
protocolFeeRateBps: gs.protocolFeeRate,
|
|
150
|
+
lpFeeRateBps: gs.lpFeeRate,
|
|
151
|
+
});
|
|
152
|
+
console.log("Net stake:", preview.netAmount, "after fees:", preview.protocolFee + preview.lpFee);
|
|
153
|
+
|
|
154
|
+
// Fire the end-to-end flow:
|
|
155
|
+
const { signature, position, userKeypair } = await client.actions.placeBet({
|
|
68
156
|
payer: wallet.publicKey,
|
|
69
157
|
user: wallet.publicKey,
|
|
70
158
|
marketId: 0n,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
159
|
+
side: 1, // 0 = NO, 1 = YES
|
|
160
|
+
amountUsdc: 5_000_000n, // $5 (USDC has 6 decimals)
|
|
161
|
+
onProgress: ({ stage, message, signature }) => {
|
|
162
|
+
// stage ∈ "validating" | "fetching-state" | "encrypting" | "submitting"
|
|
163
|
+
// | "awaiting-callback" | "refetching" | "done"
|
|
164
|
+
console.log(stage, message ?? "", signature ?? "");
|
|
165
|
+
},
|
|
74
166
|
});
|
|
75
167
|
|
|
76
|
-
|
|
77
|
-
|
|
168
|
+
// IMPORTANT: persist userKeypair.privateKey somewhere the user controls
|
|
169
|
+
// (e.g. localStorage encrypted under the wallet's signature) — without
|
|
170
|
+
// it, the user cannot decrypt this position later when claiming.
|
|
171
|
+
saveSecretForLater(position!.market, userKeypair.privateKey);
|
|
78
172
|
```
|
|
79
173
|
|
|
80
|
-
|
|
174
|
+
The progress callback lets you drive multi-step UI:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
[ Validating … ] ◄ instant, client-side
|
|
178
|
+
[ Fetching protocol state … ]
|
|
179
|
+
[ Encrypting your bet … ]
|
|
180
|
+
[ Submitting transaction … ] ◄ tx signature available here
|
|
181
|
+
[ Awaiting MPC nodes (~10s) … ]
|
|
182
|
+
[ Updating position … ]
|
|
183
|
+
[ Done! ]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 4. Create a market
|
|
81
187
|
|
|
82
188
|
```ts
|
|
83
|
-
const
|
|
189
|
+
const { marketId, marketPda, signature } = await client.actions.createMarket({
|
|
84
190
|
creator: wallet.publicKey,
|
|
85
191
|
question: "Will ETH hit $10k by end of 2026?",
|
|
86
192
|
closeTime: BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 3600),
|
|
87
|
-
category: 0,
|
|
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,
|
|
198
|
+
onProgress: (e) => console.log(e.stage),
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Multi-outcome variant — `createMarketMulti` with `numOutcomes: 2 | 3 | 4`.
|
|
203
|
+
|
|
204
|
+
### 5. Resolve, claim payout, claim refund
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// Resolver (oracle / DAO):
|
|
208
|
+
await client.actions.resolveMarket({
|
|
209
|
+
payer: wallet.publicKey,
|
|
210
|
+
resolver: wallet.publicKey,
|
|
211
|
+
marketId,
|
|
212
|
+
outcomeValue: 1,
|
|
213
|
+
onProgress: (e) => console.log(e.stage),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Winning bettor:
|
|
217
|
+
await client.actions.claimPayout({
|
|
218
|
+
payer: wallet.publicKey,
|
|
219
|
+
user: wallet.publicKey,
|
|
220
|
+
marketId,
|
|
88
221
|
});
|
|
89
222
|
|
|
90
|
-
|
|
91
|
-
|
|
223
|
+
// Bettor on an unresolved market (past resolution_deadline):
|
|
224
|
+
await client.actions.claimRefund({
|
|
225
|
+
payer: wallet.publicKey,
|
|
226
|
+
user: wallet.publicKey,
|
|
227
|
+
marketId,
|
|
228
|
+
});
|
|
92
229
|
```
|
|
93
230
|
|
|
94
|
-
|
|
231
|
+
Each refuses pre-flight if the market isn't in the right phase
|
|
232
|
+
(`marketPhase` returns `"claimable"` for payout, `"refundable"` for
|
|
233
|
+
refund) so the user never burns gas on a guaranteed-to-fail tx.
|
|
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
|
+
|
|
241
|
+
### 6. Subscribe to events
|
|
95
242
|
|
|
96
243
|
```ts
|
|
97
|
-
// Real-time (WebSocket)
|
|
244
|
+
// Real-time (WebSocket):
|
|
98
245
|
const sub = client.events.onBetPlaced((data) => {
|
|
99
|
-
|
|
246
|
+
// `data` is BetPlacedEvent — fully typed (camelCase fields, bigint amounts):
|
|
247
|
+
console.log(`Bet placed on market ${data.market.toBase58()} — odds ${data.entryOdds}`);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Generic, typed by name:
|
|
251
|
+
const sub2 = client.events.subscribe("MarketResolvedEvent", (data) => {
|
|
252
|
+
console.log("Outcome:", data.outcome, "Payout ratio:", data.payoutRatio);
|
|
253
|
+
});
|
|
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}`);
|
|
100
264
|
});
|
|
101
265
|
|
|
102
|
-
// Later
|
|
266
|
+
// Later
|
|
267
|
+
sub.unsubscribe();
|
|
268
|
+
sub2.unsubscribe();
|
|
103
269
|
|
|
104
|
-
// Poll-based (no WS
|
|
270
|
+
// Poll-based fallback (no WS required):
|
|
105
271
|
const recent = await client.events.pollEvents({ limit: 20 });
|
|
106
|
-
for (const { event } of recent) {
|
|
107
|
-
console.log(event.name,
|
|
272
|
+
for (const { event, signature, slot } of recent) {
|
|
273
|
+
console.log(event.name, "in tx", signature, "at slot", slot);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Parse events out of a known transaction:
|
|
277
|
+
import { parseLogs, parseLogsFor } from "@cypher-zk/sdk";
|
|
278
|
+
const tx = await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0 });
|
|
279
|
+
const allEvents = parseLogs(tx?.meta?.logMessages ?? []);
|
|
280
|
+
const payoutsOnly = parseLogsFor(tx?.meta?.logMessages ?? [], "PayoutClaimedEvent");
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 7. Phase helpers
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { marketPhase, projectDeadlines } from "@cypher-zk/sdk";
|
|
287
|
+
|
|
288
|
+
// Compute what action is currently available on a market:
|
|
289
|
+
switch (marketPhase(market)) {
|
|
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;
|
|
295
|
+
case "claimable": /* show "Claim payout" */ break;
|
|
296
|
+
case "refundable": /* show "Claim refund" */ break;
|
|
297
|
+
case "expired": /* show "Admin sweep eligible" */ break;
|
|
298
|
+
case "cancelled": /* show "Cancelled" */ break;
|
|
108
299
|
}
|
|
300
|
+
|
|
301
|
+
// Preview deadlines for a draft market the user is filling in:
|
|
302
|
+
const projected = projectDeadlines(BigInt(closeTimeSec));
|
|
303
|
+
console.log("Resolution deadline:", new Date(Number(projected.resolutionDeadline) * 1000));
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
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
|
|
109
340
|
```
|
|
110
341
|
|
|
111
|
-
###
|
|
342
|
+
### Three new actions
|
|
112
343
|
|
|
113
344
|
```ts
|
|
114
|
-
|
|
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").
|
|
115
373
|
|
|
116
|
-
|
|
117
|
-
const events = parseLogs(tx.meta?.logMessages ?? []);
|
|
374
|
+
### Phase gating
|
|
118
375
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
);
|
|
122
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;
|
|
123
427
|
}
|
|
124
428
|
```
|
|
125
429
|
|
|
126
|
-
|
|
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
|
+
|
|
464
|
+
## React hooks
|
|
127
465
|
|
|
128
466
|
```tsx
|
|
129
|
-
import {
|
|
467
|
+
import {
|
|
468
|
+
CypherProvider,
|
|
469
|
+
useGlobalState,
|
|
470
|
+
useMarket,
|
|
471
|
+
useMarkets,
|
|
472
|
+
usePlaceBet,
|
|
473
|
+
} from "@cypher-zk/sdk/react";
|
|
130
474
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
475
|
+
import { useState } from "react";
|
|
476
|
+
import { CypherClient, keypairToWallet, MarketState } from "@cypher-zk/sdk";
|
|
477
|
+
import type { ActionProgressEvent } from "@cypher-zk/sdk";
|
|
131
478
|
|
|
132
479
|
const queryClient = new QueryClient();
|
|
480
|
+
const client = new CypherClient({ connection, wallet, cluster: "devnet" });
|
|
133
481
|
|
|
134
482
|
function App() {
|
|
135
483
|
return (
|
|
136
484
|
<QueryClientProvider client={queryClient}>
|
|
137
485
|
<CypherProvider client={client}>
|
|
138
|
-
<MarketView />
|
|
486
|
+
<MarketView marketId={0n} />
|
|
139
487
|
</CypherProvider>
|
|
140
488
|
</QueryClientProvider>
|
|
141
489
|
);
|
|
142
490
|
}
|
|
143
491
|
|
|
144
|
-
function MarketView() {
|
|
145
|
-
const { data: market, isLoading } = useMarket(
|
|
146
|
-
const
|
|
492
|
+
function MarketView({ marketId }: { marketId: bigint }) {
|
|
493
|
+
const { data: market, isLoading } = useMarket(marketId);
|
|
494
|
+
const [stage, setStage] = useState<ActionProgressEvent | null>(null);
|
|
147
495
|
|
|
148
|
-
|
|
496
|
+
const placeBet = usePlaceBet({
|
|
497
|
+
onSuccess: ({ userKeypair }) => persistUserSecret(userKeypair.privateKey),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (isLoading) return <p>Loading market…</p>;
|
|
501
|
+
if (!market) return <p>Market not found.</p>;
|
|
149
502
|
|
|
150
503
|
return (
|
|
151
504
|
<div>
|
|
152
|
-
<
|
|
505
|
+
<h2>{market.question}</h2>
|
|
153
506
|
<button
|
|
154
|
-
disabled={isPending}
|
|
507
|
+
disabled={placeBet.isPending}
|
|
155
508
|
onClick={() =>
|
|
156
|
-
placeBet({
|
|
509
|
+
placeBet.mutate({
|
|
157
510
|
payer: wallet.publicKey,
|
|
158
511
|
user: wallet.publicKey,
|
|
159
|
-
marketId
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
512
|
+
marketId,
|
|
513
|
+
side: 1,
|
|
514
|
+
amountUsdc: 5_000_000n,
|
|
515
|
+
onProgress: setStage,
|
|
163
516
|
})
|
|
164
517
|
}
|
|
165
518
|
>
|
|
166
|
-
Bet
|
|
519
|
+
{placeBet.isPending && stage ? `Bet → ${stage.stage}` : "Bet $5 YES"}
|
|
167
520
|
</button>
|
|
521
|
+
{placeBet.error && <p style={{ color: "crimson" }}>{placeBet.error.message}</p>}
|
|
168
522
|
</div>
|
|
169
523
|
);
|
|
170
524
|
}
|
|
171
525
|
```
|
|
172
526
|
|
|
173
|
-
### Available
|
|
527
|
+
### Available hooks
|
|
528
|
+
|
|
529
|
+
| Hook | Kind | Description |
|
|
530
|
+
| --- | --- | --- |
|
|
531
|
+
| `useGlobalState()` | Query | Protocol config (fees, mint, admin, counter) |
|
|
532
|
+
| `useMarket(id)` | Query | Single market by ID |
|
|
533
|
+
| `useMarkets(filter?)` | Query | All/filtered markets (creator, state) |
|
|
534
|
+
| `useUserPositions(user)` | Query | All bet positions for a user |
|
|
535
|
+
| `usePlaceBet()` | Mutation | End-to-end private bet |
|
|
536
|
+
| `useCreateMarket()` | Mutation | Create a new market |
|
|
537
|
+
| `useResolveMarket()` | Mutation | Submit outcome + await reveal |
|
|
538
|
+
| `useClaimPayout()` | Mutation | Claim winning payout |
|
|
539
|
+
| `useClaimRefund()` | Mutation | Claim refund on unresolved market |
|
|
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 |
|
|
544
|
+
| `useMarketEvents()` | Subscription | Live event stream (component-scoped) |
|
|
545
|
+
|
|
546
|
+
Mutation hooks auto-invalidate the relevant query keys on success. Read
|
|
547
|
+
hooks expose their `queryKey` factories (`marketKeys.one(id)`,
|
|
548
|
+
`positionKeys.byUser(user)`, `globalStateKeys.all`) for manual
|
|
549
|
+
invalidation.
|
|
550
|
+
|
|
551
|
+
Live example under [`examples/react-vite/`](./examples/react-vite/) — a
|
|
552
|
+
small Vite app that renders the protocol state, lists active markets,
|
|
553
|
+
and drives `usePlaceBet` with live progress.
|
|
174
554
|
|
|
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 |
|
|
555
|
+
---
|
|
188
556
|
|
|
189
|
-
##
|
|
557
|
+
## Layout
|
|
190
558
|
|
|
191
559
|
```
|
|
192
560
|
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
|
|
561
|
+
├── src/ # framework-agnostic core (ESM)
|
|
562
|
+
│ ├── config.ts # program ID, cluster presets, constants
|
|
563
|
+
│ ├── pda.ts # PDA derivations
|
|
564
|
+
│ ├── wallet.ts # Wallet interface + Keypair adapter
|
|
565
|
+
│ ├── fees.ts # mirrors on-chain fee math
|
|
566
|
+
│ ├── deadlines.ts # market phase computation
|
|
567
|
+
│ ├── errors.ts # CypherErrorCode + Anchor error walker
|
|
568
|
+
│ ├── client.ts # CypherClient — single import surface
|
|
569
|
+
│ ├── accounts/ # fetch + memcmp filter helpers per account
|
|
570
|
+
│ ├── arcium/ # MPC glue (cipher, queue accounts, offsets)
|
|
571
|
+
│ ├── instructions/ # raw TransactionInstruction builders (29 ix, v0.2)
|
|
572
|
+
│ ├── actions/ # high-level flows + progress events
|
|
573
|
+
│ ├── events/ # typed event parser + WS/poll subscriptions
|
|
574
|
+
│ ├── idl/ # synced Anchor IDL (source of truth)
|
|
575
|
+
│ └── node/ # node-only admin helpers
|
|
576
|
+
├── react/src/ # @cypher-zk/sdk/react hooks subpath
|
|
577
|
+
├── examples/react-vite/ # minimal Vite app smoke
|
|
578
|
+
├── docs/
|
|
579
|
+
│ ├── architecture.md # three-surface model + module map
|
|
580
|
+
│ └── flows.md # one diagram per user flow
|
|
215
581
|
└── tests/
|
|
216
|
-
├── unit/ #
|
|
217
|
-
├── integration/ #
|
|
218
|
-
└── devnet/ #
|
|
582
|
+
├── unit/ # 150 tests, 712 assertions — pure, no chain
|
|
583
|
+
├── integration/ # INTEGRATION=1 — Arcium localnet lifecycle
|
|
584
|
+
└── devnet/ # DEVNET=1 — devnet read-only + opt-in writes
|
|
219
585
|
```
|
|
220
586
|
|
|
221
|
-
|
|
587
|
+
See [`docs/architecture.md`](./docs/architecture.md) for the three-surface
|
|
588
|
+
model (circuit / program / client), the account topology, and the full
|
|
589
|
+
runtime flow of a private bet. See [`docs/flows.md`](./docs/flows.md) for
|
|
590
|
+
per-flow ASCII diagrams.
|
|
591
|
+
|
|
592
|
+
---
|
|
222
593
|
|
|
223
|
-
|
|
594
|
+
## Cluster strategy
|
|
224
595
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
| `mainnet` | USDC (`EPjFWdd5…`) | TBD |
|
|
229
|
-
| `localnet` | CSDC (same as devnet) | `1116522022` |
|
|
596
|
+
The SDK is **cluster-agnostic at runtime**: it reads the accepted SPL mint
|
|
597
|
+
from `GlobalState.accepted_mint` on every flow rather than hard-coding
|
|
598
|
+
CSDC vs USDC. The same build works against any Cypher deployment.
|
|
230
599
|
|
|
231
|
-
|
|
600
|
+
| Cluster | RPC default | Accepted mint | Arcium offset |
|
|
601
|
+
| --- | --- | --- | --- |
|
|
602
|
+
| `devnet` | `api.devnet.solana.com` | CSDC (`8AF9BABN…`) | `456` |
|
|
603
|
+
| `mainnet` | `api.mainnet-beta.solana.com` | USDC (`EPjFWdd5…`) | (set at deploy) |
|
|
604
|
+
| `localnet` | `localhost:8899` | CSDC (test build) | `1116522022` |
|
|
232
605
|
|
|
233
606
|
```ts
|
|
234
|
-
// Explicit preset
|
|
607
|
+
// Explicit preset:
|
|
235
608
|
const client = new CypherClient({ connection, wallet, cluster: "devnet" });
|
|
236
609
|
|
|
237
|
-
//
|
|
610
|
+
// Custom config — override RPC and/or Arcium cluster offset:
|
|
238
611
|
const client = new CypherClient({
|
|
239
612
|
connection,
|
|
240
613
|
wallet,
|
|
241
614
|
cluster: {
|
|
242
615
|
name: "devnet",
|
|
243
|
-
rpc: "https://my-helius-
|
|
616
|
+
rpc: "https://my-helius-endpoint.example",
|
|
244
617
|
arciumClusterOffset: 456,
|
|
245
|
-
expectedMint:
|
|
618
|
+
expectedMint: KNOWN_MINTS.devnetCSDC,
|
|
246
619
|
},
|
|
247
620
|
});
|
|
248
621
|
```
|
|
249
622
|
|
|
623
|
+
---
|
|
624
|
+
|
|
250
625
|
## Scripts
|
|
251
626
|
|
|
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
|
-
|
|
627
|
+
| Command | Purpose |
|
|
628
|
+
| --- | --- |
|
|
629
|
+
| `bun install` | Install deps |
|
|
630
|
+
| `bun test` | All unit suites (gates skipped) |
|
|
631
|
+
| `bun run test:unit` | Unit-only |
|
|
632
|
+
| `bun run test:integration` | `INTEGRATION=1` — Arcium localnet lifecycle |
|
|
633
|
+
| `bun run test:devnet` | `DEVNET=1` — devnet read-only + opt-in writes |
|
|
634
|
+
| `bun run typecheck` | `tsc --noEmit` (strict) |
|
|
635
|
+
| `bun run build` | ESM + `.d.ts` via tsup → `dist/` |
|
|
636
|
+
| `bun run sync:idl` | Re-copy IDL + types from `../cypher-main` |
|
|
637
|
+
| `bun run prepublishOnly` | sync IDL → typecheck → unit tests → build |
|
|
638
|
+
|
|
639
|
+
---
|
|
262
640
|
|
|
263
641
|
## Security
|
|
264
642
|
|
|
265
|
-
The Cypher protocol
|
|
643
|
+
The Cypher protocol underwent a two-round security audit. All 9
|
|
644
|
+
findings — 3 critical, 2 high, 4 medium/low — have been remediated and
|
|
645
|
+
verified. See `cypher-main/audit_report.md` for the full report.
|
|
646
|
+
|
|
647
|
+
The SDK adds defense-in-depth client-side:
|
|
648
|
+
|
|
649
|
+
- **`computeFees` mirrors the on-chain math exactly** — `placeBet`
|
|
650
|
+
pre-asserts the encrypted `net_amount` will match what the contract
|
|
651
|
+
computes from the gross. The contract's circuit also verifies this
|
|
652
|
+
on-chain (audit fix H-1) — the SDK guard surfaces the failure as a
|
|
653
|
+
clean rejection instead of a wasted MPC computation.
|
|
654
|
+
- **`marketPhase` blocks known-bad claims** — `claimPayout` /
|
|
655
|
+
`claimRefund` refuse to send if the market isn't in the right phase,
|
|
656
|
+
saving users from on-chain `MarketStillOpen` / `ClaimPeriodExpired`
|
|
657
|
+
rejections.
|
|
658
|
+
- **Position double-claim guard** — `claimPayout` reads `position.claimed`
|
|
659
|
+
before submitting (the contract enforces it again as audit fix H-2).
|
|
660
|
+
- **Strict ciphertext-length enforcement** — every builder that takes a
|
|
661
|
+
`Uint8Array` ciphertext asserts `length === 32` before assembling the
|
|
662
|
+
ix, catching the "combined ciphertext arrays" mistake from the Arcium
|
|
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`.
|
|
694
|
+
|
|
695
|
+
---
|
|
266
696
|
|
|
267
697
|
## License
|
|
268
698
|
|
|
269
|
-
Source Available — use is permitted, redistribution is not. See
|
|
699
|
+
Source Available — use is permitted, redistribution is not. See
|
|
700
|
+
[LICENSE](./LICENSE) for full terms.
|