@collectorcrypt/vrf-client 0.1.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.
@@ -0,0 +1,655 @@
1
+ import * as anchor from "@coral-xyz/anchor";
2
+ import { BorshCoder, EventParser, Program } from "@coral-xyz/anchor";
3
+ import {
4
+ Connection,
5
+ PublicKey,
6
+ Transaction,
7
+ TransactionInstruction,
8
+ } from "@solana/web3.js";
9
+ import {
10
+ bn,
11
+ CompressedAccountWithMerkleContext,
12
+ Rpc,
13
+ } from "@lightprotocol/stateless.js";
14
+ import { verifyVRF, vrfProofToHash } from "@collectorcrypt/ecvrf";
15
+
16
+ import {
17
+ alphaHash as alphaHashFn,
18
+ deriveAuthorityAddress,
19
+ deriveProofCommitAddress,
20
+ deriveProofCommitWithBetaAddress,
21
+ encodeLabel,
22
+ memoHash as memoHashFn,
23
+ proofHash as proofHashFn,
24
+ } from "./addresses";
25
+ import { SUITE_EDWARDS25519_SHA512_TAI } from "./constants";
26
+ import {
27
+ buildCommitProofContext,
28
+ buildCreateContext,
29
+ buildMutateContext,
30
+ buildReadOnlyAuthorityContext,
31
+ forceLightV2,
32
+ } from "./light";
33
+ import { OnChainAuthority, OnChainCommit } from "./verifyEndToEnd";
34
+
35
+ /**
36
+ * Decode a VrfAuthority record from a fetched compressed-account row.
37
+ * Wraps the Anchor coder so callers can stay in terms of typed fields.
38
+ */
39
+ export function decodeAuthority(program: Program, dataBytes: Uint8Array) {
40
+ // Anchor 0.31 normalizes IDL type names to camelCase on `program.idl`.
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ return (program as any).coder.types.decode(
43
+ "vrfAuthority",
44
+ Buffer.from(dataBytes),
45
+ ) as {
46
+ owner: PublicKey;
47
+ pk: number[];
48
+ suite: number;
49
+ frozen: boolean;
50
+ revoked: boolean;
51
+ label: number[];
52
+ createdSlot: anchor.BN;
53
+ };
54
+ }
55
+
56
+ export function decodeProofCommit(program: Program, dataBytes: Uint8Array) {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ return (program as any).coder.types.decode(
59
+ "vrfProofCommit",
60
+ Buffer.from(dataBytes),
61
+ ) as {
62
+ authority: PublicKey;
63
+ memoHash: number[];
64
+ proofHash: number[];
65
+ alphaHash: number[];
66
+ committedSlot: anchor.BN;
67
+ };
68
+ }
69
+
70
+ export function decodeProofCommitWithBeta(
71
+ program: Program,
72
+ dataBytes: Uint8Array,
73
+ ) {
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ return (program as any).coder.types.decode(
76
+ "vrfProofCommitWithBeta",
77
+ Buffer.from(dataBytes),
78
+ ) as {
79
+ authority: PublicKey;
80
+ memoHash: number[];
81
+ proofHash: number[];
82
+ alphaHash: number[];
83
+ betaLo: number[];
84
+ betaHi: number[];
85
+ committedSlot: anchor.BN;
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Fetch a VrfAuthority compressed account by (owner, label). Returns null
91
+ * if no such authority exists.
92
+ */
93
+ export async function fetchAuthority(
94
+ program: Program,
95
+ rpc: Rpc,
96
+ owner: PublicKey,
97
+ label: string | Uint8Array,
98
+ ): Promise<{
99
+ authorityAddress: PublicKey;
100
+ account: CompressedAccountWithMerkleContext;
101
+ decoded: ReturnType<typeof decodeAuthority>;
102
+ onChainAuthority: OnChainAuthority;
103
+ } | null> {
104
+ forceLightV2();
105
+ const labelBytes = typeof label === "string" ? encodeLabel(label) : label;
106
+ const authorityAddress = deriveAuthorityAddress(
107
+ owner,
108
+ labelBytes,
109
+ program.programId,
110
+ );
111
+ const account = await rpc.getCompressedAccount(
112
+ bn(authorityAddress.toBytes()),
113
+ );
114
+ if (!account) return null;
115
+ const decoded = decodeAuthority(program, Uint8Array.from(account.data!.data));
116
+ return {
117
+ authorityAddress,
118
+ account,
119
+ decoded,
120
+ onChainAuthority: {
121
+ authorityAddress,
122
+ owner: decoded.owner,
123
+ pk: Uint8Array.from(decoded.pk),
124
+ suite: decoded.suite,
125
+ frozen: decoded.frozen,
126
+ revoked: decoded.revoked,
127
+ label: Uint8Array.from(decoded.label),
128
+ },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Fetch a VrfProofCommit by (authority, memo). Returns null if no such
134
+ * commit exists yet.
135
+ */
136
+ export async function fetchProofCommit(
137
+ program: Program,
138
+ rpc: Rpc,
139
+ authority: PublicKey,
140
+ memo: string | Uint8Array,
141
+ ): Promise<{
142
+ commitAddress: PublicKey;
143
+ account: CompressedAccountWithMerkleContext;
144
+ decoded: ReturnType<typeof decodeProofCommit>;
145
+ onChainCommit: OnChainCommit;
146
+ } | null> {
147
+ forceLightV2();
148
+ const mh = memoHashFn(memo);
149
+ const commitAddress = deriveProofCommitAddress(
150
+ authority,
151
+ mh,
152
+ program.programId,
153
+ );
154
+ const account = await rpc.getCompressedAccount(bn(commitAddress.toBytes()));
155
+ if (!account) return null;
156
+ const decoded = decodeProofCommit(
157
+ program,
158
+ Uint8Array.from(account.data!.data),
159
+ );
160
+ return {
161
+ commitAddress,
162
+ account,
163
+ decoded,
164
+ onChainCommit: {
165
+ authority: decoded.authority,
166
+ memoHash: Uint8Array.from(decoded.memoHash),
167
+ proofHash: Uint8Array.from(decoded.proofHash),
168
+ alphaHash: Uint8Array.from(decoded.alphaHash),
169
+ committedSlot: BigInt(decoded.committedSlot.toString()),
170
+ },
171
+ };
172
+ }
173
+
174
+ export interface InitAuthorityInput {
175
+ owner: PublicKey;
176
+ pk: Uint8Array;
177
+ suite: number;
178
+ label: string | Uint8Array;
179
+ }
180
+
181
+ /**
182
+ * Build the init_authority instruction. Caller is responsible for adding it
183
+ * to a transaction with the owner as signer and submitting it.
184
+ */
185
+ export async function buildInitAuthorityIx(
186
+ program: Program,
187
+ rpc: Rpc,
188
+ input: InitAuthorityInput,
189
+ ): Promise<{ ix: TransactionInstruction; authorityAddress: PublicKey }> {
190
+ if (input.pk.length !== 32) {
191
+ throw new Error("pk must be 32 bytes");
192
+ }
193
+ if (input.suite !== SUITE_EDWARDS25519_SHA512_TAI) {
194
+ throw new Error(
195
+ "only ECVRF-EDWARDS25519-SHA512-TAI suite 0x03 is supported",
196
+ );
197
+ }
198
+ const labelBytes =
199
+ typeof input.label === "string" ? encodeLabel(input.label) : input.label;
200
+ if (labelBytes.length !== 32) {
201
+ throw new Error("label must encode to exactly 32 bytes");
202
+ }
203
+ const authorityAddress = deriveAuthorityAddress(
204
+ input.owner,
205
+ labelBytes,
206
+ program.programId,
207
+ );
208
+
209
+ const ctx = await buildCreateContext(
210
+ rpc,
211
+ program.programId,
212
+ authorityAddress,
213
+ );
214
+
215
+ const ix = await program.methods
216
+ // @ts-ignore: Anchor IDL types are dynamic at this layer
217
+ .initAuthority(
218
+ ctx.proof,
219
+ ctx.packedAddressTreeInfo,
220
+ ctx.outputStateTreeIndex,
221
+ Array.from(input.pk),
222
+ input.suite,
223
+ Array.from(labelBytes),
224
+ )
225
+ .accounts({
226
+ owner: input.owner,
227
+ systemProgram: anchor.web3.SystemProgram.programId,
228
+ } as never)
229
+ .remainingAccounts(ctx.remainingAccountMetas)
230
+ .instruction();
231
+
232
+ return { ix, authorityAddress };
233
+ }
234
+
235
+ export interface FreezeAuthorityInput {
236
+ owner: PublicKey;
237
+ label: string | Uint8Array;
238
+ }
239
+
240
+ export async function buildFreezeAuthorityIx(
241
+ program: Program,
242
+ rpc: Rpc,
243
+ input: FreezeAuthorityInput,
244
+ ): Promise<TransactionInstruction> {
245
+ const auth = await fetchAuthority(program, rpc, input.owner, input.label);
246
+ if (!auth) throw new Error("authority not found");
247
+
248
+ const ctx = await buildMutateContext(rpc, program.programId, auth.account);
249
+
250
+ const ix = await program.methods
251
+ // @ts-ignore: Anchor IDL types are dynamic at this layer
252
+ .freezeAuthority(ctx.proof, auth.decoded, ctx.accountMeta)
253
+ .accounts({ owner: input.owner } as never)
254
+ .remainingAccounts(ctx.remainingAccountMetas)
255
+ .instruction();
256
+ return ix;
257
+ }
258
+
259
+ export async function buildRevokeAuthorityIx(
260
+ program: Program,
261
+ rpc: Rpc,
262
+ input: FreezeAuthorityInput,
263
+ ): Promise<TransactionInstruction> {
264
+ const auth = await fetchAuthority(program, rpc, input.owner, input.label);
265
+ if (!auth) throw new Error("authority not found");
266
+
267
+ const ctx = await buildMutateContext(rpc, program.programId, auth.account);
268
+
269
+ const ix = await program.methods
270
+ // @ts-ignore: Anchor IDL types are dynamic at this layer
271
+ .revokeAuthority(ctx.proof, auth.decoded, ctx.accountMeta)
272
+ .accounts({ owner: input.owner } as never)
273
+ .remainingAccounts(ctx.remainingAccountMetas)
274
+ .instruction();
275
+ return ix;
276
+ }
277
+
278
+ export interface CommitProofInput {
279
+ owner: PublicKey;
280
+ label: string | Uint8Array;
281
+ memo: string | Uint8Array;
282
+ alpha: Uint8Array;
283
+ proof: Uint8Array;
284
+ }
285
+
286
+ type FetchedAuthority = NonNullable<Awaited<ReturnType<typeof fetchAuthority>>>;
287
+
288
+ function assertAuthorityCanCommit(
289
+ auth: FetchedAuthority | null,
290
+ ): asserts auth is FetchedAuthority {
291
+ if (!auth) throw new Error("authority not found");
292
+ if (auth.decoded.suite !== SUITE_EDWARDS25519_SHA512_TAI) {
293
+ throw new Error("authority suite is not supported");
294
+ }
295
+ if (!auth.decoded.frozen) throw new Error("authority is not frozen");
296
+ if (auth.decoded.revoked) throw new Error("authority is revoked");
297
+ }
298
+
299
+ function assertProofMatchesAuthority(
300
+ auth: FetchedAuthority,
301
+ input: CommitProofInput,
302
+ ) {
303
+ const pk = Uint8Array.from(auth.decoded.pk);
304
+ if (!verifyVRF(pk, input.alpha, input.proof)) {
305
+ throw new Error("proof does not verify against authority pk and alpha");
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Build the commit_proof instruction. The authority is looked up via
311
+ * (owner, label) — must already exist on chain. memo/alpha/proof are hashed
312
+ * via SHA-256 and stored in the new VrfProofCommit PDA.
313
+ */
314
+ export async function buildCommitProofIx(
315
+ program: Program,
316
+ rpc: Rpc,
317
+ input: CommitProofInput,
318
+ ): Promise<{ ix: TransactionInstruction; commitAddress: PublicKey }> {
319
+ if (input.proof.length !== 80) {
320
+ throw new Error("proof must be 80 bytes (RFC 9381 Ed25519 ECVRF proof)");
321
+ }
322
+
323
+ const auth = await fetchAuthority(program, rpc, input.owner, input.label);
324
+ assertAuthorityCanCommit(auth);
325
+ assertProofMatchesAuthority(auth, input);
326
+
327
+ const mh = memoHashFn(input.memo);
328
+ const commitAddress = deriveProofCommitAddress(
329
+ auth.authorityAddress,
330
+ mh,
331
+ program.programId,
332
+ );
333
+
334
+ // One unified context: a single validity proof covers both the authority
335
+ // input (read-only) and the new commit address, and both pack against the
336
+ // same remainingAccounts list.
337
+ const ctx = await buildCommitProofContext(
338
+ rpc,
339
+ program.programId,
340
+ auth.account,
341
+ commitAddress,
342
+ );
343
+
344
+ const ix = await program.methods
345
+ // @ts-ignore: Anchor IDL types are dynamic at this layer
346
+ .commitProof(
347
+ ctx.proof,
348
+ ctx.authorityReadOnlyMeta,
349
+ auth.decoded,
350
+ ctx.packedAddressTreeInfo,
351
+ ctx.outputStateTreeIndex,
352
+ Array.from(mh),
353
+ Array.from(proofHashFn(input.proof)),
354
+ Array.from(alphaHashFn(input.alpha)),
355
+ )
356
+ .accounts({ owner: input.owner } as never)
357
+ .remainingAccounts(ctx.remainingAccountMetas)
358
+ .instruction();
359
+
360
+ return { ix, commitAddress };
361
+ }
362
+
363
+ /**
364
+ * Build the commit_proof_event instruction (event mode). Proves the authority
365
+ * read-only, requires it to be frozen and unrevoked, and emits a Solana log
366
+ * event rather than writing a compressed PDA.
367
+ *
368
+ * The trade-off is that the chain doesn't enforce one-commit-per-memo;
369
+ * verifiers must scan for duplicate `memo_hash` events and pick the one where
370
+ * ECVRF math passes.
371
+ */
372
+ export async function buildCommitProofEventIx(
373
+ program: Program,
374
+ rpc: Rpc,
375
+ input: CommitProofInput,
376
+ ): Promise<TransactionInstruction> {
377
+ if (input.proof.length !== 80) {
378
+ throw new Error("proof must be 80 bytes (RFC 9381 Ed25519 ECVRF proof)");
379
+ }
380
+ const labelBytes =
381
+ typeof input.label === "string" ? encodeLabel(input.label) : input.label;
382
+ if (labelBytes.length !== 32) {
383
+ throw new Error("label must encode to exactly 32 bytes");
384
+ }
385
+ const auth = await fetchAuthority(program, rpc, input.owner, labelBytes);
386
+ assertAuthorityCanCommit(auth);
387
+ assertProofMatchesAuthority(auth, input);
388
+
389
+ const ctx = await buildReadOnlyAuthorityContext(
390
+ rpc,
391
+ program.programId,
392
+ auth.account,
393
+ );
394
+ const mh = memoHashFn(input.memo);
395
+
396
+ const ix = await program.methods
397
+ // @ts-ignore: Anchor IDL types are dynamic at this layer
398
+ .commitProofEvent(
399
+ ctx.proof,
400
+ ctx.authorityReadOnlyMeta,
401
+ auth.decoded,
402
+ Array.from(labelBytes),
403
+ Array.from(mh),
404
+ Array.from(proofHashFn(input.proof)),
405
+ Array.from(alphaHashFn(input.alpha)),
406
+ )
407
+ .accounts({ owner: input.owner } as never)
408
+ .remainingAccounts(ctx.remainingAccountMetas)
409
+ .instruction();
410
+
411
+ return ix;
412
+ }
413
+
414
+ /**
415
+ * One row decoded from a `VrfProofCommitted` log event. `txSignature` is the
416
+ * Solana tx the event was emitted in; `slot` is the slot that tx confirmed in.
417
+ *
418
+ * `onChainCommit` is the same shape the verifier helpers expect, so the same
419
+ * verification path works for PDA-mode and event-mode commits.
420
+ */
421
+ export interface ProofCommitEvent {
422
+ owner: PublicKey;
423
+ label: Uint8Array;
424
+ txSignature: string;
425
+ slot: number;
426
+ onChainCommit: OnChainCommit;
427
+ }
428
+
429
+ /**
430
+ * Fetch all `VrfProofCommitted` events emitted for a given `(owner, label,
431
+ * memo)` tuple, ordered oldest → newest. Returns an empty array if none
432
+ * exist.
433
+ *
434
+ * IMPORTANT: this can return MORE than one row. The on-chain program does
435
+ * not enforce uniqueness in event mode. A safe verifier:
436
+ *
437
+ * 1. Collects all matches for the requested memo.
438
+ * 2. Runs `verifyAuthorityCommitEndToEnd` against each candidate proof.
439
+ * 3. Accepts the unique row where the ECVRF math passes.
440
+ *
441
+ * Because ECVRF proofs are deterministic for a fixed (pk, alpha), at most
442
+ * one of the candidates can have a valid `proof_hash`. The presence of extra
443
+ * events is detectable noise, not a successful forgery — but a naive verifier
444
+ * that picks "the latest event" without running ECVRF can be misled, which is
445
+ * the only soundness gap relative to the PDA path.
446
+ *
447
+ * `connection` and `programId` are passed in explicitly so log scanning works
448
+ * against a plain Solana RPC. Fetching compressed authority state still needs
449
+ * a Photon-capable RPC unless the verifier already has that state.
450
+ *
451
+ * `limit` caps how many recent signatures to scan (default 1000); pagination
452
+ * happens automatically via `getSignaturesForAddress`'s `before` cursor.
453
+ */
454
+ export async function fetchProofCommitEvents(
455
+ program: Program,
456
+ connection: Connection,
457
+ owner: PublicKey,
458
+ label: string | Uint8Array,
459
+ memo: string | Uint8Array,
460
+ options: { limit?: number } = {},
461
+ ): Promise<ProofCommitEvent[]> {
462
+ const labelBytes = typeof label === "string" ? encodeLabel(label) : label;
463
+ const targetMemoHash = memoHashFn(memo);
464
+
465
+ const parser = new EventParser(
466
+ program.programId,
467
+ new BorshCoder(program.idl),
468
+ );
469
+ const out: ProofCommitEvent[] = [];
470
+
471
+ const limit = options.limit ?? 1000;
472
+ let before: string | undefined;
473
+ let scanned = 0;
474
+ while (scanned < limit) {
475
+ const pageLimit = Math.min(1000, limit - scanned);
476
+ const sigInfos = await connection.getSignaturesForAddress(owner, {
477
+ limit: pageLimit,
478
+ before,
479
+ });
480
+ if (sigInfos.length === 0) break;
481
+ scanned += sigInfos.length;
482
+ before = sigInfos[sigInfos.length - 1].signature;
483
+
484
+ for (const sigInfo of sigInfos) {
485
+ if (sigInfo.err) continue;
486
+ const tx = await connection.getTransaction(sigInfo.signature, {
487
+ commitment: "confirmed",
488
+ maxSupportedTransactionVersion: 0,
489
+ });
490
+ if (!tx?.meta?.logMessages) continue;
491
+
492
+ for (const ev of parser.parseLogs(tx.meta.logMessages, false)) {
493
+ if (ev.name !== "VrfProofCommitted" && ev.name !== "vrfProofCommitted")
494
+ continue;
495
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
496
+ const data = ev.data as any;
497
+ const eventOwner: PublicKey = data.owner;
498
+ const eventLabel: Uint8Array = Uint8Array.from(data.label);
499
+ const eventMemoHash: Uint8Array = Uint8Array.from(data.memoHash);
500
+
501
+ if (!eventOwner.equals(owner)) continue;
502
+ if (!bytesEqual(eventLabel, labelBytes)) continue;
503
+ if (!bytesEqual(eventMemoHash, targetMemoHash)) continue;
504
+
505
+ const authority = deriveAuthorityAddress(
506
+ eventOwner,
507
+ eventLabel,
508
+ program.programId,
509
+ );
510
+ out.push({
511
+ owner: eventOwner,
512
+ label: eventLabel,
513
+ txSignature: sigInfo.signature,
514
+ slot: tx.slot,
515
+ onChainCommit: {
516
+ authority,
517
+ memoHash: eventMemoHash,
518
+ proofHash: Uint8Array.from(data.proofHash),
519
+ alphaHash: Uint8Array.from(data.alphaHash),
520
+ committedSlot: BigInt(data.committedSlot.toString()),
521
+ },
522
+ });
523
+ }
524
+ }
525
+ }
526
+
527
+ // RPC returns newest first — reverse so oldest comes first.
528
+ out.reverse();
529
+ return out;
530
+ }
531
+
532
+ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
533
+ if (a.length !== b.length) return false;
534
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
535
+ return true;
536
+ }
537
+
538
+ /**
539
+ * Build the commit_proof_with_beta instruction (PDA mode + on-chain beta).
540
+ * Like buildCommitProofIx, but additionally stores the 64-byte ECVRF beta
541
+ * (output of vrfProofToHash) in the new compressed PDA so other Solana
542
+ * programs can read it via a Light SDK CPI.
543
+ *
544
+ * Stored at the same seed prefix as regular commits, so one authority+memo can
545
+ * only use one registry mode.
546
+ */
547
+ export async function buildCommitProofWithBetaIx(
548
+ program: Program,
549
+ rpc: Rpc,
550
+ input: CommitProofInput & { beta: Uint8Array },
551
+ ): Promise<{ ix: TransactionInstruction; commitAddress: PublicKey }> {
552
+ if (input.proof.length !== 80) {
553
+ throw new Error("proof must be 80 bytes (RFC 9381 Ed25519 ECVRF proof)");
554
+ }
555
+ if (input.beta.length !== 64) {
556
+ throw new Error("beta must be 64 bytes (SHA-512 output of vrfProofToHash)");
557
+ }
558
+
559
+ const auth = await fetchAuthority(program, rpc, input.owner, input.label);
560
+ assertAuthorityCanCommit(auth);
561
+ assertProofMatchesAuthority(auth, input);
562
+ if (!bytesEqual(input.beta, vrfProofToHash(input.proof))) {
563
+ throw new Error("beta does not match vrfProofToHash(proof)");
564
+ }
565
+
566
+ const mh = memoHashFn(input.memo);
567
+ const commitAddress = deriveProofCommitWithBetaAddress(
568
+ auth.authorityAddress,
569
+ mh,
570
+ program.programId,
571
+ );
572
+
573
+ const ctx = await buildCommitProofContext(
574
+ rpc,
575
+ program.programId,
576
+ auth.account,
577
+ commitAddress,
578
+ );
579
+
580
+ // Split beta into two 32-byte halves to match the on-chain field layout.
581
+ const betaLo = input.beta.slice(0, 32);
582
+ const betaHi = input.beta.slice(32, 64);
583
+
584
+ const ix = await program.methods
585
+ // @ts-ignore: Anchor IDL types are dynamic at this layer
586
+ .commitProofWithBeta(
587
+ ctx.proof,
588
+ ctx.authorityReadOnlyMeta,
589
+ auth.decoded,
590
+ ctx.packedAddressTreeInfo,
591
+ ctx.outputStateTreeIndex,
592
+ Array.from(mh),
593
+ Array.from(proofHashFn(input.proof)),
594
+ Array.from(alphaHashFn(input.alpha)),
595
+ Array.from(betaLo),
596
+ Array.from(betaHi),
597
+ )
598
+ .accounts({ owner: input.owner } as never)
599
+ .remainingAccounts(ctx.remainingAccountMetas)
600
+ .instruction();
601
+
602
+ return { ix, commitAddress };
603
+ }
604
+
605
+ /**
606
+ * Fetch a VrfProofCommitWithBeta by (authority, memo). Returns null if no such
607
+ * commit exists yet. Reassembles the 64-byte beta from its two on-chain halves.
608
+ */
609
+ export async function fetchProofCommitWithBeta(
610
+ program: Program,
611
+ rpc: Rpc,
612
+ authority: PublicKey,
613
+ memo: string | Uint8Array,
614
+ ): Promise<{
615
+ commitAddress: PublicKey;
616
+ account: CompressedAccountWithMerkleContext;
617
+ decoded: ReturnType<typeof decodeProofCommitWithBeta>;
618
+ onChainCommit: OnChainCommit;
619
+ /** Full 64-byte beta, reassembled from beta_lo + beta_hi. */
620
+ beta: Uint8Array;
621
+ } | null> {
622
+ const mh = memoHashFn(memo);
623
+ const commitAddress = deriveProofCommitWithBetaAddress(
624
+ authority,
625
+ mh,
626
+ program.programId,
627
+ );
628
+ const account = await rpc.getCompressedAccount(bn(commitAddress.toBytes()));
629
+ if (!account) return null;
630
+ const decoded = decodeProofCommitWithBeta(
631
+ program,
632
+ Uint8Array.from(account.data!.data),
633
+ );
634
+ const beta = new Uint8Array(64);
635
+ beta.set(Uint8Array.from(decoded.betaLo), 0);
636
+ beta.set(Uint8Array.from(decoded.betaHi), 32);
637
+ return {
638
+ commitAddress,
639
+ account,
640
+ decoded,
641
+ beta,
642
+ onChainCommit: {
643
+ authority: decoded.authority,
644
+ memoHash: Uint8Array.from(decoded.memoHash),
645
+ proofHash: Uint8Array.from(decoded.proofHash),
646
+ alphaHash: Uint8Array.from(decoded.alphaHash),
647
+ committedSlot: BigInt(decoded.committedSlot.toString()),
648
+ },
649
+ };
650
+ }
651
+
652
+ /** Convenience: wrap a single ix into a Transaction. */
653
+ export function asTx(ix: TransactionInstruction): Transaction {
654
+ return new Transaction().add(ix);
655
+ }
package/src/program.ts ADDED
@@ -0,0 +1,17 @@
1
+ import * as anchor from "@coral-xyz/anchor";
2
+ import { Program } from "@coral-xyz/anchor";
3
+
4
+ // IDL is vendored into the package so the SDK is browser-safe. Refresh
5
+ // the file with `pnpm refresh:idl` from the workspace root whenever the
6
+ // program changes (it copies target/idl/cc_vrf.json here).
7
+ import idl from "./idl/cc_vrf.json";
8
+
9
+ /**
10
+ * Build an Anchor Program handle for cc-vrf. Pass an AnchorProvider with a
11
+ * Connection and Wallet — operations that mutate state (init/freeze/etc)
12
+ * use this provider's wallet as signer.
13
+ */
14
+ export function getProgram(provider: anchor.AnchorProvider): Program {
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ return new Program(idl as any, provider);
17
+ }