@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/README.md +446 -0
- package/dist/cli/index.cjs +6431 -0
- package/dist/index.cjs +1912 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2099 -0
- package/dist/index.d.ts +2099 -0
- package/dist/index.js +1840 -0
- package/dist/index.js.map +1 -0
- package/keys/transaction16.wasm +0 -0
- package/keys/transaction16.zkey +0 -0
- package/keys/transaction2.wasm +0 -0
- package/keys/transaction2.zkey +0 -0
- package/package.json +70 -0
- package/src/abi.ts +631 -0
- package/src/addresses.ts +53 -0
- package/src/balance.ts +266 -0
- package/src/cli/commands/balance.ts +118 -0
- package/src/cli/commands/deposit.ts +115 -0
- package/src/cli/commands/init.ts +147 -0
- package/src/cli/commands/keypair.ts +31 -0
- package/src/cli/commands/private-balance.ts +68 -0
- package/src/cli/commands/queue-balance.ts +58 -0
- package/src/cli/commands/register.ts +119 -0
- package/src/cli/commands/transfer.ts +137 -0
- package/src/cli/commands/withdraw.ts +79 -0
- package/src/cli/config.ts +58 -0
- package/src/cli/errors.ts +114 -0
- package/src/cli/index.ts +52 -0
- package/src/cli/wallet.ts +228 -0
- package/src/deposit.ts +183 -0
- package/src/index.ts +160 -0
- package/src/keypair.ts +170 -0
- package/src/merkle.ts +71 -0
- package/src/prover.ts +176 -0
- package/src/relay.ts +216 -0
- package/src/transaction.ts +260 -0
- package/src/transfer.ts +462 -0
- package/src/types.ts +306 -0
- package/src/utils.ts +151 -0
- package/src/utxo.ts +119 -0
- package/src/withdraw.ts +299 -0
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
|
+
}
|