nara-sdk 1.0.39 → 1.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,12 +35,12 @@ Circuit files: `withdraw.wasm` + `withdraw_final.zkey`, `ownership.wasm` + `owne
35
35
  On-chain registry for AI agents with identity, memory, and activity tracking:
36
36
 
37
37
  - Register a unique agent ID (lowercase only, no uppercase letters allowed)
38
- - Optional **referral** on registration or via `setReferral` post-registration
38
+ - **Referral system**: register with referral via `registerAgentWithReferral`, or set referral post-registration via `setReferral`
39
39
  - Store agent **bio** and **metadata** (JSON) on-chain
40
40
  - Upload persistent **memory** via chunked buffer mechanism — auto-chunked ~800-byte writes with resumable uploads
41
- - **Activity logging** with on-chain events supports referral for earning referral points (Token-2022 point mint)
41
+ - **Activity logging**: `logActivity` for standard logging, `logActivityWithReferral` for referral-based logging
42
42
  - Memory modes: `new`, `update`, `append`, `auto` (auto-detects)
43
- - Points are minted as **Token-2022 SPL tokens** via a point mint, not stored on the agent record
43
+ - Points are minted as **Token-2022 SPL tokens** separate mints for registration points (`pointMint`), referee rewards (`refereeMint`), and activity rewards (`refereeActivityMint`)
44
44
 
45
45
  ## Skills Hub
46
46
 
@@ -187,6 +187,7 @@ console.log(isValidRecipient(recipient.publicKey)); // true
187
187
  ```typescript
