@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/README.md +8 -6
- package/SDK.md +63 -5
- package/dist/cli/index.cjs +372 -283
- package/dist/index.cjs +141 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +139 -64
- package/dist/index.d.ts +139 -64
- package/dist/index.js +118 -49
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/skills/veil/SKILL.md +6 -4
- package/skills/veil/reference.md +5 -5
- package/src/abi.ts +8 -0
- package/src/balance.ts +48 -0
- package/src/cli/commands/deposit.ts +61 -15
- package/src/ffjavascript.d.ts +28 -0
- package/src/index.ts +2 -1
- package/src/keypair.ts +2 -4
- package/src/prover.ts +108 -36
- package/src/subaccount.ts +2 -0
- package/src/transaction.ts +8 -2
- package/src/transfer.ts +5 -1
- package/src/types.ts +21 -0
- package/src/utils.ts +12 -5
- package/src/utxo.ts +1 -0
- package/src/withdraw.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veil-cash/sdk",
|
|
3
|
-
"version": "0.6.
|
|
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",
|
package/skills/veil/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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:
|
package/skills/veil/reference.md
CHANGED
|
@@ -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
|
|
262
|
-
veil deposit USDC 100 --unsigned
|
|
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
|
|
311
|
-
| USDC | 10 | Fee (0.3%) added automatically
|
|
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
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
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 (
|
|
60
|
-
veil deposit USDC 100 # deposits 100 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
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
127
|
+
// Ignore non-file module URLs.
|
|
87
128
|
}
|
|
88
129
|
|
|
89
130
|
for (const p of possiblePaths) {
|
|
90
|
-
if (
|
|
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(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
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
|
-
|
|
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
|
package/src/transaction.ts
CHANGED
|
@@ -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;
|