@veil-cash/sdk 0.6.2 → 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.2",
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",
@@ -165,7 +165,7 @@ What do you want to do?
165
165
  |
166
166
  +-- Register deposit key on-chain → veil register [--unsigned]
167
167
  |
168
- +-- Deposit ETH or USDC → veil deposit <asset> <amount> [--unsigned]
168
+ +-- Deposit ETH or USDC → veil deposit <asset> <amount> [--unsigned --address 0x...]
169
169
  |
170
170
  +-- Check balances → veil balance [queue|private] [--pool eth|usdc]
171
171
  |
@@ -330,7 +330,9 @@ Important:
330
330
  - If not yet registered, returns a normal `register` payload.
331
331
 
332
332
  Deposits treat the CLI amount as the **net** amount that lands in the pool.
333
- The `0.3%` protocol fee is calculated on-chain and added automatically.
333
+ Each address gets a configurable number of fee-free deposits per UTC day.
334
+ The CLI checks automatically — if free slots remain the fee is waived;
335
+ otherwise the `0.3%` protocol fee is calculated on-chain and added.
334
336
  After submission, deposits go through screening / queue processing before they
335
337
  are accepted into the private pool. This typically takes around `10-15 minutes`.
336
338
 
@@ -338,8 +340,8 @@ are accepted into the private pool. This typically takes around `10-15 minutes`.
338
340
  veil deposit ETH 0.1
339
341
  veil deposit USDC 100
340
342
  veil deposit ETH 0.1 --json