188
188
  import {
189
189
  registerAgent,
190
+ registerAgentWithReferral,
190
191
  getAgentRecord,
191
192
  getAgentInfo,
192
193
  getAgentMemory,
@@ -195,6 +196,7 @@ import {
195
196
  setMetadata,
196
197
  uploadMemory,
197
198
  logActivity,
199
+ logActivityWithReferral,
198
200
  setReferral,
199
201
  deleteAgent,
200
202
  Keypair,
@@ -204,10 +206,12 @@ import { Connection } from "@solana/web3.js";
204
206
  const connection = new Connection("https://mainnet-api.nara.build/", "confirmed");
205
207
  const wallet = Keypair.fromSecretKey(/* your secret key */);
206
208
 
207
- // 1. Register an agent (lowercase only, charges registration fee)
208
- // Optional: pass referralAgentId to set referral on registration
209
- const { signature, agentPubkey } = await registerAgent(
210
- connection, wallet, "my-agent", undefined, "referral-agent-id"
209
+ // 1a. Register an agent (lowercase only, charges registration fee)
210
+ const { signature, agentPubkey } = await registerAgent(connection, wallet, "my-agent");
211
+
212
+ // 1b. Or register with referral
213
+ const result = await registerAgentWithReferral(
214
+ connection, wallet, "my-agent", "referral-agent-id"
211
215
  );
212
216
 
213
217
  // 2. Or set referral after registration
@@ -230,9 +234,11 @@ const bytes = await getAgentMemory(connection, "my-agent");
230
234
  const extra = Buffer.from(JSON.stringify({ more: "data" }));
231
235
  await uploadMemory(connection, wallet, "my-agent", extra, {}, "append");
232
236
 
233
- // 7. Log activity (with optional referral agent)
237
+ // 7a. Log activity
234
238
  await logActivity(connection, wallet, "my-agent", "gpt-4", "chat", "Answered a question");
235
- await logActivity(connection, wallet, "my-agent", "gpt-4", "chat", "With referral", undefined, "referral-agent-id");
239
+
240
+ // 7b. Log activity with referral
241
+ await logActivityWithReferral(connection, wallet, "my-agent", "gpt-4", "chat", "With referral", "referral-agent-id");
236
242
 
237
243
  // 8. Query agent info
238
244
  const info = await getAgentInfo(connection, "my-agent");
@@ -240,7 +246,7 @@ console.log(info.record.agentId, info.record.referralId, info.bio);
240
246
 
241
247
  // 9. Query program config
242
248
  const config = await getAgentRegistryConfig(connection);
243
- console.log(config.pointMint.toBase58(), config.pointsSelf, config.referralRegisterFee);
249
+ console.log(config.pointMint.toBase58(), config.refereeMint.toBase58(), config.pointsSelf, config.activityReward);
244
250
  ```
245
251
 
246
252
  ### Skills SDK
package/index.ts CHANGED
@@ -66,6 +66,7 @@ export {
66
66
  withdraw,
67
67
  transferZkId,
68
68
  transferZkIdByCommitment,
69
+ makeWithdrawIx,
69
70
  deriveIdSecret,
70
71
  computeIdCommitment,
71
72
  isValidRecipient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nara-sdk",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "SDK for the Nara chain (Solana-compatible)",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
package/src/quest.ts CHANGED
@@ -21,22 +21,18 @@ import naraQuestIdl from "./idls/nara_quest.json";
21
21
  const BN254_FIELD =
22
22
  21888242871839275222246405745257275088696311157297823662689037894645226208583n;
23
23
 
24
- import { fileURLToPath } from "url";
25
- import { dirname, join, resolve } from "path";
26
- import { existsSync } from "fs";
27
- const __dirname: string = import.meta.url
28
- ? dirname(fileURLToPath(import.meta.url))
29
- : eval("__dirname") as string;
30
-
31
- function findZkFile(name: string): string {
32
- const srcPath = join(__dirname, "zk", name);
33
- if (existsSync(srcPath)) return srcPath;
34
- return srcPath;
24
+ // Lazily resolve default ZK circuit file paths (Node.js only).
25
+ // In browser environments, pass circuitWasmPath/zkeyPath via QuestOptions.
26
+ async function resolveDefaultZkPaths(): Promise<{ wasm: string; zkey: string }> {
27
+ const { fileURLToPath } = await import("url");
28
+ const { dirname, join } = await import("path");
29
+ const dir = dirname(fileURLToPath(import.meta.url));
30
+ return {
31
+ wasm: join(dir, "zk", "answer_proof.wasm"),
32
+ zkey: join(dir, "zk", "answer_proof_final.zkey"),
33
+ };
35
34
  }
36
35
 
37
- const DEFAULT_CIRCUIT_WASM = findZkFile("answer_proof.wasm");
38
- const DEFAULT_ZKEY = findZkFile("answer_proof_final.zkey");
39
-
40
36
  // ─── Types ───────────────────────────────────────────────────────
41
37
 
42
38
  export interface QuestInfo {
@@ -77,8 +73,10 @@ export interface SubmitRelayResult {
77
73
 
78
74
  export interface QuestOptions {
79
75
  programId?: string;
80
- circuitWasmPath?: string;
81
- zkeyPath?: string;
76
+ /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
77
+ circuitWasmPath?: string | Uint8Array;
78
+ /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
79
+ zkeyPath?: string | Uint8Array;
82
80
  }
83
81
 
84
82
  export interface ActivityLog {
@@ -90,71 +88,79 @@ export interface ActivityLog {
90
88
  referralAgentId?: string;
91
89
  }
92
90
 
93
- // ─── ZK utilities ────────────────────────────────────────────────
91
+ // ─── ZK utilities (browser-compatible, no Buffer) ───────────────
92
+
93
+ function hexFromBytes(bytes: Uint8Array): string {
94
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
95
+ }
96
+
97
+ function concatBytes(...arrays: Uint8Array[]): Uint8Array {
98
+ const total = arrays.reduce((sum, a) => sum + a.length, 0);
99
+ const result = new Uint8Array(total);
100
+ let offset = 0;
101
+ for (const a of arrays) { result.set(a, offset); offset += a.length; }
102
+ return result;
103
+ }
94
104
 
95
- function toBigEndian32(v: bigint): Buffer {
96
- return Buffer.from(v.toString(16).padStart(64, "0"), "hex");
105
+ function bigintToBytes32(v: bigint): Uint8Array {
106
+ const hex = v.toString(16).padStart(64, "0");
107
+ const bytes = new Uint8Array(32);
108
+ for (let i = 0; i < 32; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
109
+ return bytes;
97
110
  }
98
111
 
99
112
  function answerToField(answer: string): bigint {
100
- return (
101
- BigInt("0x" + Buffer.from(answer, "utf-8").toString("hex")) % BN254_FIELD
102
- );
113
+ const encoded = new TextEncoder().encode(answer);
114
+ return BigInt("0x" + hexFromBytes(encoded)) % BN254_FIELD;
103
115
  }
104
116
 
105
117
  function hashBytesToFieldStr(hashBytes: number[]): string {
106
- return BigInt("0x" + Buffer.from(hashBytes).toString("hex")).toString();
118
+ return BigInt("0x" + hexFromBytes(new Uint8Array(hashBytes))).toString();
107
119
  }
108
120
 
109
121
  function pubkeyToCircuitInputs(pubkey: PublicKey): {
110
122
  lo: string;
111
123
  hi: string;
112
124
  } {
113
- const bytes = pubkey.toBuffer();
125
+ const bytes = pubkey.toBytes();
114
126
  return {
115
- lo: BigInt("0x" + bytes.subarray(16, 32).toString("hex")).toString(),
116
- hi: BigInt("0x" + bytes.subarray(0, 16).toString("hex")).toString(),
127
+ lo: BigInt("0x" + hexFromBytes(bytes.slice(16, 32))).toString(),
128
+ hi: BigInt("0x" + hexFromBytes(bytes.slice(0, 16))).toString(),
117
129
  };
118
130
  }
119
131
 
120
132
  function proofToSolana(proof: any): ZkProof {
121
- const negY = (y: string) => toBigEndian32(BN254_FIELD - BigInt(y));
122
- const be = (s: string) => toBigEndian32(BigInt(s));
133
+ const negY = (y: string) => bigintToBytes32(BN254_FIELD - BigInt(y));
134
+ const be = (s: string) => bigintToBytes32(BigInt(s));
123
135
  return {
124
- proofA: Array.from(
125
- Buffer.concat([be(proof.pi_a[0]), negY(proof.pi_a[1])])
126
- ),
127
- proofB: Array.from(
128
- Buffer.concat([
129
- be(proof.pi_b[0][1]),
130
- be(proof.pi_b[0][0]),
131
- be(proof.pi_b[1][1]),
132
- be(proof.pi_b[1][0]),
133
- ])
134
- ),
135
- proofC: Array.from(
136
- Buffer.concat([be(proof.pi_c[0]), be(proof.pi_c[1])])
137
- ),
136
+ proofA: Array.from(concatBytes(be(proof.pi_a[0]), negY(proof.pi_a[1]))),
137
+ proofB: Array.from(concatBytes(
138
+ be(proof.pi_b[0][1]),
139
+ be(proof.pi_b[0][0]),
140
+ be(proof.pi_b[1][1]),
141
+ be(proof.pi_b[1][0]),
142
+ )),
143
+ proofC: Array.from(concatBytes(be(proof.pi_c[0]), be(proof.pi_c[1]))),
138
144
  };
139
145
  }
140
146
 
141
147
  function proofToHex(proof: any): ZkProofHex {
142
- const negY = (y: string) => toBigEndian32(BN254_FIELD - BigInt(y));
143
- const be = (s: string) => toBigEndian32(BigInt(s));
148
+ const negY = (y: string) => bigintToBytes32(BN254_FIELD - BigInt(y));
149
+ const be = (s: string) => bigintToBytes32(BigInt(s));
144
150
  return {
145
- proofA: Buffer.concat([be(proof.pi_a[0]), negY(proof.pi_a[1])]).toString("hex"),
146
- proofB: Buffer.concat([
151
+ proofA: hexFromBytes(concatBytes(be(proof.pi_a[0]), negY(proof.pi_a[1]))),
152
+ proofB: hexFromBytes(concatBytes(
147
153
  be(proof.pi_b[0][1]),
148
154
  be(proof.pi_b[0][0]),
149
155
  be(proof.pi_b[1][1]),
150
156
  be(proof.pi_b[1][0]),
151
- ]).toString("hex"),
152
- proofC: Buffer.concat([be(proof.pi_c[0]), be(proof.pi_c[1])]).toString("hex"),
157
+ )),
158
+ proofC: hexFromBytes(concatBytes(be(proof.pi_c[0]), be(proof.pi_c[1]))),
153
159
  };
154
160
  }
155
161
 
156
162
  // Suppress console output from snarkjs WASM during proof generation.
157
- async function silentProve(snarkjs: any, input: Record<string, string>, wasmPath: string, zkeyPath: string) {
163
+ async function silentProve(snarkjs: any, input: Record<string, string>, wasmPath: string | Uint8Array, zkeyPath: string | Uint8Array) {
158
164
  const savedLog = console.log;
159
165
  const savedError = console.error;
160
166
  console.log = () => {};
@@ -188,7 +194,7 @@ function createProgram(
188
194
 
189
195
  function getPoolPda(programId: PublicKey): PublicKey {
190
196
  const [pda] = PublicKey.findProgramAddressSync(
191
- [Buffer.from("quest_pool")],
197
+ [new TextEncoder().encode("quest_pool")],
192
198
  programId
193
199
  );
194
200
  return pda;
@@ -199,7 +205,7 @@ function getWinnerRecordPda(
199
205
  user: PublicKey
200
206
  ): PublicKey {
201
207
  const [pda] = PublicKey.findProgramAddressSync(
202
- [Buffer.from("quest_winner"), user.toBuffer()],
208
+ [new TextEncoder().encode("quest_winner"), user.toBytes()],
203
209
  programId
204
210
  );
205
211
  return pda;
@@ -279,8 +285,13 @@ export async function generateProof(
279
285
  round: string,
280
286
  options?: QuestOptions
281
287
  ): Promise<{ solana: ZkProof; hex: ZkProofHex }> {
282
- const wasmPath = options?.circuitWasmPath ?? process.env.QUEST_CIRCUIT_WASM ?? DEFAULT_CIRCUIT_WASM;
283
- const zkeyPath = options?.zkeyPath ?? process.env.QUEST_ZKEY ?? DEFAULT_ZKEY;
288
+ let wasmSource = options?.circuitWasmPath;
289
+ let zkeySource = options?.zkeyPath;
290
+ if (!wasmSource || !zkeySource) {
291
+ const defaults = await resolveDefaultZkPaths();
292
+ wasmSource ??= defaults.wasm;
293
+ zkeySource ??= defaults.zkey;
294
+ }
284
295
 
285
296
  const snarkjs = await import("snarkjs");
286
297
  const answerHashFieldStr = hashBytesToFieldStr(answerHash);
@@ -295,8 +306,8 @@ export async function generateProof(
295
306
  pubkey_hi: hi,
296
307
  round: round,
297
308
  },
298
- wasmPath,
299
- zkeyPath
309
+ wasmSource,
310
+ zkeySource
300
311
  );
301
312
 
302
313
  return {
@@ -455,7 +466,7 @@ export async function computeAnswerHash(answer: string): Promise<number[]> {
455
466
  const fieldVal = answerToField(answer);
456
467
  const hashRaw = poseidon([fieldVal]);
457
468
  const hashStr: string = poseidon.F.toString(hashRaw);
458
- return Array.from(toBigEndian32(BigInt(hashStr)));
469
+ return Array.from(bigintToBytes32(BigInt(hashStr)));
459
470
  }
460
471
 
461
472
  /**
package/src/zkid.ts CHANGED
@@ -7,10 +7,7 @@
7
7
  * - Ownership can be transferred via ZK proof without revealing the owner's wallet
8
8
  */
9
9
 
10
- import { createHash } from "crypto";
11
- import { fileURLToPath } from "url";
12
- import { dirname, join } from "path";
13
- import { Connection, Keypair, PublicKey } from "@solana/web3.js";
10
+ import { Connection, Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js";
14
11
  import * as anchor from "@coral-xyz/anchor";
15
12
  import { Program, AnchorProvider, Wallet } from "@coral-xyz/anchor";
16
13
  import { buildPoseidon as _buildPoseidon } from "circomlibjs";
@@ -19,16 +16,6 @@ import BN from "bn.js";
19
16
  import type { NaraZk } from "./idls/nara_zk";
20
17
  import { DEFAULT_ZKID_PROGRAM_ID } from "./constants";
21
18
  import naraZkIdl from "./idls/nara_zk.json";
22
- import { createRequire } from "module";
23
-
24
- // _require is used only for snarkjs (external, loaded at runtime).
25
- // Supports both ESM (tsx dev) and esbuild CJS bundle (import.meta.url is "" — falsy)
26
- const _require: NodeRequire = import.meta.url
27
- ? createRequire(import.meta.url)
28
- : eval("require") as NodeRequire;
29
- const __dirname: string = import.meta.url
30
- ? dirname(fileURLToPath(import.meta.url))
31
- : eval("__dirname") as string;
32
19
 
33
20
  // ─── Constants ────────────────────────────────────────────────────────────────
34
21
 
@@ -36,10 +23,22 @@ const BN254_PRIME =
36
23
  21888242871839275222246405745257275088696311157297823662689037894645226208583n;
37
24
  const MERKLE_LEVELS = 64;
38
25
 
39
- const WITHDRAW_WASM = join(__dirname, "zk", "withdraw.wasm");
40
- const WITHDRAW_ZKEY = join(__dirname, "zk", "withdraw_final.zkey");
41
- const OWNERSHIP_WASM = join(__dirname, "zk", "ownership.wasm");
42
- const OWNERSHIP_ZKEY = join(__dirname, "zk", "ownership_final.zkey");
26
+ // Lazily resolve default ZK circuit file paths (Node.js only).
27
+ // In browser environments, pass withdrawWasm/withdrawZkey/ownershipWasm/ownershipZkey via ZkIdOptions.
28
+ async function resolveDefaultZkPaths(): Promise<{
29
+ withdrawWasm: string; withdrawZkey: string;
30
+ ownershipWasm: string; ownershipZkey: string;
31
+ }> {
32
+ const { fileURLToPath } = await import("url");
33
+ const { dirname, join } = await import("path");
34
+ const dir = dirname(fileURLToPath(import.meta.url));
35
+ return {
36
+ withdrawWasm: join(dir, "zk", "withdraw.wasm"),
37
+ withdrawZkey: join(dir, "zk", "withdraw_final.zkey"),
38
+ ownershipWasm: join(dir, "zk", "ownership.wasm"),
39
+ ownershipZkey: join(dir, "zk", "ownership_final.zkey"),
40
+ };
41
+ }
43
42
 
44
43
  // ─── Public types ─────────────────────────────────────────────────────────────
45
44
 
@@ -69,9 +68,36 @@ export interface ClaimableDeposit {
69
68
 
70
69
  export interface ZkIdOptions {
71
70
  programId?: string;
71
+ /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
72
+ withdrawWasm?: string | Uint8Array;
73
+ /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
74
+ withdrawZkey?: string | Uint8Array;
75
+ /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
76
+ ownershipWasm?: string | Uint8Array;
77
+ /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
78
+ ownershipZkey?: string | Uint8Array;
72
79
  }
73
80
 
74
- // ─── Internal crypto helpers ─────────────────────────────────────────────────
81
+ // ─── Internal crypto helpers (browser-compatible, no Buffer) ────────────────
82
+
83
+ function hexFromBytes(bytes: Uint8Array): string {
84
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
85
+ }
86
+
87
+ function concatBytes(...arrays: Uint8Array[]): Uint8Array {
88
+ const total = arrays.reduce((sum, a) => sum + a.length, 0);
89
+ const result = new Uint8Array(total);
90
+ let offset = 0;
91
+ for (const a of arrays) { result.set(a, offset); offset += a.length; }
92
+ return result;
93
+ }
94
+
95
+ function bigintToBytes32(v: bigint): Uint8Array {
96
+ const hex = v.toString(16).padStart(64, "0");
97
+ const bytes = new Uint8Array(32);
98
+ for (let i = 0; i < 32; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
99
+ return bytes;
100
+ }
75
101
 
76
102
  let _poseidon: any = null;
77
103
 
@@ -86,58 +112,60 @@ async function poseidonHash(inputs: bigint[]): Promise<bigint> {
86
112
  return poseidon.F.toObject(result);
87
113
  }
88
114
 
89
- function bigIntToBytes32BE(n: bigint): Buffer {
115
+ function bigIntToBytes32BE(n: bigint): Uint8Array {
90
116
  if (n < 0n || n >= BN254_PRIME) {
91
117
  throw new Error(`bigint out of BN254 field range: ${n}`);
92
118
  }
93
- return Buffer.from(n.toString(16).padStart(64, "0"), "hex");
119
+ return bigintToBytes32(n);
94
120
  }
95
121
 
96
- function bytes32ToBigInt(buf: Buffer | Uint8Array): bigint {
97
- return BigInt("0x" + Buffer.from(buf).toString("hex"));
122
+ function bytes32ToBigInt(buf: Uint8Array): bigint {
123
+ return BigInt("0x" + hexFromBytes(buf));
98
124
  }
99
125
 
100
- function toBytes32(buf: Buffer | Uint8Array): number[] {
126
+ function toBytes32(buf: Uint8Array): number[] {
101
127
  return Array.from(buf.slice(0, 32));
102
128
  }
103
129
 
104
- function computeNameHash(name: string): Buffer {
105
- return createHash("sha256").update("nara-zk:" + name).digest();
130
+ async function computeNameHash(name: string): Promise<Uint8Array> {
131
+ const data = new TextEncoder().encode("nara-zk:" + name);
132
+ const digest = await crypto.subtle.digest("SHA-256", data);
133
+ return new Uint8Array(digest);
106
134
  }
107
135
 
108
- function denomBuf(denomination: BN): Buffer {
109
- return Buffer.from(denomination.toArray("le", 8));
136
+ function denomBuf(denomination: BN): Uint8Array {
137
+ const bytes = new Uint8Array(8);
138
+ const arr = denomination.toArray("le", 8);
139
+ bytes.set(arr);
140
+ return bytes;
110
141
  }
111
142
 
112
143
  function packProof(proof: {
113
144
  pi_a: string[];
114
145
  pi_b: string[][];
115
146
  pi_c: string[];
116
- }): Buffer {
147
+ }): Uint8Array {
117
148
  const ax = BigInt(proof.pi_a[0]!);
118
149
  const ay = BigInt(proof.pi_a[1]!);
119
150
  const ay_neg = BN254_PRIME - ay; // negate y: standard G1 negation on BN254
120
151
 
121
- const proofA = Buffer.concat([
122
- bigIntToBytes32BE(ax),
123
- bigIntToBytes32BE(ay_neg),
124
- ]);
125
- const proofB = Buffer.concat([
152
+ const proofA = concatBytes(bigIntToBytes32BE(ax), bigIntToBytes32BE(ay_neg));
153
+ const proofB = concatBytes(
126
154
  bigIntToBytes32BE(BigInt(proof.pi_b[0]![1]!)), // x.c1
127
155
  bigIntToBytes32BE(BigInt(proof.pi_b[0]![0]!)), // x.c0
128
156
  bigIntToBytes32BE(BigInt(proof.pi_b[1]![1]!)), // y.c1
129
157
  bigIntToBytes32BE(BigInt(proof.pi_b[1]![0]!)), // y.c0
130
- ]);
131
- const proofC = Buffer.concat([
158
+ );
159
+ const proofC = concatBytes(
132
160
  bigIntToBytes32BE(BigInt(proof.pi_c[0]!)),
133
161
  bigIntToBytes32BE(BigInt(proof.pi_c[1]!)),
134
- ]);
135
- return Buffer.concat([proofA, proofB, proofC]); // 256 bytes total
162
+ );
163
+ return concatBytes(proofA, proofB, proofC); // 256 bytes total
136
164
  }
137
165
 
138
166
  async function buildMerklePath(
139
167
  leafIndex: bigint,
140
- filledSubtrees: Buffer[],
168
+ filledSubtrees: Uint8Array[],
141
169
  zeros: bigint[]
142
170
  ): Promise<{ pathElements: bigint[]; pathIndices: number[] }> {
143
171
  const pathElements: bigint[] = new Array(MERKLE_LEVELS);
@@ -156,10 +184,10 @@ async function buildMerklePath(
156
184
  // Suppress snarkjs WASM console noise during proof generation.
157
185
  async function silentProve(
158
186
  input: Record<string, string | string[]>,
159
- wasmPath: string,
160
- zkeyPath: string
187
+ wasmPath: string | Uint8Array,
188
+ zkeyPath: string | Uint8Array
161
189
  ) {
162
- const snarkjs = _require("snarkjs");
190
+ const snarkjs: any = await import("snarkjs");
163
191
  const savedLog = console.log;
164
192
  const savedError = console.error;
165
193
  console.log = () => {};
@@ -208,31 +236,31 @@ function createReadProgram(
208
236
 
209
237
  // ─── PDA helpers ─────────────────────────────────────────────────────────────
210
238
 
211
- function findZkIdPda(nameHashBuf: Buffer, programId: PublicKey): [PublicKey, number] {
239
+ function findZkIdPda(nameHashBuf: Uint8Array, programId: PublicKey): [PublicKey, number] {
212
240
  return PublicKey.findProgramAddressSync(
213
- [Buffer.from("zk_id"), nameHashBuf],
241
+ [new TextEncoder().encode("zk_id"), nameHashBuf],
214
242
  programId
215
243
  );
216
244
  }
217
245
 
218
- function findInboxPda(nameHashBuf: Buffer, programId: PublicKey): [PublicKey, number] {
246
+ function findInboxPda(nameHashBuf: Uint8Array, programId: PublicKey): [PublicKey, number] {
219
247
  return PublicKey.findProgramAddressSync(
220
- [Buffer.from("inbox"), nameHashBuf],
248
+ [new TextEncoder().encode("inbox"), nameHashBuf],
221
249
  programId
222
250
  );
223
251
  }
224
252
 
225
253
  function findConfigPda(programId: PublicKey): [PublicKey, number] {
226
- return PublicKey.findProgramAddressSync([Buffer.from("config")], programId);
254
+ return PublicKey.findProgramAddressSync([new TextEncoder().encode("config")], programId);
227
255
  }
228
256
 
229
257
  function findNullifierPda(
230
258
  denomination: BN,
231
- nullifierHash: Buffer,
259
+ nullifierHash: Uint8Array,
232
260
  programId: PublicKey
233
261
  ): [PublicKey, number] {
234
262
  return PublicKey.findProgramAddressSync(
235
- [Buffer.from("nullifier"), denomBuf(denomination), nullifierHash],
263
+ [new TextEncoder().encode("nullifier"), denomBuf(denomination), nullifierHash],
236
264
  programId
237
265
  );
238
266
  }
@@ -248,10 +276,10 @@ function findNullifierPda(
248
276
  * has a unique idSecret, preventing nullifier collisions.
249
277
  */
250
278
  export async function deriveIdSecret(keypair: Keypair, name: string): Promise<bigint> {
251
- const message = Buffer.from(`nara-zk:idsecret:v1:${name}`);
279
+ const message = new TextEncoder().encode(`nara-zk:idsecret:v1:${name}`);
252
280
  const sig = nacl.sign.detached(message, keypair.secretKey);
253
- const digest = createHash("sha256").update(sig).digest();
254
- const n = BigInt("0x" + digest.toString("hex"));
281
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", sig));
282
+ const n = BigInt("0x" + hexFromBytes(digest));
255
283
  return (n % (BN254_PRIME - 1n)) + 1n;
256
284
  }
257
285
 
@@ -260,7 +288,7 @@ export async function deriveIdSecret(keypair: Keypair, name: string): Promise<bi
260
288
  * The ZK withdraw circuit encodes the recipient as a BN254 field element.
261
289
  */
262
290
  export function isValidRecipient(pubkey: PublicKey): boolean {
263
- return bytes32ToBigInt(Buffer.from(pubkey.toBytes())) < BN254_PRIME;
291
+ return bytes32ToBigInt(pubkey.toBytes()) < BN254_PRIME;
264
292
  }
265
293
 
266
294
  /**
@@ -285,7 +313,7 @@ export async function getZkIdInfo(
285
313
  ): Promise<ZkIdInfo | null> {
286
314
  const program = createReadProgram(connection, options?.programId);
287
315
  const programId = new PublicKey(options?.programId ?? DEFAULT_ZKID_PROGRAM_ID);
288
- const [zkIdPda] = findZkIdPda(computeNameHash(name), programId);
316
+ const [zkIdPda] = findZkIdPda(await computeNameHash(name), programId);
289
317
  try {
290
318
  const data = await program.account.zkIdAccount.fetch(zkIdPda);
291
319
  return {
@@ -316,7 +344,7 @@ export async function createZkId(
316
344
  const program = createProgram(connection, payer, options?.programId);
317
345
  const programId = new PublicKey(options?.programId ?? DEFAULT_ZKID_PROGRAM_ID);
318
346
 
319
- const nameHashBuf = computeNameHash(name);
347
+ const nameHashBuf = await computeNameHash(name);
320
348
  const idCommitment = await poseidonHash([idSecret]);
321
349
  const idCommitmentBuf = bigIntToBytes32BE(idCommitment);
322
350
 
@@ -347,7 +375,7 @@ export async function deposit(
347
375
  options?: ZkIdOptions
348
376
  ): Promise<string> {
349
377
  const program = createProgram(connection, payer, options?.programId);
350
- const nameHashBuf = computeNameHash(name);
378
+ const nameHashBuf = await computeNameHash(name);
351
379
 
352
380
  return await program.methods
353
381
  .deposit(toBytes32(nameHashBuf), denomination)
@@ -369,7 +397,7 @@ export async function scanClaimableDeposits(
369
397
  ): Promise<ClaimableDeposit[]> {
370
398
  const program = createReadProgram(connection, options?.programId);
371
399
  const programId = new PublicKey(options?.programId ?? DEFAULT_ZKID_PROGRAM_ID);
372
- const nameHashBuf = computeNameHash(name);
400
+ const nameHashBuf = await computeNameHash(name);
373
401
 
374
402
  const [zkIdPda] = findZkIdPda(nameHashBuf, programId);
375
403
  const [inboxPda] = findInboxPda(nameHashBuf, programId);
@@ -453,19 +481,19 @@ export async function withdraw(
453
481
 
454
482
  // Fetch Merkle tree state
455
483
  const [treePda] = PublicKey.findProgramAddressSync(
456
- [Buffer.from("tree"), denomBuf(denominationBN)],
484
+ [new TextEncoder().encode("tree"), denomBuf(denominationBN)],
457
485
  programId
458
486
  );
459
487
  const treeData = await program.account.merkleTreeAccount.fetch(treePda);
460
488
 
461
489
  const rootIdx: number = treeData.currentRootIndex;
462
- const root = Buffer.from((treeData.roots as number[][])[rootIdx]!);
490
+ const root = new Uint8Array((treeData.roots as number[][])[rootIdx]!);
463
491
  const filledSubtrees = (treeData.filledSubtrees as number[][]).map(s =>
464
- Buffer.from(s)
492
+ new Uint8Array(s)
465
493
  );
466
494
  // Use on-chain precomputed zeros (avoids expensive client-side computation)
467
495
  const zeros = (treeData.zeros as number[][]).map(z =>
468
- bytes32ToBigInt(Buffer.from(z))
496
+ bytes32ToBigInt(new Uint8Array(z))
469
497
  );
470
498
 
471
499
  const { pathElements, pathIndices } = await buildMerklePath(
@@ -476,7 +504,7 @@ export async function withdraw(
476
504
 
477
505
  const nullifier = await poseidonHash([idSecret, BigInt(depositInfo.depositIndex)]);
478
506
  const nullifierHashBuf = bigIntToBytes32BE(nullifier);
479
- const recipientField = bytes32ToBigInt(Buffer.from(recipient.toBytes()));
507
+ const recipientField = bytes32ToBigInt(recipient.toBytes());
480
508
 
481
509
  const input = {
482
510
  idSecret: idSecret.toString(),
@@ -488,12 +516,20 @@ export async function withdraw(
488
516
  recipient: recipientField.toString(),
489
517
  };
490
518
 
491
- const { proof } = await silentProve(input, WITHDRAW_WASM, WITHDRAW_ZKEY);
519
+ let wasmSource = options?.withdrawWasm;
520
+ let zkeySource = options?.withdrawZkey;
521
+ if (!wasmSource || !zkeySource) {
522
+ const defaults = await resolveDefaultZkPaths();
523
+ wasmSource ??= defaults.withdrawWasm;
524
+ zkeySource ??= defaults.withdrawZkey;
525
+ }
526
+
527
+ const { proof } = await silentProve(input, wasmSource, zkeySource);
492
528
  const packedProof = packProof(proof);
493
529
 
494
530
  return await program.methods
495
531
  .withdraw(
496
- packedProof,
532
+ Buffer.from(packedProof) as any,
497
533
  toBytes32(root),
498
534
  toBytes32(nullifierHashBuf),
499
535
  recipient,
@@ -506,6 +542,98 @@ export async function withdraw(
506
542
  .rpc();
507
543
  }
508
544
 
545
+ /**
546
+ * Build a withdraw instruction without executing it.
547
+ * Useful for composing into an existing transaction.
548
+ *
549
+ * Same ZK proof generation as withdraw(), but returns a TransactionInstruction
550
+ * instead of sending the transaction.
551
+ *
552
+ * @param authority - The payer/signer public key (does not need Keypair since we don't sign)
553
+ * @param depositInfo - From scanClaimableDeposits()
554
+ * @param recipient - Destination address. Must satisfy isValidRecipient().
555
+ */
556
+ export async function makeWithdrawIx(
557
+ connection: Connection,
558
+ authority: PublicKey,
559
+ name: string,
560
+ idSecret: bigint,
561
+ depositInfo: ClaimableDeposit,
562
+ recipient: PublicKey,
563
+ options?: ZkIdOptions
564
+ ): Promise<TransactionInstruction> {
565
+ if (!isValidRecipient(recipient)) {
566
+ throw new Error(
567
+ "Recipient pubkey is >= BN254 field prime. Use generateValidRecipient() to get a compatible address."
568
+ );
569
+ }
570
+
571
+ const program = createReadProgram(connection, options?.programId);
572
+ const programId = new PublicKey(options?.programId ?? DEFAULT_ZKID_PROGRAM_ID);
573
+ const denominationBN = new BN(depositInfo.denomination.toString());
574
+
575
+ // Fetch Merkle tree state
576
+ const [treePda] = PublicKey.findProgramAddressSync(
577
+ [new TextEncoder().encode("tree"), denomBuf(denominationBN)],
578
+ programId
579
+ );
580
+ const treeData = await program.account.merkleTreeAccount.fetch(treePda);
581
+
582
+ const rootIdx: number = treeData.currentRootIndex;
583
+ const root = new Uint8Array((treeData.roots as number[][])[rootIdx]!);
584
+ const filledSubtrees = (treeData.filledSubtrees as number[][]).map(s =>
585
+ new Uint8Array(s)
586
+ );
587
+ const zeros = (treeData.zeros as number[][]).map(z =>
588
+ bytes32ToBigInt(new Uint8Array(z))
589
+ );
590
+
591
+ const { pathElements, pathIndices } = await buildMerklePath(
592
+ depositInfo.leafIndex,
593
+ filledSubtrees,
594
+ zeros
595
+ );
596
+
597
+ const nullifier = await poseidonHash([idSecret, BigInt(depositInfo.depositIndex)]);
598
+ const nullifierHashBuf = bigIntToBytes32BE(nullifier);
599
+ const recipientField = bytes32ToBigInt(recipient.toBytes());
600
+
601
+ const input = {
602
+ idSecret: idSecret.toString(),
603
+ depositIndex: depositInfo.depositIndex.toString(),
604
+ pathElements: pathElements.map(e => e.toString()),
605
+ pathIndices: pathIndices.map(i => i.toString()),
606
+ root: bytes32ToBigInt(root).toString(),
607
+ nullifierHash: nullifier.toString(),
608
+ recipient: recipientField.toString(),
609
+ };
610
+
611
+ let wasmSource = options?.withdrawWasm;
612
+ let zkeySource = options?.withdrawZkey;
613
+ if (!wasmSource || !zkeySource) {
614
+ const defaults = await resolveDefaultZkPaths();
615
+ wasmSource ??= defaults.withdrawWasm;
616
+ zkeySource ??= defaults.withdrawZkey;
617
+ }
618
+
619
+ const { proof } = await silentProve(input, wasmSource, zkeySource);
620
+ const packedProof = packProof(proof);
621
+
622
+ return program.methods
623
+ .withdraw(
624
+ Buffer.from(packedProof) as any,
625
+ toBytes32(root),
626
+ toBytes32(nullifierHashBuf),
627
+ recipient,
628
+ denominationBN
629
+ )
630
+ .accounts({
631
+ payer: authority,
632
+ recipient,
633
+ } as any)
634
+ .instruction();
635
+ }
636
+
509
637
  /**
510
638
  * Transfer ZK ID ownership to a new identity.
511
639
  *
@@ -526,13 +654,13 @@ export async function transferZkId(
526
654
  ): Promise<string> {
527
655
  const program = createProgram(connection, payer, options?.programId);
528
656
  const programId = new PublicKey(options?.programId ?? DEFAULT_ZKID_PROGRAM_ID);
529
- const nameHashBuf = computeNameHash(name);
657
+ const nameHashBuf = await computeNameHash(name);
530
658
 
531
659
  // Fetch current id_commitment from chain
532
660
  const [zkIdPda] = findZkIdPda(nameHashBuf, programId);
533
661
  const zkId = await program.account.zkIdAccount.fetch(zkIdPda);
534
662
  const currentCommitmentField = bytes32ToBigInt(
535
- Buffer.from(zkId.idCommitment as number[])
663
+ new Uint8Array(zkId.idCommitment as number[])
536
664
  );
537
665
 
538
666
  // Compute new id_commitment
@@ -544,14 +672,23 @@ export async function transferZkId(
544
672
  idSecret: currentIdSecret.toString(),
545
673
  idCommitment: currentCommitmentField.toString(),
546
674
  };
547
- const { proof } = await silentProve(input, OWNERSHIP_WASM, OWNERSHIP_ZKEY);
675
+
676
+ let wasmSource = options?.ownershipWasm;
677
+ let zkeySource = options?.ownershipZkey;
678
+ if (!wasmSource || !zkeySource) {
679
+ const defaults = await resolveDefaultZkPaths();
680
+ wasmSource ??= defaults.ownershipWasm;
681
+ zkeySource ??= defaults.ownershipZkey;
682
+ }
683
+
684
+ const { proof } = await silentProve(input, wasmSource, zkeySource);
548
685
  const packedProof = packProof(proof);
549
686
 
550
687
  return await program.methods
551
688
  .transferZkId(
552
689
  toBytes32(nameHashBuf),
553
690
  toBytes32(newCommitmentBuf),
554
- packedProof
691
+ Buffer.from(packedProof) as any
555
692
  )
556
693
  .accounts({ payer: payer.publicKey } as any)
557
694
  .rpc();
@@ -566,7 +703,7 @@ export async function transferZkId(
566
703
  export async function computeIdCommitment(keypair: Keypair, name: string): Promise<string> {
567
704
  const idSecret = await deriveIdSecret(keypair, name);
568
705
  const commitment = await poseidonHash([idSecret]);
569
- return bigIntToBytes32BE(commitment).toString("hex");
706
+ return hexFromBytes(bigIntToBytes32BE(commitment));
570
707
  }
571
708
 
572
709
  /**
@@ -588,12 +725,12 @@ export async function transferZkIdByCommitment(
588
725
  options?: ZkIdOptions
589
726
  ): Promise<string> {
590
727
  const program = createProgram(connection, payer, options?.programId);
591
- const nameHashBuf = computeNameHash(name);
728
+ const nameHashBuf = await computeNameHash(name);
592
729
 
593
730
  const [zkIdPda] = findZkIdPda(nameHashBuf, new PublicKey(options?.programId ?? DEFAULT_ZKID_PROGRAM_ID));
594
731
  const zkId = await program.account.zkIdAccount.fetch(zkIdPda);
595
732
  const currentCommitmentField = bytes32ToBigInt(
596
- Buffer.from(zkId.idCommitment as number[])
733
+ new Uint8Array(zkId.idCommitment as number[])
597
734
  );
598
735
 
599
736
  const newCommitmentBuf = bigIntToBytes32BE(newIdCommitment);
@@ -602,14 +739,23 @@ export async function transferZkIdByCommitment(
602
739
  idSecret: currentIdSecret.toString(),
603
740
  idCommitment: currentCommitmentField.toString(),
604
741
  };
605
- const { proof } = await silentProve(input, OWNERSHIP_WASM, OWNERSHIP_ZKEY);
742
+
743
+ let wasmSource = options?.ownershipWasm;
744
+ let zkeySource = options?.ownershipZkey;
745
+ if (!wasmSource || !zkeySource) {
746
+ const defaults = await resolveDefaultZkPaths();
747
+ wasmSource ??= defaults.ownershipWasm;
748
+ zkeySource ??= defaults.ownershipZkey;
749
+ }
750
+
751
+ const { proof } = await silentProve(input, wasmSource, zkeySource);
606
752
  const packedProof = packProof(proof);
607
753
 
608
754
  return await program.methods
609
755
  .transferZkId(
610
756
  toBytes32(nameHashBuf),
611
757
  toBytes32(newCommitmentBuf),
612
- packedProof
758
+ Buffer.from(packedProof) as any
613
759
  )
614
760
  .accounts({ payer: payer.publicKey } as any)
615
761
  .rpc();
@@ -630,18 +776,19 @@ export async function getConfig(
630
776
  ): Promise<{ admin: PublicKey; feeRecipient: PublicKey; feeAmount: number }> {
631
777
  const programId = new PublicKey(options?.programId ?? DEFAULT_ZKID_PROGRAM_ID);
632
778
  const [configPda] = PublicKey.findProgramAddressSync(
633
- [Buffer.from("config")],
779
+ [new TextEncoder().encode("config")],
634
780
  programId
635
781
  );
636
782
  const accountInfo = await connection.getAccountInfo(configPda);
637
783
  if (!accountInfo) {
638
784
  throw new Error("ZK ID config account not found");
639
785
  }
640
- const buf = Buffer.from(accountInfo.data);
786
+ const data = accountInfo.data;
787
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
641
788
  let offset = 8; // skip discriminator
642
- const admin = new PublicKey(buf.subarray(offset, offset + 32)); offset += 32;
643
- const feeRecipient = new PublicKey(buf.subarray(offset, offset + 32)); offset += 32;
644
- const feeAmount = Number(buf.readBigUInt64LE(offset));
789
+ const admin = new PublicKey(data.subarray(offset, offset + 32)); offset += 32;
790
+ const feeRecipient = new PublicKey(data.subarray(offset, offset + 32)); offset += 32;
791
+ const feeAmount = Number(view.getBigUint64(offset, true));
645
792
  return { admin, feeRecipient, feeAmount };
646
793
  }
647
794