@veil-cash/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/keypair.ts ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Veil Keypair class
3
+ * Generates and manages keypairs for Veil deposits
4
+ */
5
+
6
+ import { ethers } from 'ethers';
7
+ import { poseidonHash, toFixedHex } from './utils.js';
8
+ import type { EncryptedMessage } from './types.js';
9
+
10
+ // eth-sig-util for x25519 encryption
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const ethSigUtil = require('eth-sig-util');
13
+
14
+ /**
15
+ * Pack encrypted message into hex string
16
+ */
17
+ export function packEncryptedMessage(encryptedMessage: EncryptedMessage): string {
18
+ const nonceBuf = Buffer.from(encryptedMessage.nonce, 'base64');
19
+ const ephemPublicKeyBuf = Buffer.from(encryptedMessage.ephemPublicKey, 'base64');
20
+ const ciphertextBuf = Buffer.from(encryptedMessage.ciphertext, 'base64');
21
+ const messageBuff = Buffer.concat([
22
+ Buffer.alloc(24 - nonceBuf.length),
23
+ nonceBuf,
24
+ Buffer.alloc(32 - ephemPublicKeyBuf.length),
25
+ ephemPublicKeyBuf,
26
+ ciphertextBuf,
27
+ ]);
28
+ return '0x' + messageBuff.toString('hex');
29
+ }
30
+
31
+ /**
32
+ * Unpack hex string into encrypted message
33
+ */
34
+ export function unpackEncryptedMessage(encryptedMessage: string): EncryptedMessage {
35
+ if (encryptedMessage.slice(0, 2) === '0x') {
36
+ encryptedMessage = encryptedMessage.slice(2);
37
+ }
38
+ const messageBuff = Buffer.from(encryptedMessage, 'hex');
39
+ const nonceBuf = messageBuff.slice(0, 24);
40
+ const ephemPublicKeyBuf = messageBuff.slice(24, 56);
41
+ const ciphertextBuf = messageBuff.slice(56);
42
+ return {
43
+ version: 'x25519-xsalsa20-poly1305',
44
+ nonce: nonceBuf.toString('base64'),
45
+ ephemPublicKey: ephemPublicKeyBuf.toString('base64'),
46
+ ciphertext: ciphertextBuf.toString('base64'),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Veil Keypair for deposits
52
+ *
53
+ * A keypair consists of:
54
+ * - Private key: Random 32-byte Ethereum-style key
55
+ * - Public key: Poseidon hash of the private key
56
+ * - Encryption key: x25519 public key for encrypted outputs
57
+ *
58
+ * The deposit key (used for registration) is: pubkey + encryptionKey
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * // Generate new keypair
63
+ * const keypair = new Keypair();
64
+ * console.log(keypair.depositKey()); // Register this on-chain
65
+ * console.log(keypair.privkey); // Store securely!
66
+ *
67
+ * // Restore from existing private key
68
+ * const restored = new Keypair(savedPrivkey);
69
+ * ```
70
+ */
71
+ export class Keypair {
72
+ /** Private key (null if created from public deposit key only) */
73
+ public privkey: string | null;
74
+
75
+ /** Public key (Poseidon hash of private key) */
76
+ public pubkey: bigint;
77
+
78
+ /** x25519 encryption public key */
79
+ public encryptionKey: string;
80
+
81
+ /**
82
+ * Create a new Keypair
83
+ * @param privkey - Optional private key. If not provided, generates a random one.
84
+ */
85
+ constructor(privkey: string = ethers.Wallet.createRandom().privateKey) {
86
+ this.privkey = privkey;
87
+ this.pubkey = poseidonHash([this.privkey]);
88
+ this.encryptionKey = ethSigUtil.getEncryptionPublicKey(privkey.slice(2));
89
+ }
90
+
91
+ /**
92
+ * Get the deposit key for this keypair
93
+ * This is what you register on-chain
94
+ * @returns Deposit key as hex string (130 chars with 0x prefix)
95
+ */
96
+ toString(): string {
97
+ return toFixedHex(this.pubkey) + Buffer.from(this.encryptionKey, 'base64').toString('hex');
98
+ }
99
+
100
+ /**
101
+ * Alias for toString() - returns the deposit key
102
+ * @returns Deposit key as hex string
103
+ */
104
+ depositKey(): string {
105
+ return this.toString();
106
+ }
107
+
108
+ /**
109
+ * Create a Keypair from a public deposit key (without private key)
110
+ * Useful for sending transfers to other users
111
+ * @param str - Deposit key (128 or 130 hex chars)
112
+ * @returns Keypair instance (privkey will be null)
113
+ */
114
+ static fromString(str: string): Keypair {
115
+ if (str.length === 130) {
116
+ str = str.slice(2);
117
+ }
118
+ if (str.length !== 128) {
119
+ throw new Error('Invalid deposit key length. Expected 128 hex chars (or 130 with 0x prefix)');
120
+ }
121
+ return Object.assign(new Keypair(), {
122
+ privkey: null,
123
+ pubkey: BigInt('0x' + str.slice(0, 64)),
124
+ encryptionKey: Buffer.from(str.slice(64, 128), 'hex').toString('base64'),
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Sign a message using the private key
130
+ * @param commitment - Commitment hash
131
+ * @param merklePath - Merkle path
132
+ * @returns Signature as bigint
133
+ */
134
+ sign(commitment: string | number | bigint, merklePath: string | number | bigint): bigint {
135
+ if (!this.privkey) {
136
+ throw new Error('Cannot sign without private key');
137
+ }
138
+ return poseidonHash([this.privkey, commitment, merklePath]);
139
+ }
140
+
141
+ /**
142
+ * Encrypt data using the encryption key
143
+ * @param bytes - Data to encrypt
144
+ * @returns Encrypted data as hex string
145
+ */
146
+ encrypt(bytes: Buffer): string {
147
+ return packEncryptedMessage(
148
+ ethSigUtil.encrypt(
149
+ this.encryptionKey,
150
+ { data: bytes.toString('base64') },
151
+ 'x25519-xsalsa20-poly1305'
152
+ )
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Decrypt data using the private key
158
+ * @param data - Encrypted data as hex string
159
+ * @returns Decrypted data as Buffer
160
+ */
161
+ decrypt(data: string): Buffer {
162
+ if (!this.privkey) {
163
+ throw new Error('Cannot decrypt without private key');
164
+ }
165
+ return Buffer.from(
166
+ ethSigUtil.decrypt(unpackEncryptedMessage(data), this.privkey.slice(2)),
167
+ 'base64'
168
+ );
169
+ }
170
+ }
package/src/merkle.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Merkle tree utilities for Veil SDK
3
+ * Build merkle trees from UTXO commitments for ZK proofs
4
+ */
5
+
6
+ // @ts-ignore - fixed-merkle-tree doesn't have TypeScript declarations
7
+ import MerkleTree from 'fixed-merkle-tree-legacy';
8
+ import { poseidonHash2, toFixedHex } from './utils.js';
9
+
10
+ /**
11
+ * Height of the merkle tree (matches on-chain contract)
12
+ */
13
+ export const MERKLE_TREE_HEIGHT = 23;
14
+
15
+ /**
16
+ * Build a merkle tree from UTXO commitments
17
+ * Uses Poseidon hash function compatible with on-chain verification
18
+ *
19
+ * @param commitments - Array of commitment hashes (hex strings or bigints)
20
+ * @returns MerkleTree instance
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const commitments = await poolContract.getCommitments(0, 1000);
25
+ * const tree = await buildMerkleTree(commitments);
26
+ * const root = tree.root();
27
+ * ```
28
+ */
29
+ export async function buildMerkleTree(commitments: (string | bigint)[]): Promise<MerkleTree> {
30
+ // Convert commitments to fixed hex format
31
+ const leaves = commitments.map((commitment) => toFixedHex(commitment));
32
+
33
+ // Create hash function that uses Poseidon (matching on-chain)
34
+ const hashFunction = (left: string | bigint, right: string | bigint): string => {
35
+ const result = poseidonHash2(left, right);
36
+ return result.toString();
37
+ };
38
+
39
+ const tree = new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction });
40
+
41
+ return tree;
42
+ }
43
+
44
+ /**
45
+ * Get merkle path for a commitment
46
+ *
47
+ * @param tree - Merkle tree instance
48
+ * @param commitment - Commitment to get path for
49
+ * @returns Path elements and indices
50
+ */
51
+ export function getMerklePath(tree: MerkleTree, commitment: bigint | string): {
52
+ pathElements: bigint[];
53
+ pathIndices: number;
54
+ } {
55
+ const commitmentHex = toFixedHex(commitment);
56
+ const index = tree.indexOf(commitmentHex);
57
+
58
+ if (index < 0) {
59
+ throw new Error(`Commitment ${commitmentHex} not found in merkle tree`);
60
+ }
61
+
62
+ const { pathElements } = tree.path(index);
63
+
64
+ return {
65
+ pathElements: pathElements.map((el: string | number | bigint) => BigInt(el)),
66
+ pathIndices: index,
67
+ };
68
+ }
69
+
70
+ // Re-export MerkleTree type for consumers
71
+ export type { MerkleTree };
package/src/prover.ts ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * ZK Proof generation for Veil SDK
3
+ * Uses snarkjs groth16 to generate proofs for transactions
4
+ */
5
+
6
+ import { groth16 } from 'snarkjs';
7
+ import { toFixedHex } from './utils.js';
8
+ import * as path from 'path';
9
+ import * as fs from 'fs';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ // Type definition for ffjavascript utils
13
+ interface FFJavascriptUtils {
14
+ stringifyBigInts: (obj: unknown) => unknown;
15
+ unstringifyBigInts: (obj: unknown) => unknown;
16
+ }
17
+
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
+ }
27
+
28
+ /**
29
+ * Input data for ZK proof generation
30
+ */
31
+ export interface ProofInput {
32
+ root: bigint;
33
+ inputNullifier: bigint[];
34
+ outputCommitment: bigint[];
35
+ publicAmount: string;
36
+ extDataHash: bigint;
37
+
38
+ // Input UTXO data
39
+ inAmount: bigint[];
40
+ inPrivateKey: (string | null)[];
41
+ inBlinding: bigint[];
42
+ inPathIndices: number[];
43
+ inPathElements: (bigint | number)[][];
44
+
45
+ // Output UTXO data
46
+ outAmount: bigint[];
47
+ outBlinding: bigint[];
48
+ outPubkey: bigint[];
49
+ }
50
+
51
+ /**
52
+ * Raw snarkjs proof structure
53
+ */
54
+ interface SnarkProof {
55
+ pi_a: [string, string];
56
+ pi_b: [[string, string], [string, string]];
57
+ pi_c: [string, string];
58
+ }
59
+
60
+ interface ProveResult {
61
+ proof: SnarkProof;
62
+ publicSignals: string[];
63
+ }
64
+
65
+ /**
66
+ * Find the keys directory containing circuit files
67
+ * Works in both development and installed package scenarios
68
+ */
69
+ function findKeysDirectory(): string {
70
+ // Try multiple possible locations
71
+ const possiblePaths = [
72
+ // When running from package (installed via npm)
73
+ path.resolve(__dirname, '..', 'keys'),
74
+ path.resolve(__dirname, '..', '..', 'keys'),
75
+ // When running from source
76
+ path.resolve(process.cwd(), 'keys'),
77
+ // ESM module path
78
+ ];
79
+
80
+ // Try to get module directory for ESM
81
+ try {
82
+ const currentFilePath = fileURLToPath(import.meta.url);
83
+ const currentDir = path.dirname(currentFilePath);
84
+ possiblePaths.unshift(path.resolve(currentDir, '..', 'keys'));
85
+ } catch {
86
+ // Not ESM environment, use __dirname
87
+ }
88
+
89
+ for (const p of possiblePaths) {
90
+ if (fs.existsSync(p) && fs.existsSync(path.join(p, 'transaction2.wasm'))) {
91
+ return p;
92
+ }
93
+ }
94
+
95
+ throw new Error(
96
+ 'Circuit keys not found. Expected to find keys/ directory with transaction2.wasm and transaction2.zkey files.'
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Generate a ZK proof for a transaction
102
+ *
103
+ * @param input - Proof input data
104
+ * @param circuitName - Circuit name (e.g., 'transaction2' or 'transaction16')
105
+ * @returns Serialized proof as hex string
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const proof = await prove(proofInput, 'transaction2');
110
+ * // Returns: 0x1234...abcd (256 bytes hex)
111
+ * ```
112
+ */
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
+ }
129
+
130
+ // Generate proof using snarkjs
131
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
132
+ const result = await groth16.fullProve(
133
+ utils.stringifyBigInts(input) as any,
134
+ wasmPath,
135
+ zkeyPath,
136
+ );
137
+ const proof = result.proof as unknown as SnarkProof;
138
+
139
+ // Serialize proof to hex string format expected by on-chain verifier
140
+ // Format: pi_a[0] + pi_a[1] + pi_b[0][1] + pi_b[0][0] + pi_b[1][1] + pi_b[1][0] + pi_c[0] + pi_c[1]
141
+ return (
142
+ '0x' +
143
+ toFixedHex(proof.pi_a[0]).slice(2) +
144
+ toFixedHex(proof.pi_a[1]).slice(2) +
145
+ toFixedHex(proof.pi_b[0][1]).slice(2) +
146
+ toFixedHex(proof.pi_b[0][0]).slice(2) +
147
+ toFixedHex(proof.pi_b[1][1]).slice(2) +
148
+ toFixedHex(proof.pi_b[1][0]).slice(2) +
149
+ toFixedHex(proof.pi_c[0]).slice(2) +
150
+ toFixedHex(proof.pi_c[1]).slice(2)
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Get the supported circuit names and their max input counts
156
+ */
157
+ export const CIRCUIT_CONFIG = {
158
+ transaction2: { maxInputs: 2, maxOutputs: 2 },
159
+ transaction16: { maxInputs: 16, maxOutputs: 2 },
160
+ } as const;
161
+
162
+ /**
163
+ * Select the appropriate circuit based on input count
164
+ *
165
+ * @param inputCount - Number of input UTXOs
166
+ * @returns Circuit name to use
167
+ */
168
+ export function selectCircuit(inputCount: number): string {
169
+ if (inputCount <= 2) {
170
+ return 'transaction2';
171
+ } else if (inputCount <= 16) {
172
+ return 'transaction16';
173
+ } else {
174
+ throw new Error(`Too many inputs: ${inputCount}. Maximum supported is 16.`);
175
+ }
176
+ }
package/src/relay.ts ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Relay functions for submitting withdrawals and transfers
3
+ *
4
+ * The relay service handles transaction submission for privacy-preserving
5
+ * withdrawals and transfers from Veil pools.
6
+ *
7
+ * Note: Public API is rate limited to 5 requests per minute per IP.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { submitRelay } from '@veil-cash/sdk';
12
+ *
13
+ * const result = await submitRelay({
14
+ * type: 'withdraw',
15
+ * pool: 'eth',
16
+ * proofArgs: { ... },
17
+ * extData: { ... },
18
+ * metadata: { amount: '0.1' }
19
+ * });
20
+ *
21
+ * console.log(result.transactionHash);
22
+ * ```
23
+ */
24
+
25
+ import { getRelayUrl } from './addresses.js';
26
+ import type {
27
+ RelayPool,
28
+ RelayResponse,
29
+ RelayErrorResponse,
30
+ SubmitRelayOptions,
31
+ } from './types.js';
32
+
33
+ /**
34
+ * Error thrown when relay request fails
35
+ */
36
+ export class RelayError extends Error {
37
+ /** HTTP status code */
38
+ statusCode: number;
39
+ /** Seconds until rate limit resets (only for 429 errors) */
40
+ retryAfter?: number;
41
+ /** Network the error occurred on */
42
+ network?: string;
43
+
44
+ constructor(message: string, statusCode: number, retryAfter?: number, network?: string) {
45
+ super(message);
46
+ this.name = 'RelayError';
47
+ this.statusCode = statusCode;
48
+ this.retryAfter = retryAfter;
49
+ this.network = network;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Submit a withdrawal or transfer to the relay service
55
+ *
56
+ * The relay service submits the transaction on behalf of the user,
57
+ * allowing for privacy-preserving withdrawals and transfers.
58
+ *
59
+ * Rate limit: 5 requests per minute per IP (public API)
60
+ *
61
+ * @param options - Relay options including type, pool, proofArgs, extData
62
+ * @returns Promise resolving to relay response with transaction hash
63
+ * @throws RelayError if the request fails
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * // Withdraw ETH
68
+ * const result = await submitRelay({
69
+ * type: 'withdraw',
70
+ * pool: 'eth',
71
+ * proofArgs: proofData.args,
72
+ * extData: proofData.extData,
73
+ * metadata: { amount: '0.1', recipient: '0x...' }
74
+ * });
75
+ *
76
+ * // Transfer USDC
77
+ * const result = await submitRelay({
78
+ * type: 'transfer',
79
+ * pool: 'usdc',
80
+ * proofArgs: proofData.args,
81
+ * extData: proofData.extData,
82
+ * metadata: { amount: '100' }
83
+ * });
84
+ * ```
85
+ */
86
+ export async function submitRelay(options: SubmitRelayOptions): Promise<RelayResponse> {
87
+ const {
88
+ type,
89
+ pool = 'eth',
90
+ proofArgs,
91
+ extData,
92
+ metadata,
93
+ relayUrl: customRelayUrl,
94
+ } = options;
95
+
96
+ // Validate inputs
97
+ if (type !== 'withdraw' && type !== 'transfer') {
98
+ throw new RelayError('Invalid type. Must be "withdraw" or "transfer"', 400);
99
+ }
100
+
101
+ if (pool !== 'eth' && pool !== 'usdc') {
102
+ throw new RelayError('Invalid pool. Must be "eth" or "usdc"', 400);
103
+ }
104
+
105
+ if (!proofArgs || !extData) {
106
+ throw new RelayError('Missing proofArgs or extData', 400);
107
+ }
108
+
109
+ const relayUrl = customRelayUrl || getRelayUrl();
110
+ const endpoint = `${relayUrl}/relay/${pool}`;
111
+
112
+ const response = await fetch(endpoint, {
113
+ method: 'POST',
114
+ headers: {
115
+ 'Content-Type': 'application/json',
116
+ },
117
+ body: JSON.stringify({
118
+ type,
119
+ proofArgs,
120
+ extData,
121
+ metadata,
122
+ }),
123
+ });
124
+
125
+ const data = await response.json();
126
+
127
+ if (!response.ok) {
128
+ const errorData = data as RelayErrorResponse;
129
+ throw new RelayError(
130
+ errorData.error || errorData.message || 'Relay request failed',
131
+ response.status,
132
+ errorData.retryAfter,
133
+ errorData.network
134
+ );
135
+ }
136
+
137
+ return data as RelayResponse;
138
+ }
139
+
140
+ /**
141
+ * Check if relay service is healthy
142
+ *
143
+ * @param relayUrl - Optional custom relay URL
144
+ * @returns Promise resolving to health status
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const health = await checkRelayHealth();
149
+ * console.log(health.status); // 'ok'
150
+ * console.log(health.network); // 'base'
151
+ * ```
152
+ */
153
+ export async function checkRelayHealth(relayUrl?: string): Promise<{
154
+ status: string;
155
+ service: string;
156
+ network: string;
157
+ timestamp: string;
158
+ }> {
159
+ const url = relayUrl || getRelayUrl();
160
+ const response = await fetch(`${url}/health`);
161
+
162
+ if (!response.ok) {
163
+ throw new RelayError('Relay service health check failed', response.status);
164
+ }
165
+
166
+ return response.json() as Promise<{
167
+ status: string;
168
+ service: string;
169
+ network: string;
170
+ timestamp: string;
171
+ }>;
172
+ }
173
+
174
+ /**
175
+ * Get relay service info
176
+ *
177
+ * @param relayUrl - Optional custom relay URL
178
+ * @returns Promise resolving to service info including rate limit config
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const info = await getRelayInfo();
183
+ * console.log(info.rateLimit.limit); // 5
184
+ * console.log(info.rateLimit.windowMs); // 60000
185
+ * ```
186
+ */
187
+ export async function getRelayInfo(relayUrl?: string): Promise<{
188
+ service: string;
189
+ version: string;
190
+ network: string;
191
+ endpoints: Record<string, string>;
192
+ rateLimit: {
193
+ limit: number;
194
+ windowMs: number;
195
+ note: string;
196
+ };
197
+ }> {
198
+ const url = relayUrl || getRelayUrl();
199
+ const response = await fetch(url);
200
+
201
+ if (!response.ok) {
202
+ throw new RelayError('Failed to get relay service info', response.status);
203
+ }
204
+
205
+ return response.json() as Promise<{
206
+ service: string;
207
+ version: string;
208
+ network: string;
209
+ endpoints: Record<string, string>;
210
+ rateLimit: {
211
+ limit: number;
212
+ windowMs: number;
213
+ note: string;
214
+ };
215
+ }>;
216
+ }