341
- veil deposit ETH 0.1 --unsigned
342
- veil deposit USDC 100 --unsigned
343
+ veil deposit ETH 0.1 --unsigned --address 0x...
344
+ SIGNER_ADDRESS=0x... veil deposit USDC 100 --unsigned
343
345
  ```
344
346
 
345
347
  Minimums:
@@ -258,8 +258,8 @@ SIGNER_ADDRESS=0x... veil register --unsigned --force # Unsigned register/change
258
258
  veil register --unsigned --address 0x... # Unsigned register payload (explicit address)
259
259
  veil register --json # Register and output result as JSON
260
260
 
261
- veil deposit ETH 0.1 --unsigned # Unsigned ETH deposit payload
262
- veil deposit USDC 100 --unsigned # Unsigned USDC deposit payload(s)
261
+ veil deposit ETH 0.1 --unsigned --address 0x... # Unsigned ETH deposit payload
262
+ SIGNER_ADDRESS=0x... veil deposit USDC 100 --unsigned # Unsigned USDC deposit payload(s)
263
263
  veil deposit ETH 0.1 --json # Deposit and output result as JSON
264
264
 
265
265
  veil balance # All pool balances
@@ -307,10 +307,10 @@ Common codes: `VEIL_KEY_MISSING`, `WALLET_KEY_MISSING`, `DEPOSIT_KEY_MISSING`,
307
307
 
308
308
  | Asset | Minimum (net) | Notes |
309
309
  |-------|--------------|-------|
310
- | ETH | 0.01 | Fee (0.3%) added automatically via on-chain `getDepositAmountWithFee` |
311
- | USDC | 10 | Fee (0.3%) added automatically via on-chain `getDepositAmountWithFee` |
310
+ | ETH | 0.01 | Fee (0.3%) added automatically, or waived if daily free deposits remain |
311
+ | USDC | 10 | Fee (0.3%) added automatically, or waived if daily free deposits remain |
312
312
 
313
- The CLI amount is the **net** amount that lands in the pool. The fee is calculated on-chain and added to the transaction automatically — users do not need to account for it.
313
+ The CLI amount is the **net** amount that lands in the pool. The CLI checks `getDailyFreeRemaining` on the queue contract — if the user has free slots left today the fee is skipped; otherwise the 0.3% fee is calculated on-chain and added to the transaction automatically.
314
314
 
315
315
  ---
316
316
 
package/src/abi.ts CHANGED
@@ -218,6 +218,14 @@ export const QUEUE_ABI = [
218
218
  stateMutability: 'view',
219
219
  type: 'function',
220
220
  },
221
+ // Get remaining daily free deposits for an address (V3+)
222
+ {
223
+ inputs: [{ name: '_depositor', type: 'address' }],
224
+ name: 'getDailyFreeRemaining',
225
+ outputs: [{ name: 'remaining', type: 'uint256' }],
226
+ stateMutability: 'view',
227
+ type: 'function',
228
+ },
221
229
  ] as const;
222
230
 
223
231
  /**
package/src/balance.ts CHANGED
@@ -127,6 +127,54 @@ export async function getQueueBalance(options: {
127
127
  };
128
128
  }
129
129
 
130
+ /**
131
+ * Get remaining daily free deposits for an address.
132
+ * Returns 0 if the queue contract has not been upgraded to V3 yet
133
+ * or if the daily free feature is disabled.
134
+ *
135
+ * @param options - Query options
136
+ * @param options.address - Depositor address to check
137
+ * @param options.pool - Pool identifier ('eth' or 'usdc', default: 'eth')
138
+ * @param options.rpcUrl - Optional RPC URL
139
+ * @returns Number of free deposits remaining today
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * const remaining = await getDailyFreeRemaining({
144
+ * address: '0x...',
145
+ * pool: 'eth',
146
+ * });
147
+ * console.log(`Free deposits left today: ${remaining}`);
148
+ * ```
149
+ */
150
+ export async function getDailyFreeRemaining(options: {
151
+ address: `0x${string}`;
152
+ pool?: RelayPool;
153
+ rpcUrl?: string;
154
+ }): Promise<number> {
155
+ const { address, pool = 'eth', rpcUrl } = options;
156
+ const queueAddress = getQueueAddress(pool);
157
+
158
+ const publicClient = createPublicClient({
159
+ chain: base,
160
+ transport: http(rpcUrl),
161
+ });
162
+
163
+ try {
164
+ const remaining = await publicClient.readContract({
165
+ address: queueAddress,
166
+ abi: QUEUE_ABI,
167
+ functionName: 'getDailyFreeRemaining',
168
+ args: [address],
169
+ }) as bigint;
170
+
171
+ return Number(remaining);
172
+ } catch {
173
+ // V2 contracts don't have this function — treat as 0 remaining
174
+ return 0;
175
+ }
176
+ }
177
+
130
178
  /**
131
179
  * Get private balance from the Pool contract
132
180
  * Decrypts all encrypted outputs, calculates nullifiers, and checks spent status
@@ -4,8 +4,9 @@
4
4
 
5
5
  import { Command } from 'commander';
6
6
  import { buildDepositETHTx, buildDepositUSDCTx, buildApproveUSDCTx } from '../../deposit.js';
7
+ import { getDailyFreeRemaining } from '../../balance.js';
7
8
  import { sendTransaction, getAddress, getBalance } from '../wallet.js';
8
- import { getConfig } from '../config.js';
9
+ import { getConfig, resolveAddress } from '../config.js';
9
10
  import { createPublicClient, http, parseEther, parseUnits, formatEther, formatUnits } from 'viem';
10
11
  import { base } from 'viem/chains';
11
12
  import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
@@ -13,6 +14,7 @@ import { clearProgress, createProgressReporter, printFields, printHeader, printJ
13
14
  import { POOL_CONFIG, getAddresses } from '../../addresses.js';
14
15
  import { ENTRY_ABI, ERC20_ABI } from '../../abi.js';
15
16
  import type { TransactionData } from '../../types.js';
17
+ import type { WalletConfig } from '../wallet.js';
16
18
 
17
19
  const MINIMUM_NET: Record<string, number> = {
18
20
  ETH: 0.01,
@@ -20,13 +22,22 @@ const MINIMUM_NET: Record<string, number> = {
20
22
  };
21
23
 
22
24
  /**
23
- * Query the Entry contract for the exact gross amount (net + fee).
24
- * This matches the contract's own fee math and avoids rounding mismatches.
25
+ * Compute the gross amount and fee for a deposit.
26
+ * Checks daily free deposit availability first if the user has
27
+ * free slots remaining the fee is waived and gross === net.
25
28
  */
