solvrn-sdk 1.0.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 ADDED
@@ -0,0 +1,172 @@
1
+ # solvrn-sdk
2
+
3
+ **Solvrn SDK** — Privacy SDK for Solana Governance
4
+
5
+ A TypeScript SDK for building private, encrypted voting applications on Solana using zero-knowledge proofs and confidential computing.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install solvrn-sdk
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { SolvrnClient } from 'solvrn-sdk';
17
+ import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
18
+ import { Connection, Keypair } from '@solana/web3.js';
19
+
20
+ // Initialize the client
21
+ const solvrn = new SolvrnClient(
22
+ 'http://localhost:3000', // Relayer URL
23
+ 'DBCtofDd6f3U342nwz768FXbH6K5QyGxZUGLjFeb9JTS', // Arcium Program ID (optional)
24
+ 'AL2krCFs4WuzAdjZJbiYJCUnjJ2gmzQdtQuh7YJ3LXcv' // Solvrn Program ID (optional)
25
+ );
26
+
27
+ // Initialize ZK backend (must be called once)
28
+ await solvrn.init(circuitJson); // Pass compiled Noir circuit JSON
29
+
30
+ // Create a proposal
31
+ const { proposalId, txid } = await solvrn.createProposal(
32
+ provider, // AnchorProvider
33
+ authority, // PublicKey
34
+ votingMint, // Token mint address
35
+ metadata, // { title, desc, duration }
36
+ gasBufferSol // SOL amount for gas (e.g., 0.05)
37
+ );
38
+
39
+ // Cast a vote (full flow)
40
+ const result = await solvrn.castVote(
41
+ provider, // AnchorProvider
42
+ walletPubkey, // string (base58)
43
+ proposalId, // number
44
+ choice // 0 = NO, 1 = YES
45
+ );
46
+
47
+ console.log('Vote submitted:', result.tx);
48
+ ```
49
+
50
+ ## API
51
+
52
+ ### `SolvrnClient`
53
+
54
+ #### Constructor
55
+ ```typescript
56
+ constructor(
57
+ relayerUrl: string,
58
+ arciumProgramId?: string,
59
+ programId?: string
60
+ )
61
+ ```
62
+
63
+ #### Methods
64
+
65
+ - **`init(circuitJson: any): Promise<void>`** — Initialize ZK backend with Noir circuit
66
+ - **`createProposal(provider, authority, votingMint, metadata, gasBufferSol, proposalIdOverride?): Promise<{proposalId, txid}>`** — Create voting proposal and snapshot
67
+ - **`castVote(provider, walletPubkey, proposalId, choice): Promise<{success, tx, error}>`** — Full voting flow (proof + encryption + submission)
68
+ - **`api.getNextProposalId(): Promise<{nextId, success}>`** — Get next available proposal ID
69
+ - **`api.initializeSnapshot(proposalId, votingMint, metadata, creator): Promise<SnapshotResponse>`** — Initialize voting snapshot
70
+ - **`api.getProposal(proposalId): Promise<ProposalResponse>`** — Get proposal data
71
+ - **`api.getProof(proposalId, userPubkey): Promise<ProofResponse>`** — Get Merkle proof for voter
72
+ - **`api.submitVote(proposalId, nullifier, encryptedBallot): Promise<SubmitVoteResponse>`** — Submit encrypted vote
73
+ - **`api.getVoteCounts(proposalId): Promise<VoteCountsResponse>`** — Get vote counts
74
+ - **`api.proveTally(proposalId, yesVotes, noVotes, threshold, quorum): Promise<TallyProofResponse>`** — Generate ZK tally proof
75
+
76
+ ### Sub-modules
77
+
78
+ - **`prover.generateVoteProof(secret, proofData, proposalId)`** — Generate ZK proof of eligibility
79
+ - **`encryption.encryptVote(provider, voteChoice, votingWeight)`** — Encrypt vote using Arcium MPC
80
+
81
+ ## Requirements
82
+
83
+ - Relayer running at `relayerUrl` (see [Solvrn Relayer](https://github.com/solvrn-labs/solvrn-relayer))
84
+ - Compiled Noir circuit JSON (from `frontend/circuit/target/circuit.json`)
85
+ - Solana wallet connected via AnchorProvider
86
+
87
+ ## How It Works
88
+
89
+ 1. **Snapshot** — Relayer fetches token holders and builds Merkle tree
90
+ 2. **Proof** — SDK gets Merkle proof + voter secret from relayer
91
+ 3. **ZK Proof** — SDK generates zero-knowledge proof proving eligibility
92
+ 4. **Encryption** — Vote choice encrypted using Arcium MPC
93
+ 5. **Submission** — Encrypted vote + ZK proof sent to relayer → Solana
94
+ 6. **Tally** — Relayer decrypts votes and generates ZK tally proof
95
+ 7. **Verification** — Tally proof verifies quorum & majority thresholds
96
+
97
+ ## What's Real
98
+
99
+ ✅ **Real ZK Proofs** - Uses Barretenberg WASM + Noir circuits
100
+ ✅ **Real Encryption** - Arcium MPC encryption
101
+ ✅ **Real On-Chain** - Actual Solana transactions
102
+ ✅ **Real Merkle Trees** - Built from actual token holders
103
+ ✅ **Real Nullifiers** - Prevents double voting
104
+ ✅ **Real Vote Storage** - Encrypted votes stored on-chain
105
+
106
+ ## Current Limitations
107
+
108
+ ⚠️ **Vote Decryption** - Currently simulated (relayer-side). Real Arcium MPC decryption coming soon.
109
+ ⚠️ **Vote Count Breakdown** - `getVoteCounts()` returns simulated yes/no breakdown. The total vote count (`realVoteCount`) is accurate.
110
+ ✅ **Tally Proofs** - Work perfectly with user-provided vote counts. ZK proofs are 100% real.
111
+
112
+ ## Important: Using Tally in Production
113
+
114
+ **⚠️ CRITICAL:** `proveTally()` generates REAL ZK proofs, but if you use `getVoteCounts()` for vote counts, you'll be proving SIMULATED data.
115
+
116
+ ### ✅ Full Flow (With Limitation):
117
+
118
+ ```typescript
119
+ // 1. Create proposal ✅ REAL
120
+ const { proposalId } = await solvrn.createProposal(...);
121
+
122
+ // 2. Cast vote ✅ REAL
123
+ await solvrn.castVote(provider, wallet, proposalId, 1);
124
+
125
+ // 3. Get vote counts ⚠️ PARTIALLY REAL
126
+ const counts = await solvrn.api.getVoteCounts(proposalId);
127
+ // counts.realVoteCount ✅ - Accurate total
128
+ // counts.yesVotes/noVotes ⚠️ - Simulated breakdown
129
+
130
+ // 4. Prove tally ⚠️ REAL PROOF, BUT PROVING SIMULATED DATA
131
+ const tallyProof = await solvrn.api.proveTally(
132
+ proposalId,
133
+ counts.yesVotes, // ⚠️ Simulated!
134
+ counts.noVotes, // ⚠️ Simulated!
135
+ 51, 10
136
+ );
137
+ // Returns: Real ZK proof ✅
138
+ // But proving: Simulated vote counts ⚠️
139
+ ```
140
+
141
+ ### ✅ Recommended: Provide Your Own Vote Counts
142
+
143
+ For production, decrypt votes yourself or wait for relayer decryption:
144
+
145
+ ```typescript
146
+ // Get accurate total vote count
147
+ const counts = await solvrn.api.getVoteCounts(proposalId);
148
+ console.log(`Total votes: ${counts.realVoteCount}`); // ✅ Accurate
149
+
150
+ // Provide your own yes/no breakdown (from your own decryption)
151
+ const tallyProof = await solvrn.api.proveTally(
152
+ proposalId,
153
+ yourDecryptedYesVotes, // ✅ Your own decryption
154
+ yourDecryptedNoVotes, // ✅ Your own decryption
155
+ 51, 10
156
+ );
157
+ // Returns: Real ZK proof of REAL data ✅
158
+ ```
159
+
160
+ **What's Real:**
161
+ - ✅ Total vote count (`realVoteCount`) - Accurate
162
+ - ✅ ZK proof generation - 100% real
163
+ - ✅ Vote encryption - 100% real
164
+ - ✅ On-chain storage - 100% real
165
+ - ✅ Tally proof generation - 100% real
166
+
167
+ **What's Simulated:**
168
+ - ⚠️ Yes/No breakdown from `getVoteCounts()` - Until Arcium MPC decryption is implemented
169
+
170
+ ## License
171
+
172
+ ISC
@@ -0,0 +1,79 @@
1
+ import * as _aztec_bb_js from '@aztec/bb.js';
2
+ import { Buffer } from 'buffer';
3
+ import { AnchorProvider } from '@coral-xyz/anchor';
4
+ import { PublicKey } from '@solana/web3.js';
5
+
6
+ interface ProofResponse {
7
+ proof: {
8
+ path: string[];
9
+ index: number;
10
+ root: string;
11
+ balance: string;
12
+ weight: string;
13
+ secret: string;
14
+ leaf: string;
15
+ };
16
+ success: boolean;
17
+ error?: string;
18
+ }
19
+ interface ProposalMetadata {
20
+ title: string;
21
+ desc: string;
22
+ duration: number;
23
+ }
24
+ declare class SolvrnApi {
25
+ private baseUrl;
26
+ constructor(baseUrl: string);
27
+ initializeSnapshot(proposalId: number, votingMint: string, metadata?: ProposalMetadata, creator?: string): Promise<any>;
28
+ getNextProposalId(): Promise<{
29
+ nextId: number;
30
+ success: boolean;
31
+ }>;
32
+ getProposal(proposalId: number): Promise<any>;
33
+ getProof(proposalId: number, userPubkey: string): Promise<ProofResponse>;
34
+ submitVote(proposalId: number, nullifierHex: string, encryptedBallot: {
35
+ ciphertext: Uint8Array;
36
+ public_key: number[];
37
+ nonce: number[];
38
+ }): Promise<any>;
39
+ proveTally(proposalId: number, yesVotes: number, noVotes: number, threshold: number, quorum: number): Promise<any>;
40
+ getVoteCounts(proposalId: number): Promise<any>;
41
+ private post;
42
+ private get;
43
+ }
44
+
45
+ declare class SolvrnProver {
46
+ private backend;
47
+ private noir;
48
+ init(circuitJson: any): Promise<void>;
49
+ generateVoteProof(secret: string, proofRes: any, proposalId: number): Promise<_aztec_bb_js.ProofData>;
50
+ }
51
+
52
+ declare class SolvrnEncryption {
53
+ private programId;
54
+ constructor(programId?: string);
55
+ encryptVote(provider: AnchorProvider, voteChoice: number, votingWeight: number): Promise<{
56
+ ciphertext: Buffer<ArrayBuffer>;
57
+ nonce: number[];
58
+ public_key: number[];
59
+ }>;
60
+ }
61
+
62
+ declare class SolvrnClient {
63
+ api: SolvrnApi;
64
+ prover: SolvrnProver;
65
+ encryption: SolvrnEncryption;
66
+ constructor(relayerUrl: string, arciumProgramId?: string, programId?: string);
67
+ init(circuitJson: any): Promise<void>;
68
+ createProposal(provider: AnchorProvider, authorityPubkey: PublicKey, votingMint: string, metadata: ProposalMetadata, gasBufferSol: number, proposalIdOverride?: number): Promise<{
69
+ proposalId: number;
70
+ txid: string;
71
+ }>;
72
+ castVote(provider: AnchorProvider, walletPubkey: string, proposalId: number, choice: number): Promise<{
73
+ success: any;
74
+ tx: any;
75
+ error: any;
76
+ }>;
77
+ }
78
+
79
+ export { SolvrnClient };
package/dist/index.js ADDED
@@ -0,0 +1,960 @@
1
+ import { Buffer } from "buffer";
2
+ global.Buffer = Buffer;
3
+
4
+ // src/utils.ts
5
+ import { PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
6
+ import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
7
+ var SYSTEM_PROGRAM_ID = new PublicKey("11111111111111111111111111111111");
8
+ var LEGACY_TOKEN_PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
9
+ var hexToBytes = (hex) => {
10
+ let cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
11
+ if (cleanHex.length % 2 !== 0) cleanHex = "0" + cleanHex;
12
+ const bytes = [];
13
+ for (let i = 0; i < cleanHex.length; i += 2) bytes.push(parseInt(cleanHex.substr(i, 2), 16));
14
+ return bytes;
15
+ };
16
+
17
+ // src/api.ts
18
+ var SolvrnApi = class {
19
+ constructor(baseUrl) {
20
+ this.baseUrl = baseUrl;
21
+ }
22
+ // --- UPDATED: Accepts Creator Argument ---
23
+ async initializeSnapshot(proposalId, votingMint, metadata, creator) {
24
+ if (process.env.NODE_ENV !== "development") {
25
+ }
26
+ return this.post("initialize-snapshot", {
27
+ proposalId,
28
+ votingMint,
29
+ metadata,
30
+ creator
31
+ // <--- THIS MUST BE SENT TO THE RELAYER
32
+ });
33
+ }
34
+ async getNextProposalId() {
35
+ return this.get("next-proposal-id");
36
+ }
37
+ async getProposal(proposalId) {
38
+ return this.get(`proposal/${proposalId}`);
39
+ }
40
+ async getProof(proposalId, userPubkey) {
41
+ const res = await this.post("get-proof", { proposalId: proposalId.toString(), userPubkey });
42
+ if (!res.success) throw new Error(res.error || "Failed to fetch merkle proof");
43
+ return res;
44
+ }
45
+ async submitVote(proposalId, nullifierHex, encryptedBallot) {
46
+ return this.post("relay-vote", {
47
+ nullifier: hexToBytes(nullifierHex),
48
+ ciphertext: Array.from(encryptedBallot.ciphertext),
49
+ pubkey: encryptedBallot.public_key,
50
+ nonce: encryptedBallot.nonce,
51
+ proposalId
52
+ });
53
+ }
54
+ async proveTally(proposalId, yesVotes, noVotes, threshold, quorum) {
55
+ return this.post("prove-tally", { proposalId, yesVotes, noVotes, threshold, quorum });
56
+ }
57
+ async getVoteCounts(proposalId) {
58
+ return this.get(`vote-counts/${proposalId}`);
59
+ }
60
+ async post(endpoint, body) {
61
+ const allowedEndpoints = [
62
+ "initialize-snapshot",
63
+ "get-proof",
64
+ "relay-vote",
65
+ "prove-tally"
66
+ ];
67
+ if (!allowedEndpoints.includes(endpoint)) {
68
+ throw new Error(`Endpoint '${endpoint}' is not accessible through SDK. Use direct API calls for admin functionality.`);
69
+ }
70
+ const res = await fetch(`${this.baseUrl}/${endpoint}`, {
71
+ method: "POST",
72
+ headers: { "Content-Type": "application/json" },
73
+ body: JSON.stringify(body)
74
+ });
75
+ return await res.json();
76
+ }
77
+ async get(endpoint) {
78
+ const allowedEndpoints = [
79
+ "next-proposal-id",
80
+ "proposal",
81
+ "vote-counts"
82
+ ];
83
+ const isAllowed = allowedEndpoints.some(
84
+ (allowed) => endpoint === allowed || endpoint.startsWith(`${allowed}/`)
85
+ );
86
+ if (!isAllowed) {
87
+ throw new Error(`Endpoint '${endpoint}' is not accessible through SDK. Use direct API calls for admin functionality.`);
88
+ }
89
+ const res = await fetch(`${this.baseUrl}/${endpoint}`);
90
+ return await res.json();
91
+ }
92
+ };
93
+
94
+ // src/prover.ts
95
+ import { Barretenberg, UltraHonkBackend } from "@aztec/bb.js";
96
+ import { Noir } from "@noir-lang/noir_js";
97
+ var SolvrnProver = class {
98
+ constructor() {
99
+ this.backend = null;
100
+ this.noir = null;
101
+ }
102
+ async init(circuitJson) {
103
+ const bb = await Barretenberg.new();
104
+ this.backend = new UltraHonkBackend(circuitJson.bytecode, bb);
105
+ this.noir = new Noir(circuitJson);
106
+ }
107
+ async generateVoteProof(secret, proofRes, proposalId) {
108
+ if (!this.noir || !this.backend) throw new Error("Prover not initialized. Call init() first.");
109
+ const weightVal = BigInt(proofRes.proof.weight);
110
+ const balanceVal = BigInt(proofRes.proof.balance);
111
+ const inputs = {
112
+ user_secret: "0x" + secret.replace("0x", "").padStart(64, "0"),
113
+ balance: "0x" + balanceVal.toString(16).padStart(64, "0"),
114
+ weight: "0x" + weightVal.toString(16).padStart(64, "0"),
115
+ merkle_path: proofRes.proof.path,
116
+ // Pass as-is, no padding
117
+ merkle_index: Number(proofRes.proof.index),
118
+ merkle_root: proofRes.proof.root,
119
+ // Pass as-is, no padding
120
+ proposal_id: "0x" + BigInt(proposalId).toString(16).padStart(64, "0")
121
+ };
122
+ console.log("Noir inputs:", JSON.stringify(inputs, null, 2));
123
+ const { witness } = await this.noir.execute(inputs);
124
+ const proof = await this.backend.generateProof(witness);
125
+ return proof;
126
+ }
127
+ };
128
+
129
+ // src/encryption.ts
130
+ import { getMXEPublicKey, RescueCipher, x25519 } from "@arcium-hq/client";
131
+ import { PublicKey as PublicKey2 } from "@solana/web3.js";
132
+ import { Buffer as Buffer2 } from "buffer";
133
+ var SolvrnEncryption = class {
134
+ constructor(programId = process.env.ARCIUM_PROGRAM_ID || "DBCtofDd6f3U342nwz768FXbH6K5QyGxZUGLjFeb9JTS") {
135
+ this.programId = new PublicKey2(programId);
136
+ }
137
+ async encryptVote(provider, voteChoice, votingWeight) {
138
+ let mxePublicKey = null;
139
+ let attempts = 0;
140
+ while (!mxePublicKey && attempts < 20) {
141
+ try {
142
+ mxePublicKey = await getMXEPublicKey(provider, this.programId);
143
+ if (mxePublicKey) break;
144
+ } catch (err) {
145
+ }
146
+ await new Promise((r) => setTimeout(r, 2e3));
147
+ attempts++;
148
+ }
149
+ if (!mxePublicKey) throw new Error("Arcium MXE Public Key not found.");
150
+ const ephemeralSecret = x25519.utils.randomSecretKey();
151
+ const ephemeralPublic = x25519.getPublicKey(ephemeralSecret);
152
+ const sharedSecret = x25519.getSharedSecret(ephemeralSecret, mxePublicKey);
153
+ const cipher = new RescueCipher(sharedSecret);
154
+ const inputs = [BigInt(votingWeight), BigInt(voteChoice)];
155
+ const nonce = x25519.utils.randomSecretKey().slice(0, 16);
156
+ const encrypted = cipher.encrypt(inputs, nonce);
157
+ return {
158
+ ciphertext: Buffer2.from(new Uint8Array(encrypted.flat().map((n) => Number(n)))),
159
+ nonce: Array.from(nonce),
160
+ public_key: Array.from(ephemeralPublic)
161
+ };
162
+ }
163
+ };
164
+
165
+ // src/index.ts
166
+ import { Program } from "@coral-xyz/anchor";
167
+ import BN from "bn.js";
168
+ import { PublicKey as PublicKey3, SystemProgram, LAMPORTS_PER_SOL as LAMPORTS_PER_SOL2 } from "@solana/web3.js";
169
+
170
+ // src/idl.json
171
+ var idl_default = {
172
+ address: "AL2krCFs4WuzAdjZJbiYJCUnjJ2gmzQdtQuh7YJ3LXcv",
173
+ metadata: {
174
+ name: "solvote_chain",
175
+ version: "0.1.0",
176
+ spec: "0.1.0",
177
+ description: "Created with Anchor"
178
+ },
179
+ instructions: [
180
+ {
181
+ name: "finalize_proposal",
182
+ discriminator: [
183
+ 23,
184
+ 68,
185
+ 51,
186
+ 167,
187
+ 109,
188
+ 173,
189
+ 187,
190
+ 164
191
+ ],
192
+ accounts: [
193
+ {
194
+ name: "proposal",
195
+ writable: true,
196
+ pda: {
197
+ seeds: [
198
+ {
199
+ kind: "const",
200
+ value: [
201
+ 115,
202
+ 118,
203
+ 114,
204
+ 110,
205
+ 95,
206
+ 118,
207
+ 53
208
+ ]
209
+ },
210
+ {
211
+ kind: "account",
212
+ path: "proposal.proposal_id",
213
+ account: "Proposal"
214
+ }
215
+ ]
216
+ }
217
+ },
218
+ {
219
+ name: "proposal_token_account",
220
+ writable: true,
221
+ pda: {
222
+ seeds: [
223
+ {
224
+ kind: "account",
225
+ path: "proposal"
226
+ },
227
+ {
228
+ kind: "const",
229
+ value: [
230
+ 6,
231
+ 221,
232
+ 246,
233
+ 225,
234
+ 215,
235
+ 101,
236
+ 161,
237
+ 147,
238
+ 217,
239
+ 203,
240
+ 225,
241
+ 70,
242
+ 206,
243
+ 235,
244
+ 121,
245
+ 172,
246
+ 28,
247
+ 180,
248
+ 133,
249
+ 237,
250
+ 95,
251
+ 91,
252
+ 55,
253
+ 145,
254
+ 58,
255
+ 140,
256
+ 245,
257
+ 133,
258
+ 126,
259
+ 255,
260
+ 0,
261
+ 169
262
+ ]
263
+ },
264
+ {
265
+ kind: "account",
266
+ path: "treasury_mint"
267
+ }
268
+ ],
269
+ program: {
270
+ kind: "const",
271
+ value: [
272
+ 140,
273
+ 151,
274
+ 37,
275
+ 143,
276
+ 78,
277
+ 36,
278
+ 137,
279
+ 241,
280
+ 187,
281
+ 61,
282
+ 16,
283
+ 41,
284
+ 20,
285
+ 142,
286
+ 13,
287
+ 131,
288
+ 11,
289
+ 90,
290
+ 19,
291
+ 153,
292
+ 218,
293
+ 255,
294
+ 16,
295
+ 132,
296
+ 4,
297
+ 142,
298
+ 123,
299
+ 216,
300
+ 219,
301
+ 233,
302
+ 248,
303
+ 89
304
+ ]
305
+ }
306
+ }
307
+ },
308
+ {
309
+ name: "target_token_account",
310
+ writable: true,
311
+ pda: {
312
+ seeds: [
313
+ {
314
+ kind: "account",
315
+ path: "target_wallet"
316
+ },
317
+ {
318
+ kind: "const",
319
+ value: [
320
+ 6,
321
+ 221,
322
+ 246,
323
+ 225,
324
+ 215,
325
+ 101,
326
+ 161,
327
+ 147,
328
+ 217,
329
+ 203,
330
+ 225,
331
+ 70,
332
+ 206,
333
+ 235,
334
+ 121,
335
+ 172,
336
+ 28,
337
+ 180,
338
+ 133,
339
+ 237,
340
+ 95,
341
+ 91,
342
+ 55,
343
+ 145,
344
+ 58,
345
+ 140,
346
+ 245,
347
+ 133,
348
+ 126,
349
+ 255,
350
+ 0,
351
+ 169
352
+ ]
353
+ },
354
+ {
355
+ kind: "account",
356
+ path: "treasury_mint"
357
+ }
358
+ ],
359
+ program: {
360
+ kind: "const",
361
+ value: [
362
+ 140,
363
+ 151,
364
+ 37,
365
+ 143,
366
+ 78,
367
+ 36,
368
+ 137,
369
+ 241,
370
+ 187,
371
+ 61,
372
+ 16,
373
+ 41,
374
+ 20,
375
+ 142,
376
+ 13,
377
+ 131,
378
+ 11,
379
+ 90,
380
+ 19,
381
+ 153,
382
+ 218,
383
+ 255,
384
+ 16,
385
+ 132,
386
+ 4,
387
+ 142,
388
+ 123,
389
+ 216,
390
+ 219,
391
+ 233,
392
+ 248,
393
+ 89
394
+ ]
395
+ }
396
+ }
397
+ },
398
+ {
399
+ name: "target_wallet",
400
+ relations: [
401
+ "proposal"
402
+ ]
403
+ },
404
+ {
405
+ name: "treasury_mint",
406
+ relations: [
407
+ "proposal"
408
+ ]
409
+ },
410
+ {
411
+ name: "authority",
412
+ signer: true,
413
+ relations: [
414
+ "proposal"
415
+ ]
416
+ },
417
+ {
418
+ name: "token_program",
419
+ address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
420
+ }
421
+ ],
422
+ args: [
423
+ {
424
+ name: "proof",
425
+ type: "bytes"
426
+ },
427
+ {
428
+ name: "yes_votes",
429
+ type: "u64"
430
+ },
431
+ {
432
+ name: "no_votes",
433
+ type: "u64"
434
+ },
435
+ {
436
+ name: "threshold",
437
+ type: "u64"
438
+ },
439
+ {
440
+ name: "quorum",
441
+ type: "u64"
442
+ }
443
+ ]
444
+ },
445
+ {
446
+ name: "initialize_proposal",
447
+ discriminator: [
448
+ 50,
449
+ 73,
450
+ 156,
451
+ 98,
452
+ 129,
453
+ 149,
454
+ 21,
455
+ 158
456
+ ],
457
+ accounts: [
458
+ {
459
+ name: "proposal",
460
+ writable: true,
461
+ pda: {
462
+ seeds: [
463
+ {
464
+ kind: "const",
465
+ value: [
466
+ 115,
467
+ 118,
468
+ 114,
469
+ 110,
470
+ 95,
471
+ 118,
472
+ 53
473
+ ]
474
+ },
475
+ {
476
+ kind: "arg",
477
+ path: "proposal_id"
478
+ }
479
+ ]
480
+ }
481
+ },
482
+ {
483
+ name: "proposal_token_account",
484
+ writable: true,
485
+ pda: {
486
+ seeds: [
487
+ {
488
+ kind: "account",
489
+ path: "proposal"
490
+ },
491
+ {
492
+ kind: "const",
493
+ value: [
494
+ 6,
495
+ 221,
496
+ 246,
497
+ 225,
498
+ 215,
499
+ 101,
500
+ 161,
501
+ 147,
502
+ 217,
503
+ 203,
504
+ 225,
505
+ 70,
506
+ 206,
507
+ 235,
508
+ 121,
509
+ 172,
510
+ 28,
511
+ 180,
512
+ 133,
513
+ 237,
514
+ 95,
515
+ 91,
516
+ 55,
517
+ 145,
518
+ 58,
519
+ 140,
520
+ 245,
521
+ 133,
522
+ 126,
523
+ 255,
524
+ 0,
525
+ 169
526
+ ]
527
+ },
528
+ {
529
+ kind: "account",
530
+ path: "treasury_mint"
531
+ }
532
+ ],
533
+ program: {
534
+ kind: "const",
535
+ value: [
536
+ 140,
537
+ 151,
538
+ 37,
539
+ 143,
540
+ 78,
541
+ 36,
542
+ 137,
543
+ 241,
544
+ 187,
545
+ 61,
546
+ 16,
547
+ 41,
548
+ 20,
549
+ 142,
550
+ 13,
551
+ 131,
552
+ 11,
553
+ 90,
554
+ 19,
555
+ 153,
556
+ 218,
557
+ 255,
558
+ 16,
559
+ 132,
560
+ 4,
561
+ 142,
562
+ 123,
563
+ 216,
564
+ 219,
565
+ 233,
566
+ 248,
567
+ 89
568
+ ]
569
+ }
570
+ }
571
+ },
572
+ {
573
+ name: "voting_mint"
574
+ },
575
+ {
576
+ name: "treasury_mint"
577
+ },
578
+ {
579
+ name: "target_wallet"
580
+ },
581
+ {
582
+ name: "authority",
583
+ writable: true,
584
+ signer: true
585
+ },
586
+ {
587
+ name: "token_program",
588
+ address: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
589
+ },
590
+ {
591
+ name: "associated_token_program",
592
+ address: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
593
+ },
594
+ {
595
+ name: "system_program",
596
+ address: "11111111111111111111111111111111"
597
+ }
598
+ ],
599
+ args: [
600
+ {
601
+ name: "proposal_id",
602
+ type: "u64"
603
+ },
604
+ {
605
+ name: "merkle_root",
606
+ type: {
607
+ array: [
608
+ "u8",
609
+ 32
610
+ ]
611
+ }
612
+ },
613
+ {
614
+ name: "execution_amount",
615
+ type: "u64"
616
+ }
617
+ ]
618
+ },
619
+ {
620
+ name: "submit_vote",
621
+ discriminator: [
622
+ 115,
623
+ 242,
624
+ 100,
625
+ 0,
626
+ 49,
627
+ 178,
628
+ 242,
629
+ 133
630
+ ],
631
+ accounts: [
632
+ {
633
+ name: "proposal",
634
+ writable: true
635
+ },
636
+ {
637
+ name: "nullifier_account",
638
+ writable: true,
639
+ pda: {
640
+ seeds: [
641
+ {
642
+ kind: "const",
643
+ value: [
644
+ 110,
645
+ 117,
646
+ 108,
647
+ 108,
648
+ 105,
649
+ 102,
650
+ 105,
651
+ 101,
652
+ 114
653
+ ]
654
+ },
655
+ {
656
+ kind: "account",
657
+ path: "proposal"
658
+ },
659
+ {
660
+ kind: "arg",
661
+ path: "nullifier"
662
+ }
663
+ ]
664
+ }
665
+ },
666
+ {
667
+ name: "relayer",
668
+ writable: true,
669
+ signer: true
670
+ },
671
+ {
672
+ name: "system_program",
673
+ address: "11111111111111111111111111111111"
674
+ }
675
+ ],
676
+ args: [
677
+ {
678
+ name: "nullifier",
679
+ type: {
680
+ array: [
681
+ "u8",
682
+ 32
683
+ ]
684
+ }
685
+ },
686
+ {
687
+ name: "ciphertext",
688
+ type: "bytes"
689
+ },
690
+ {
691
+ name: "pubkey",
692
+ type: {
693
+ array: [
694
+ "u8",
695
+ 32
696
+ ]
697
+ }
698
+ },
699
+ {
700
+ name: "nonce",
701
+ type: "u128"
702
+ }
703
+ ]
704
+ }
705
+ ],
706
+ accounts: [
707
+ {
708
+ name: "NullifierAccount",
709
+ discriminator: [
710
+ 250,
711
+ 31,
712
+ 238,
713
+ 177,
714
+ 213,
715
+ 98,
716
+ 48,
717
+ 172
718
+ ]
719
+ },
720
+ {
721
+ name: "Proposal",
722
+ discriminator: [
723
+ 26,
724
+ 94,
725
+ 189,
726
+ 187,
727
+ 116,
728
+ 136,
729
+ 53,
730
+ 33
731
+ ]
732
+ }
733
+ ],
734
+ errors: [
735
+ {
736
+ code: 6e3,
737
+ name: "AlreadyExecuted",
738
+ msg: "Already executed."
739
+ },
740
+ {
741
+ code: 6001,
742
+ name: "ProposalNotPassed",
743
+ msg: "Proposal did not pass (State)."
744
+ },
745
+ {
746
+ code: 6002,
747
+ name: "InvalidProof",
748
+ msg: "Invalid ZK Proof."
749
+ },
750
+ {
751
+ code: 6003,
752
+ name: "QuorumNotMet",
753
+ msg: "Quorum not met."
754
+ },
755
+ {
756
+ code: 6004,
757
+ name: "MajorityNotMet",
758
+ msg: "Majority threshold not met."
759
+ }
760
+ ],
761
+ types: [
762
+ {
763
+ name: "NullifierAccount",
764
+ type: {
765
+ kind: "struct",
766
+ fields: [
767
+ {
768
+ name: "proposal",
769
+ type: "pubkey"
770
+ },
771
+ {
772
+ name: "nullifier",
773
+ type: {
774
+ array: [
775
+ "u8",
776
+ 32
777
+ ]
778
+ }
779
+ },
780
+ {
781
+ name: "ciphertext",
782
+ type: "bytes"
783
+ },
784
+ {
785
+ name: "pubkey",
786
+ type: {
787
+ array: [
788
+ "u8",
789
+ 32
790
+ ]
791
+ }
792
+ },
793
+ {
794
+ name: "nonce",
795
+ type: "u128"
796
+ }
797
+ ]
798
+ }
799
+ },
800
+ {
801
+ name: "Proposal",
802
+ type: {
803
+ kind: "struct",
804
+ fields: [
805
+ {
806
+ name: "proposal_id",
807
+ type: "u64"
808
+ },
809
+ {
810
+ name: "vote_count",
811
+ type: "u64"
812
+ },
813
+ {
814
+ name: "authority",
815
+ type: "pubkey"
816
+ },
817
+ {
818
+ name: "merkle_root",
819
+ type: {
820
+ array: [
821
+ "u8",
822
+ 32
823
+ ]
824
+ }
825
+ },
826
+ {
827
+ name: "voting_mint",
828
+ type: "pubkey"
829
+ },
830
+ {
831
+ name: "treasury_mint",
832
+ type: "pubkey"
833
+ },
834
+ {
835
+ name: "execution_amount",
836
+ type: "u64"
837
+ },
838
+ {
839
+ name: "target_wallet",
840
+ type: "pubkey"
841
+ },
842
+ {
843
+ name: "tally_result",
844
+ type: "u8"
845
+ },
846
+ {
847
+ name: "is_executed",
848
+ type: "bool"
849
+ }
850
+ ]
851
+ }
852
+ }
853
+ ]
854
+ };
855
+
856
+ // src/index.ts
857
+ var PROGRAM_ID = null;
858
+ var getProgramId = () => {
859
+ if (PROGRAM_ID) return PROGRAM_ID;
860
+ const id = globalThis.process?.env?.PROGRAM_ID || process.env.PROGRAM_ID;
861
+ if (!id) {
862
+ throw new Error("PROGRAM_ID environment variable required. Please set process.env.PROGRAM_ID in your application or pass programId to SolvrnClient constructor.");
863
+ }
864
+ PROGRAM_ID = new PublicKey3(id);
865
+ return PROGRAM_ID;
866
+ };
867
+ var TOKEN_PROGRAM_ID = new PublicKey3("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
868
+ var ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey3("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
869
+ var SolvrnClient = class {
870
+ constructor(relayerUrl, arciumProgramId, programId) {
871
+ if (programId) {
872
+ globalThis.process = globalThis.process || {};
873
+ globalThis.process.env = globalThis.process.env || {};
874
+ globalThis.process.env.PROGRAM_ID = programId;
875
+ }
876
+ this.api = new SolvrnApi(relayerUrl);
877
+ this.prover = new SolvrnProver();
878
+ this.encryption = new SolvrnEncryption(arciumProgramId);
879
+ }
880
+ async init(circuitJson) {
881
+ await this.prover.init(circuitJson);
882
+ }
883
+ async createProposal(provider, authorityPubkey, votingMint, metadata, gasBufferSol, proposalIdOverride) {
884
+ let proposalId = proposalIdOverride;
885
+ if (!proposalId) {
886
+ const { nextId, success } = await this.api.getNextProposalId();
887
+ if (!success) throw new Error("Failed to get ID");
888
+ proposalId = nextId;
889
+ }
890
+ const snap = await this.api.initializeSnapshot(
891
+ proposalId,
892
+ votingMint,
893
+ metadata,
894
+ authorityPubkey.toBase58()
895
+ // <--- Force Creator into Snapshot
896
+ );
897
+ if (!snap.success) throw new Error(snap.error || "Snapshot failed.");
898
+ const program = new Program(idl_default, provider);
899
+ console.log("SDK CALLING PROGRAM ID:", program.programId.toBase58());
900
+ const [pda] = PublicKey3.findProgramAddressSync(
901
+ [Buffer.from("svrn_v5"), new BN(proposalId).toArrayLike(Buffer, "le", 8)],
902
+ getProgramId()
903
+ );
904
+ const [vault] = PublicKey3.findProgramAddressSync(
905
+ [
906
+ pda.toBuffer(),
907
+ TOKEN_PROGRAM_ID.toBuffer(),
908
+ // Legacy
909
+ new PublicKey3(votingMint).toBuffer()
910
+ ],
911
+ ASSOCIATED_TOKEN_PROGRAM_ID
912
+ );
913
+ const tx = await program.methods.initializeProposal(new BN(proposalId), hexToBytes(snap.root), new BN(1e3)).accounts({
914
+ proposal: pda,
915
+ proposalTokenAccount: vault,
916
+ authority: authorityPubkey,
917
+ votingMint: new PublicKey3(votingMint),
918
+ treasuryMint: new PublicKey3(votingMint),
919
+ targetWallet: authorityPubkey,
920
+ tokenProgram: TOKEN_PROGRAM_ID,
921
+ // Strict Legacy
922
+ associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
923
+ systemProgram: SystemProgram.programId
924
+ }).transaction();
925
+ tx.add(SystemProgram.transfer({
926
+ fromPubkey: authorityPubkey,
927
+ toPubkey: pda,
928
+ lamports: gasBufferSol * LAMPORTS_PER_SOL2
929
+ }));
930
+ const { blockhash } = await provider.connection.getLatestBlockhash();
931
+ tx.recentBlockhash = blockhash;
932
+ tx.feePayer = authorityPubkey;
933
+ const signedTx = await provider.wallet.signTransaction(tx);
934
+ const txid = await provider.connection.sendRawTransaction(signedTx.serialize());
935
+ await provider.connection.confirmTransaction(txid);
936
+ return { proposalId, txid };
937
+ }
938
+ async castVote(provider, walletPubkey, proposalId, choice) {
939
+ if (!walletPubkey || typeof walletPubkey !== "string") {
940
+ throw new Error("Invalid wallet public key");
941
+ }
942
+ if (!Number.isInteger(proposalId) || proposalId < 0) {
943
+ throw new Error("Invalid proposal ID");
944
+ }
945
+ if (!Number.isInteger(choice) || choice !== 0 && choice !== 1) {
946
+ throw new Error("Invalid vote choice: must be 0 (NO) or 1 (YES)");
947
+ }
948
+ const proofData = await this.api.getProof(proposalId, walletPubkey);
949
+ const relayerSecret = proofData.proof.secret;
950
+ const zkProof = await this.prover.generateVoteProof(relayerSecret, proofData, proposalId);
951
+ const weight = Number(proofData.proof.weight);
952
+ const encrypted = await this.encryption.encryptVote(provider, choice, weight);
953
+ const nullifier = zkProof.publicInputs[zkProof.publicInputs.length - 1];
954
+ const relayResponse = await this.api.submitVote(proposalId, nullifier, encrypted);
955
+ return { success: relayResponse.success, tx: relayResponse.tx, error: relayResponse.error };
956
+ }
957
+ };
958
+ export {
959
+ SolvrnClient
960
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "solvrn-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Privacy SDK for Solana governance using zero-knowledge proofs and confidential computing",
5
+ "type": "module",
6
+ "keywords": [
7
+ "solana",
8
+ "governance",
9
+ "zk",
10
+ "zero-knowledge",
11
+ "privacy",
12
+ "voting",
13
+ "noir",
14
+ "arcium",
15
+ "mpc",
16
+ "blockchain"
17
+ ],
18
+ "author": "Solvrn Labs",
19
+ "license": "ISC",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/solvrn-labs/solvrn-sdk.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/solvrn-labs/solvrn-sdk/issues"
26
+ },
27
+ "homepage": "https://github.com/solvrn-labs/solvrn-sdk#readme",
28
+ "main": "./dist/index.js",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "dev": "tsup --watch",
40
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
41
+ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
42
+ "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage"
43
+ },
44
+ "dependencies": {
45
+ "@arcium-hq/client": "^0.6.2",
46
+ "@aztec/bb.js": "^3.0.0-nightly.20260102",
47
+ "@coral-xyz/anchor": "^0.32.1",
48
+ "@noir-lang/noir_js": "1.0.0-beta.15",
49
+ "@solana/spl-token": "^0.4.14",
50
+ "@solana/web3.js": "^1.95.0",
51
+ "bn.js": "^5.2.2",
52
+ "buffer": "^6.0.3"
53
+ },
54
+ "devDependencies": {
55
+ "@types/bn.js": "^5.2.0",
56
+ "@types/jest": "^29.5.14",
57
+ "@types/node": "^20.0.0",
58
+ "jest": "^29.7.0",
59
+ "ts-jest": "^29.4.6",
60
+ "tsup": "^8.0.0",
61
+ "typescript": "^5.3.3"
62
+ }
63
+ }