@veil-cash/sdk 0.6.3 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veil-cash/sdk",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "SDK and CLI for interacting with Veil Cash privacy pools - keypair generation, deposits, and status checking",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -48,6 +48,7 @@
48
48
  "url": "https://github.com/veildotcash/veildotcash-sdk"
49
49
  },
50
50
  "dependencies": {
51
+ "buffer": "^6.0.3",
51
52
  "circomlib": "github:tornadocash/circomlib#d20d53411d1bef61f38c99a8b36d5d0cc4836aa1",
52
53
  "commander": "^14.0.2",
53
54
  "dotenv": "^17.2.3",
@@ -0,0 +1,28 @@
1
+ declare module 'ffjavascript' {
2
+ export const utils: {
3
+ stringifyBigInts: (obj: unknown) => unknown;
4
+ unstringifyBigInts: (obj: unknown) => unknown;
5
+ };
6
+ }
7
+
8
+ declare module 'circomlib' {
9
+ const circomlib: {
10
+ poseidon: (items: Array<bigint | string | number>) => { toString: () => string };
11
+ };
12
+ export default circomlib;
13
+ }
14
+
15
+ declare module 'eth-sig-util' {
16
+ import type { EncryptedMessage } from './types.js';
17
+
18
+ const ethSigUtil: {
19
+ getEncryptionPublicKey: (privateKey: string) => string;
20
+ encrypt: (
21
+ receiverPublicKey: string,
22
+ msgParams: { data: string },
23
+ version: 'x25519-xsalsa20-poly1305'
24
+ ) => EncryptedMessage;
25
+ decrypt: (encryptedData: EncryptedMessage, receiverPrivateKey: string) => string;
26
+ };
27
+ export default ethSigUtil;
28
+ }
package/src/index.ts CHANGED
@@ -96,7 +96,7 @@ export {
96
96
  selectCircuit,
97
97
  CIRCUIT_CONFIG,
98
98
  } from './prover.js';
99
- export type { ProofInput } from './prover.js';
99
+ export type { ProofInput, ProveOptions, ProvingKeyPath } from './prover.js';
100
100
 
101
101
  // Addresses and config
102
102
  export {
package/src/keypair.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  */
5
5
 
6
6
  import { ethers } from 'ethers';
7
+ import { Buffer } from 'buffer';
7
8
  import { privateKeyToAccount } from 'viem/accounts';
9
+ import ethSigUtil from 'eth-sig-util';
8
10
  import { poseidonHash, toFixedHex } from './utils.js';
9
11
  import type { EncryptedMessage } from './types.js';
10
12
 
@@ -20,10 +22,6 @@ export const VEIL_SIGNED_MESSAGE = "Sign this message to create your Veil Wallet
20
22
  */
21
23
  export type MessageSigner = (message: string) => Promise<string>;
22
24
 
23
- // eth-sig-util for x25519 encryption
24
- // eslint-disable-next-line @typescript-eslint/no-require-imports
25
- const ethSigUtil = require('eth-sig-util');
26
-
27
25
  /**
28
26
  * Pack encrypted message into hex string
29
27
  */
package/src/prover.ts CHANGED
@@ -4,10 +4,8 @@
4
4
  */
5
5
 
6
6
  import { groth16 } from 'snarkjs';
7
+ import { utils } from 'ffjavascript';
7
8
  import { toFixedHex } from './utils.js';
8
- import * as path from 'path';
9
- import * as fs from 'fs';
10
- import { fileURLToPath } from 'url';
11
9
 
12
10
  // Type definition for ffjavascript utils
13
11
  interface FFJavascriptUtils {
@@ -15,15 +13,7 @@ interface FFJavascriptUtils {
15
13
  unstringifyBigInts: (obj: unknown) => unknown;
16
14
  }
17
15
 
18
- // Dynamic import for ffjavascript
19
- let utils: FFJavascriptUtils | null = null;
20
- try {
21
- // eslint-disable-next-line @typescript-eslint/no-require-imports
22
- const ffjavascript = require('ffjavascript');
23
- utils = ffjavascript.utils;
24
- } catch {
25
- console.warn('ffjavascript not found. Proof generation may not work.');
26
- }
16
+ const ffUtils = utils as FFJavascriptUtils;
27
17
 
28
18
  /**
29
19
  * Input data for ZK proof generation
@@ -62,19 +52,70 @@ interface ProveResult {
62
52
  publicSignals: string[];
63
53
  }
64
54
 
55
+ /**
56
+ * Directory/base URL where proving keys are hosted, or a resolver that returns
57
+ * the circuit base path without the `.wasm` / `.zkey` extension.
58
+ */
59
+ export type ProvingKeyPath = string | ((circuitName: string) => string);
60
+
61
+ export interface ProveOptions {
62
+ /**
63
+ * Proving key location.
64
+ *
65
+ * In Node this defaults to the package/source `keys` directory. In browsers
66
+ * this defaults to `/keys`, so apps can serve `/keys/transaction2.wasm` and
67
+ * `/keys/transaction2.zkey` from their own origin.
68
+ */
69
+ provingKeyPath?: ProvingKeyPath;
70
+ /** Force snarkjs single-threaded proving. Defaults to true. */
71
+ singleThread?: boolean;
72
+ }
73
+
74
+ function isBrowserRuntime(): boolean {
75
+ return !(typeof process !== 'undefined' && !!process.versions?.node);
76
+ }
77
+
78
+ function stripTrailingSlash(value: string): string {
79
+ return value.endsWith('/') ? value.slice(0, -1) : value;
80
+ }
81
+
82
+ function normalizeCircuitBasePath(provingKeyPath: ProvingKeyPath, circuitName: string): string {
83
+ const resolvedPath =
84
+ typeof provingKeyPath === 'function' ? provingKeyPath(circuitName) : provingKeyPath;
85
+
86
+ const withoutExtension = resolvedPath.replace(/\.(wasm|zkey)$/i, '');
87
+ if (withoutExtension.endsWith(`/${circuitName}`) || withoutExtension.endsWith(`\\${circuitName}`)) {
88
+ return withoutExtension;
89
+ }
90
+
91
+ return `${stripTrailingSlash(withoutExtension)}/${circuitName}`;
92
+ }
93
+
94
+ function importNodeModule<T>(specifier: string): Promise<T> {
95
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (
96
+ specifier: string
97
+ ) => Promise<T>;
98
+ return dynamicImport(specifier);
99
+ }
100
+
65
101
  /**
66
102
  * Find the keys directory containing circuit files
67
103
  * Works in both development and installed package scenarios
68
104
  */
69
- function findKeysDirectory(): string {
105
+ async function findNodeKeysDirectory(): Promise<string> {
106
+ const [{ existsSync }, pathModule, { fileURLToPath }] = await Promise.all([
107
+ importNodeModule<typeof import('node:fs')>('node:fs'),
108
+ importNodeModule<typeof import('node:path')>('node:path'),
109
+ importNodeModule<typeof import('node:url')>('node:url'),
110
+ ]);
111
+ const path = pathModule;
112
+
70
113
  // Try multiple possible locations
71
114
  const possiblePaths = [
72
115
  // When running from package (installed via npm)
73
- path.resolve(__dirname, '..', 'keys'),
74
- path.resolve(__dirname, '..', '..', 'keys'),
116
+ path.resolve(process.cwd(), 'node_modules', '@veil-cash', 'sdk', 'keys'),
75
117
  // When running from source
76
118
  path.resolve(process.cwd(), 'keys'),
77
- // ESM module path
78
119
  ];
79
120
 
80
121
  // Try to get module directory for ESM
@@ -83,11 +124,11 @@ function findKeysDirectory(): string {
83
124
  const currentDir = path.dirname(currentFilePath);
84
125
  possiblePaths.unshift(path.resolve(currentDir, '..', 'keys'));
85
126
  } catch {
86
- // Not ESM environment, use __dirname
127
+ // Ignore non-file module URLs.
87
128
  }
88
129
 
89
130
  for (const p of possiblePaths) {
90
- if (fs.existsSync(p) && fs.existsSync(path.join(p, 'transaction2.wasm'))) {
131
+ if (existsSync(p) && existsSync(path.join(p, 'transaction2.wasm'))) {
91
132
  return p;
92
133
  }
93
134
  }
@@ -97,6 +138,46 @@ function findKeysDirectory(): string {
97
138
  );
98
139
  }
99
140
 
141
+ async function resolveProvingKeyPaths(
142
+ circuitName: string,
143
+ provingKeyPath?: ProvingKeyPath,
144
+ ): Promise<{ wasmPath: string; zkeyPath: string }> {
145
+ if (provingKeyPath) {
146
+ const circuitBasePath = normalizeCircuitBasePath(provingKeyPath, circuitName);
147
+ return {
148
+ wasmPath: `${circuitBasePath}.wasm`,
149
+ zkeyPath: `${circuitBasePath}.zkey`,
150
+ };
151
+ }
152
+
153
+ if (isBrowserRuntime()) {
154
+ return {
155
+ wasmPath: `/keys/${circuitName}.wasm`,
156
+ zkeyPath: `/keys/${circuitName}.zkey`,
157
+ };
158
+ }
159
+
160
+ const keysDir = await findNodeKeysDirectory();
161
+ return {
162
+ wasmPath: `${keysDir}/${circuitName}.wasm`,
163
+ zkeyPath: `${keysDir}/${circuitName}.zkey`,
164
+ };
165
+ }
166
+
167
+ async function assertNodeKeyFilesExist(wasmPath: string, zkeyPath: string): Promise<void> {
168
+ if (isBrowserRuntime() || wasmPath.startsWith('http://') || wasmPath.startsWith('https://')) {
169
+ return;
170
+ }
171
+
172
+ const { existsSync } = await importNodeModule<typeof import('node:fs')>('node:fs');
173
+ if (!existsSync(wasmPath)) {
174
+ throw new Error(`Circuit WASM file not found: ${wasmPath}`);
175
+ }
176
+ if (!existsSync(zkeyPath)) {
177
+ throw new Error(`Circuit zkey file not found: ${zkeyPath}`);
178
+ }
179
+ }
180
+
100
181
  /**
101
182
  * Generate a ZK proof for a transaction
102
183
  *
@@ -110,32 +191,23 @@ function findKeysDirectory(): string {
110
191
  * // Returns: 0x1234...abcd (256 bytes hex)
111
192
  * ```
112
193
  */
113
- export async function prove(input: ProofInput, circuitName: string): Promise<string> {
114
- if (!utils) {
115
- throw new Error('ffjavascript is required for proof generation. Please install it: npm install ffjavascript');
116
- }
117
-
118
- const keysDir = findKeysDirectory();
119
- const wasmPath = path.join(keysDir, `${circuitName}.wasm`);
120
- const zkeyPath = path.join(keysDir, `${circuitName}.zkey`);
121
-
122
- // Verify files exist
123
- if (!fs.existsSync(wasmPath)) {
124
- throw new Error(`Circuit WASM file not found: ${wasmPath}`);
125
- }
126
- if (!fs.existsSync(zkeyPath)) {
127
- throw new Error(`Circuit zkey file not found: ${zkeyPath}`);
128
- }
194
+ export async function prove(
195
+ input: ProofInput,
196
+ circuitName: string,
197
+ options: ProveOptions = {},
198
+ ): Promise<string> {
199
+ const { wasmPath, zkeyPath } = await resolveProvingKeyPaths(circuitName, options.provingKeyPath);
200
+ await assertNodeKeyFilesExist(wasmPath, zkeyPath);
129
201
 
130
202
  // Generate proof using snarkjs
131
203
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
132
204
  const result = await groth16.fullProve(
133
- utils.stringifyBigInts(input) as any,
205
+ ffUtils.stringifyBigInts(input) as any,
134
206
  wasmPath,
135
207
  zkeyPath,
136
208
  undefined,
137
209
  undefined,
138
- { singleThread: true },
210
+ { singleThread: options.singleThread ?? true },
139
211
  );
140
212
  const proof = result.proof as unknown as SnarkProof;
141
213
 
package/src/subaccount.ts CHANGED
@@ -563,6 +563,7 @@ export async function mergeSubaccount(
563
563
  pool = 'eth',
564
564
  rpcUrl,
565
565
  relayUrl,
566
+ provingKeyPath,
566
567
  onProgress,
567
568
  } = options;
568
569
 
@@ -684,6 +685,7 @@ export async function mergeSubaccount(
684
685
  recipient: '0x0000000000000000000000000000000000000000',
685
686
  relayer: '0x0000000000000000000000000000000000000000',
686
687
  onProgress,
688
+ provingKeyPath,
687
689
  });
688
690
 
689
691
  // Submit to relay
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { buildMerkleTree, MERKLE_TREE_HEIGHT } from './merkle.js';
7
- import { prove, selectCircuit, type ProofInput } from './prover.js';
7
+ import { prove, selectCircuit, type ProofInput, type ProvingKeyPath } from './prover.js';
8
8
  import { toFixedHex, getExtDataHash, shuffle, FIELD_SIZE } from './utils.js';
9
9
  import { Utxo } from './utxo.js';
10
10
 
@@ -58,6 +58,8 @@ export interface PrepareTransactionParams {
58
58
  relayer?: string | bigint | number;
59
59
  /** Optional progress callback */
60
60
  onProgress?: (stage: string, detail?: string) => void;
61
+ /** Optional proving key directory/base URL or circuit path resolver */
62
+ provingKeyPath?: ProvingKeyPath;
61
63
  }
62
64
 
63
65
  /**
@@ -72,6 +74,7 @@ interface GetProofParams {
72
74
  recipient: string | bigint;
73
75
  relayer: string | bigint;
74
76
  onProgress?: (stage: string, detail?: string) => void;
77
+ provingKeyPath?: ProvingKeyPath;
75
78
  }
76
79
 
77
80
  async function getProof({
@@ -83,6 +86,7 @@ async function getProof({
83
86
  recipient,
84
87
  relayer,
85
88
  onProgress,
89
+ provingKeyPath,
86
90
  }: GetProofParams): Promise<TransactionResult> {
87
91
  // Shuffle inputs and outputs for privacy
88
92
  inputs = shuffle([...inputs]);
@@ -160,7 +164,7 @@ async function getProof({
160
164
 
161
165
  // Select circuit based on input count and generate proof
162
166
  const circuitName = selectCircuit(inputs.length);
163
- const proof = await prove(proofInput, circuitName);
167
+ const proof = await prove(proofInput, circuitName, { provingKeyPath });
164
168
 
165
169
  // Build proof arguments for on-chain verification
166
170
  const args: ProofArgs = {
@@ -218,6 +222,7 @@ export async function prepareTransaction({
218
222
  recipient = 0,
219
223
  relayer = 0,
220
224
  onProgress,
225
+ provingKeyPath,
221
226
  }: PrepareTransactionParams): Promise<TransactionResult> {
222
227
  // Validate input/output counts
223
228
  if (inputs.length > 16 || outputs.length > 2) {
@@ -254,6 +259,7 @@ export async function prepareTransaction({
254
259
  recipient: String(recipient),
255
260
  relayer: String(relayer),
256
261
  onProgress,
262
+ provingKeyPath,
257
263
  });
258
264
 
259
265
  return result;
package/src/transfer.ts CHANGED
@@ -140,6 +140,7 @@ export async function buildTransferProof(
140
140
  senderKeypair,
141
141
  pool = 'eth',
142
142
  rpcUrl,
143
+ provingKeyPath,
143
144
  onProgress,
144
145
  } = options;
145
146
 
@@ -246,6 +247,7 @@ export async function buildTransferProof(
246
247
  recipient: '0x0000000000000000000000000000000000000000',
247
248
  relayer: '0x0000000000000000000000000000000000000000',
248
249
  onProgress,
250
+ provingKeyPath,
249
251
  });
250
252
 
251
253
  return {
@@ -338,9 +340,10 @@ export async function mergeUtxos(options: {
338
340
  keypair: Keypair;
339
341
  pool?: RelayPool;
340
342
  rpcUrl?: string;
343
+ provingKeyPath?: import('./prover.js').ProvingKeyPath;
341
344
  onProgress?: (stage: string, detail?: string) => void;
342
345
  }): Promise<TransferResult> {
343
- const { amount, keypair, pool = 'eth', rpcUrl, onProgress } = options;
346
+ const { amount, keypair, pool = 'eth', rpcUrl, provingKeyPath, onProgress } = options;
344
347
 
345
348
  const poolConfig = POOL_CONFIG[pool];
346
349
  const poolAddress = getPoolAddress(pool);
@@ -432,6 +435,7 @@ export async function mergeUtxos(options: {
432
435
  recipient: '0x0000000000000000000000000000000000000000',
433
436
  relayer: '0x0000000000000000000000000000000000000000',
434
437
  onProgress,
438
+ provingKeyPath,
435
439
  });
436
440
 
437
441
  // 6. Submit to relay
package/src/types.ts CHANGED
@@ -228,6 +228,13 @@ export interface BuildWithdrawProofOptions {
228
228
  pool?: RelayPool;
229
229
  /** Optional RPC URL */
230
230
  rpcUrl?: string;
231
+ /**
232
+ * Optional proving key directory/base URL or resolver.
233
+ *
234
+ * Browsers default to `/keys`, expecting files like
235
+ * `/keys/transaction2.wasm` and `/keys/transaction2.zkey`.
236
+ */
237
+ provingKeyPath?: import('./prover.js').ProvingKeyPath;
231
238
  /** Progress callback */
232
239
  onProgress?: (stage: string, detail?: string) => void;
233
240
  }
@@ -246,6 +253,13 @@ export interface BuildTransferProofOptions {
246
253
  pool?: RelayPool;
247
254
  /** Optional RPC URL */
248
255
  rpcUrl?: string;
256
+ /**
257
+ * Optional proving key directory/base URL or resolver.
258
+ *
259
+ * Browsers default to `/keys`, expecting files like
260
+ * `/keys/transaction2.wasm` and `/keys/transaction2.zkey`.
261
+ */
262
+ provingKeyPath?: import('./prover.js').ProvingKeyPath;
249
263
  /** Progress callback */
250
264
  onProgress?: (stage: string, detail?: string) => void;
251
265
  }
@@ -461,6 +475,13 @@ export interface SubaccountMergeOptions {
461
475
  rpcUrl?: string;
462
476
  /** Optional relay URL */
463
477
  relayUrl?: string;
478
+ /**
479
+ * Optional proving key directory/base URL or resolver.
480
+ *
481
+ * Browsers default to `/keys`, expecting files like
482
+ * `/keys/transaction2.wasm` and `/keys/transaction2.zkey`.
483
+ */
484
+ provingKeyPath?: import('./prover.js').ProvingKeyPath;
464
485
  /** Progress callback */
465
486
  onProgress?: (stage: string, detail?: string) => void;
466
487
  }
package/src/utils.ts CHANGED
@@ -3,10 +3,10 @@
3
3
  * Poseidon hash, hex conversion, and random number generation
4
4
  */
5
5
 
6
- import * as crypto from 'crypto';
6
+ import { ethers } from 'ethers';
7
+ import { Buffer } from 'buffer';
8
+ import circomlib from 'circomlib';
7
9
 
8
- // eslint-disable-next-line @typescript-eslint/no-require-imports
9
- const circomlib = require('circomlib');
10
10
  const poseidon = circomlib.poseidon;
11
11
 
12
12
  /**
@@ -39,7 +39,15 @@ export const poseidonHash2 = (a: bigint | string | number, b: bigint | string |
39
39
  * @returns Random bigint
40
40
  */
41
41
  export const randomBN = (nbytes: number = 31): bigint => {
42
- const bytes = crypto.randomBytes(nbytes);
42
+ const cryptoApi = (globalThis as unknown as {
43
+ crypto?: { getRandomValues?: <T extends Uint8Array>(array: T) => T };
44
+ }).crypto;
45
+
46
+ if (!cryptoApi?.getRandomValues) {
47
+ throw new Error('Secure random number generation is unavailable in this runtime');
48
+ }
49
+
50
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(nbytes));
43
51
  let hex = '0x';
44
52
  for (let i = 0; i < bytes.length; i++) {
45
53
  hex += bytes[i].toString(16).padStart(2, '0');
@@ -106,7 +114,6 @@ export interface ExtDataInput {
106
114
  */
107
115
  export function getExtDataHash(extData: ExtDataInput): bigint {
108
116
  // Use ethers ABI encoder for Solidity-compatible encoding
109
- const { ethers } = require('ethers');
110
117
  const abi = ethers.AbiCoder.defaultAbiCoder();
111
118
 
112
119
  // Encode the struct exactly as Solidity would
package/src/utxo.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * Represents a private balance entry that can be spent
4
4
  */
5
5
 
6
+ import { Buffer } from 'buffer';
6
7
  import { Keypair } from './keypair.js';
7
8
  import { poseidonHash, toBuffer, randomBN } from './utils.js';
8
9
 
package/src/withdraw.ts CHANGED
@@ -145,6 +145,7 @@ export async function buildWithdrawProof(
145
145
  keypair,
146
146
  pool = 'eth',
147
147
  rpcUrl,
148
+ provingKeyPath,
148
149
  onProgress,
149
150
  } = options;
150
151
 
@@ -231,6 +232,7 @@ export async function buildWithdrawProof(
231
232
  recipient,
232
233
  relayer: '0x0000000000000000000000000000000000000000',
233
234
  onProgress,
235
+ provingKeyPath,
234
236
  });
235
237
 
236
238
  return {