26
29
  async function getGrossAmount(
27
30
  netWei: bigint,
31
+ depositor: `0x${string}`,
32
+ pool: 'eth' | 'usdc',
28
33
  rpcUrl: string | undefined,
29
- ): Promise<{ grossWei: bigint; feeWei: bigint }> {
34
+ ): Promise<{ grossWei: bigint; feeWei: bigint; dailyFreeUsed: boolean; dailyFreeRemaining: number }> {
35
+ const freeRemaining = await getDailyFreeRemaining({ address: depositor, pool, rpcUrl });
36
+
37
+ if (freeRemaining > 0) {
38
+ return { grossWei: netWei, feeWei: 0n, dailyFreeUsed: true, dailyFreeRemaining: freeRemaining - 1 };
39
+ }
40
+
30
41
  const publicClient = createPublicClient({
31
42
  chain: base,
32
43
  transport: http(rpcUrl),
@@ -39,7 +50,7 @@ async function getGrossAmount(
39
50
  args: [netWei],
40
51
  }) as bigint;
41
52
 
42
- return { grossWei, feeWei: grossWei - netWei };
53
+ return { grossWei, feeWei: grossWei - netWei, dailyFreeUsed: false, dailyFreeRemaining: 0 };
43
54
  }
44
55
 
45
56
  const SUPPORTED_ASSETS = ['ETH', 'USDC'];
@@ -49,16 +60,19 @@ export function createDepositCommand(): Command {
49
60
  .description('Deposit ETH or USDC into Veil')
50
61
  .argument('<asset>', 'Asset to deposit (ETH or USDC)')
51
62
  .argument('<amount>', 'Amount to deposit — this is what arrives in your Veil balance')
63
+ .option('--address <address>', 'Signer address (required in --unsigned mode unless SIGNER_ADDRESS or WALLET_KEY is set)')
52
64
  .option('--unsigned', 'Output unsigned transaction payload instead of sending')
53
65
  .option('--json', 'Output as JSON')
54
66
  .addHelpText('after', `
55
67
  The amount you specify is the net amount that lands in your Veil balance.
56
- The 0.3% protocol fee is automatically added on top.
68
+ A 0.3% protocol fee is normally added on top, but each address gets
69
+ free daily deposits (fee waived). The CLI checks automatically.
57
70
 
58
71
  Examples:
59
- veil deposit ETH 0.1 # deposits 0.1 ETH (sends ~0.1003 ETH)
60
- veil deposit USDC 100 # deposits 100 USDC (sends ~100.30 USDC)
61
- veil deposit ETH 0.1 --unsigned
72
+ veil deposit ETH 0.1 # deposits 0.1 ETH (free or ~0.1003 ETH)
73
+ veil deposit USDC 100 # deposits 100 USDC (free or ~100.30 USDC)
74
+ veil deposit ETH 0.1 --unsigned --address 0x...
75
+ SIGNER_ADDRESS=0x... veil deposit ETH 0.1 --unsigned
62
76
  veil deposit ETH 0.1 --json
63
77
  `)
64
78
  .action(async (asset: string, amount: string, options) => {
@@ -80,15 +94,41 @@ Examples:
80
94
  }
81
95
 
82
96
  const rpcUrl = process.env.RPC_URL;
83
- const poolConfig = POOL_CONFIG[assetUpper.toLowerCase() as 'eth' | 'usdc'];
97
+ const pool = assetUpper.toLowerCase() as 'eth' | 'usdc';
98
+ const poolConfig = POOL_CONFIG[pool];
84
99
  const netWei = assetUpper === 'ETH'
85
100
  ? parseEther(amount)
86
101
  : parseUnits(amount, poolConfig.decimals);
87
102
 
88
103
  const progress = createProgressReporter();
89
- progress('Calculating fee...');
90
104
 
91
- const { grossWei, feeWei } = await getGrossAmount(netWei, rpcUrl);
105
+ let config: WalletConfig | null = null;
106
+ let address: `0x${string}`;
107
+ let feeRpcUrl = rpcUrl;
108
+
109
+ if (options.unsigned) {
110
+ const resolved = resolveAddress({ address: options.address }, { required: true });
111
+ if (!resolved) {
112
+ throw new CLIError(
113
+ ErrorCode.WALLET_KEY_MISSING,
114
+ 'Must provide --address, set SIGNER_ADDRESS, or set WALLET_KEY env.',
115
+ );
116
+ }
117
+ address = resolved.address;
118
+ } else {
119
+ config = getConfig(options);
120
+ address = getAddress(config.privateKey);
121
+ feeRpcUrl = config.rpcUrl;
122
+ }
123
+
124
+ progress('Checking deposit fee...');
125
+
126
+ const { grossWei, feeWei, dailyFreeUsed, dailyFreeRemaining } = await getGrossAmount(
127
+ netWei,
128
+ address,
129
+ pool,
130
+ feeRpcUrl,
131
+ );
92
132
  const grossStr = assetUpper === 'ETH'
93
133
  ? formatEther(grossWei)
94
134
  : formatUnits(grossWei, poolConfig.decimals);
@@ -140,8 +180,9 @@ Examples:
140
180
  return;
141
181
  }
142
182
 
143
- const config = getConfig(options);
144
- const address = getAddress(config.privateKey);
183
+ if (!config) {
184
+ throw new CLIError(ErrorCode.WALLET_KEY_MISSING, 'WALLET_KEY env var required. Set it before running this command.');
185
+ }
145
186
 
146
187
  if (assetUpper === 'ETH') {
147
188
  progress('Checking balance...');
@@ -203,6 +244,7 @@ Examples:
203
244
  asset: assetUpper,
204
245
  amount,
205
246
  fee: feeStr,
247
+ dailyFreeUsed,
206
248
  totalSent: grossStr,
207
249
  blockNumber: result.receipt.blockNumber.toString(),
208
250
  };
@@ -212,11 +254,15 @@ Examples:
212
254
  return;
213
255
  }
214
256
 
257
+ const feeLabel = dailyFreeUsed
258
+ ? `0 ${assetUpper} (free — ${dailyFreeRemaining} remaining today)`
259
+ : `${feeStr} ${assetUpper} (0.3%)`;
260
+
215
261
  printHeader('Deposit Submitted');
216
262
  printFields([
217
263
  { label: 'Asset', value: assetUpper },
218
264
  { label: 'Amount', value: `${amount} ${assetUpper}` },
219
- { label: 'Fee', value: `${feeStr} ${assetUpper} (0.3%)` },
265
+ { label: 'Fee', value: feeLabel },
220
266
  { label: 'Total sent', value: `${grossStr} ${assetUpper}` },
221
267
  { label: 'From', value: address },
222
268
  { label: 'Transaction', value: txUrl(result.hash) },
@@ -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
@@ -53,6 +53,7 @@ export {
53
53
  export {
54
54
  getQueueBalance,
55
55
  getPrivateBalance,
56
+ getDailyFreeRemaining,
56
57
  } from './balance.js';
57
58
  export type { ProgressCallback } from './balance.js';
58
59
 
@@ -95,7 +96,7 @@ export {
95
96
  selectCircuit,
96
97
  CIRCUIT_CONFIG,
97
98
  } from './prover.js';
98
- export type { ProofInput } from './prover.js';
99
+ export type { ProofInput, ProveOptions, ProvingKeyPath } from './prover.js';
99
100
 
100
101
  // Addresses and config
101
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;