@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/balance.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balance functions for Veil SDK
|
|
3
|
+
* Query queue and private balances directly from blockchain
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createPublicClient, http, formatEther } from 'viem';
|
|
7
|
+
import { base } from 'viem/chains';
|
|
8
|
+
import { getAddresses } from './addresses.js';
|
|
9
|
+
import { QUEUE_ABI, POOL_ABI } from './abi.js';
|
|
10
|
+
import { Keypair } from './keypair.js';
|
|
11
|
+
import { Utxo } from './utxo.js';
|
|
12
|
+
import { toFixedHex } from './utils.js';
|
|
13
|
+
import type {
|
|
14
|
+
QueueBalanceResult,
|
|
15
|
+
PendingDeposit,
|
|
16
|
+
PrivateBalanceResult,
|
|
17
|
+
UtxoInfo,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
|
|
20
|
+
// Deposit status enum from Queue contract
|
|
21
|
+
const DEPOSIT_STATUS_MAP = {
|
|
22
|
+
0: 'pending',
|
|
23
|
+
1: 'accepted',
|
|
24
|
+
2: 'rejected',
|
|
25
|
+
3: 'refunded',
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Progress callback type
|
|
30
|
+
*/
|
|
31
|
+
export type ProgressCallback = (stage: string, detail?: string) => void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get queue balance and pending deposits for an address
|
|
35
|
+
* Queries the Queue contract directly (no API dependency)
|
|
36
|
+
*
|
|
37
|
+
* @param options - Query options
|
|
38
|
+
* @param options.address - Address to check
|
|
39
|
+
* @param options.rpcUrl - Optional RPC URL (uses default if not provided)
|
|
40
|
+
* @param options.onProgress - Optional progress callback
|
|
41
|
+
* @returns Queue balance and pending deposits
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const result = await getQueueBalance({
|
|
46
|
+
* address: '0x...',
|
|
47
|
+
* onProgress: (stage, detail) => console.log(stage, detail),
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* console.log(`Queue balance: ${result.queueBalance} ETH`);
|
|
51
|
+
* console.log(`Pending deposits: ${result.pendingCount}`);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export async function getQueueBalance(options: {
|
|
55
|
+
address: `0x${string}`;
|
|
56
|
+
rpcUrl?: string;
|
|
57
|
+
onProgress?: ProgressCallback;
|
|
58
|
+
}): Promise<QueueBalanceResult> {
|
|
59
|
+
const { address, rpcUrl, onProgress } = options;
|
|
60
|
+
const addresses = getAddresses();
|
|
61
|
+
|
|
62
|
+
// Create public client
|
|
63
|
+
const publicClient = createPublicClient({
|
|
64
|
+
chain: base,
|
|
65
|
+
transport: http(rpcUrl),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Get all pending deposit nonces
|
|
69
|
+
onProgress?.('Fetching pending deposits...');
|
|
70
|
+
const pendingNonces = await publicClient.readContract({
|
|
71
|
+
address: addresses.ethQueue,
|
|
72
|
+
abi: QUEUE_ABI,
|
|
73
|
+
functionName: 'getPendingDeposits',
|
|
74
|
+
}) as bigint[];
|
|
75
|
+
onProgress?.('Queue status', `${pendingNonces.length} pending deposits in queue`);
|
|
76
|
+
|
|
77
|
+
// Fetch deposit details for each pending nonce
|
|
78
|
+
const pendingDeposits: PendingDeposit[] = [];
|
|
79
|
+
let totalQueueBalance = 0n;
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < pendingNonces.length; i++) {
|
|
82
|
+
const nonce = pendingNonces[i];
|
|
83
|
+
onProgress?.('Checking deposit', `${i + 1}/${pendingNonces.length}`);
|
|
84
|
+
|
|
85
|
+
const deposit = await publicClient.readContract({
|
|
86
|
+
address: addresses.ethQueue,
|
|
87
|
+
abi: QUEUE_ABI,
|
|
88
|
+
functionName: 'getDeposit',
|
|
89
|
+
args: [nonce],
|
|
90
|
+
}) as {
|
|
91
|
+
fallbackReceiver: `0x${string}`;
|
|
92
|
+
amountIn: bigint;
|
|
93
|
+
fee: bigint;
|
|
94
|
+
shieldAmount: bigint;
|
|
95
|
+
timestamp: bigint;
|
|
96
|
+
status: number;
|
|
97
|
+
depositKey: `0x${string}`;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Check if this deposit belongs to the user
|
|
101
|
+
if (deposit.fallbackReceiver.toLowerCase() === address.toLowerCase()) {
|
|
102
|
+
totalQueueBalance += deposit.amountIn;
|
|
103
|
+
pendingDeposits.push({
|
|
104
|
+
nonce: nonce.toString(),
|
|
105
|
+
status: DEPOSIT_STATUS_MAP[deposit.status as keyof typeof DEPOSIT_STATUS_MAP] || 'pending',
|
|
106
|
+
amount: formatEther(deposit.amountIn),
|
|
107
|
+
amountWei: deposit.amountIn.toString(),
|
|
108
|
+
timestamp: new Date(Number(deposit.timestamp) * 1000).toISOString(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (pendingDeposits.length > 0) {
|
|
114
|
+
onProgress?.('Found', `${pendingDeposits.length} deposits for your address`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
address,
|
|
119
|
+
queueBalance: formatEther(totalQueueBalance),
|
|
120
|
+
queueBalanceWei: totalQueueBalance.toString(),
|
|
121
|
+
pendingDeposits,
|
|
122
|
+
pendingCount: pendingDeposits.length,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get private balance from the Pool contract
|
|
128
|
+
* Decrypts all encrypted outputs, calculates nullifiers, and checks spent status
|
|
129
|
+
*
|
|
130
|
+
* @param options - Query options
|
|
131
|
+
* @param options.keypair - Keypair to decrypt UTXOs with
|
|
132
|
+
* @param options.rpcUrl - Optional RPC URL (uses default if not provided)
|
|
133
|
+
* @param options.onProgress - Optional progress callback
|
|
134
|
+
* @returns Private balance and UTXO details
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const keypair = new Keypair(process.env.VEIL_KEY);
|
|
139
|
+
* const result = await getPrivateBalance({
|
|
140
|
+
* keypair,
|
|
141
|
+
* onProgress: (stage, detail) => console.log(stage, detail),
|
|
142
|
+
* });
|
|
143
|
+
*
|
|
144
|
+
* console.log(`Private balance: ${result.privateBalance} ETH`);
|
|
145
|
+
* console.log(`Unspent UTXOs: ${result.unspentCount}`);
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export async function getPrivateBalance(options: {
|
|
149
|
+
keypair: Keypair;
|
|
150
|
+
rpcUrl?: string;
|
|
151
|
+
onProgress?: ProgressCallback;
|
|
152
|
+
}): Promise<PrivateBalanceResult> {
|
|
153
|
+
const { keypair, rpcUrl, onProgress } = options;
|
|
154
|
+
const addresses = getAddresses();
|
|
155
|
+
|
|
156
|
+
if (!keypair.privkey) {
|
|
157
|
+
throw new Error('Keypair must have a private key to calculate private balance');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create public client
|
|
161
|
+
const publicClient = createPublicClient({
|
|
162
|
+
chain: base,
|
|
163
|
+
transport: http(rpcUrl),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 1. Get total count of encrypted outputs
|
|
167
|
+
onProgress?.('Fetching pool index...');
|
|
168
|
+
const nextIndex = await publicClient.readContract({
|
|
169
|
+
address: addresses.ethPool,
|
|
170
|
+
abi: POOL_ABI,
|
|
171
|
+
functionName: 'nextIndex',
|
|
172
|
+
}) as number;
|
|
173
|
+
onProgress?.('Pool index', `${nextIndex} commitments`);
|
|
174
|
+
|
|
175
|
+
if (nextIndex === 0) {
|
|
176
|
+
return {
|
|
177
|
+
privateBalance: '0',
|
|
178
|
+
privateBalanceWei: '0',
|
|
179
|
+
utxoCount: 0,
|
|
180
|
+
spentCount: 0,
|
|
181
|
+
unspentCount: 0,
|
|
182
|
+
utxos: [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 2. Fetch encrypted outputs in batches of 5000
|
|
187
|
+
const BATCH_SIZE = 5000;
|
|
188
|
+
const allEncryptedOutputs: string[] = [];
|
|
189
|
+
const totalBatches = Math.ceil(nextIndex / BATCH_SIZE);
|
|
190
|
+
|
|
191
|
+
for (let start = 0; start < nextIndex; start += BATCH_SIZE) {
|
|
192
|
+
const end = Math.min(start + BATCH_SIZE, nextIndex);
|
|
193
|
+
const batchNum = Math.floor(start / BATCH_SIZE) + 1;
|
|
194
|
+
onProgress?.('Fetching encrypted outputs', `batch ${batchNum}/${totalBatches} (${start}-${end})`);
|
|
195
|
+
|
|
196
|
+
const batch = await publicClient.readContract({
|
|
197
|
+
address: addresses.ethPool,
|
|
198
|
+
abi: POOL_ABI,
|
|
199
|
+
functionName: 'getEncryptedOutputs',
|
|
200
|
+
args: [BigInt(start), BigInt(end)],
|
|
201
|
+
}) as string[];
|
|
202
|
+
allEncryptedOutputs.push(...batch);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 3. Decrypt outputs - only user's UTXOs will decrypt successfully
|
|
206
|
+
onProgress?.('Decrypting outputs', `scanning ${allEncryptedOutputs.length} outputs...`);
|
|
207
|
+
const decryptedUtxos: { utxo: Utxo; index: number }[] = [];
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < allEncryptedOutputs.length; i++) {
|
|
210
|
+
try {
|
|
211
|
+
const utxo = Utxo.decrypt(allEncryptedOutputs[i], keypair);
|
|
212
|
+
utxo.index = i;
|
|
213
|
+
// Only include UTXOs with non-zero amounts
|
|
214
|
+
if (utxo.amount > 0n) {
|
|
215
|
+
decryptedUtxos.push({ utxo, index: i });
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Not our UTXO - decryption fails, skip
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
onProgress?.('Found UTXOs', `${decryptedUtxos.length} belonging to you`);
|
|
222
|
+
|
|
223
|
+
// 4. Check spent status for each UTXO
|
|
224
|
+
const utxoInfos: UtxoInfo[] = [];
|
|
225
|
+
let totalBalance = 0n;
|
|
226
|
+
let spentCount = 0;
|
|
227
|
+
let unspentCount = 0;
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < decryptedUtxos.length; i++) {
|
|
230
|
+
const { utxo, index } = decryptedUtxos[i];
|
|
231
|
+
onProgress?.('Checking spent status', `UTXO ${i + 1}/${decryptedUtxos.length}`);
|
|
232
|
+
|
|
233
|
+
const nullifier = utxo.getNullifier();
|
|
234
|
+
const nullifierHex = toFixedHex(nullifier) as `0x${string}`;
|
|
235
|
+
|
|
236
|
+
const isSpent = await publicClient.readContract({
|
|
237
|
+
address: addresses.ethPool,
|
|
238
|
+
abi: POOL_ABI,
|
|
239
|
+
functionName: 'isSpent',
|
|
240
|
+
args: [nullifierHex],
|
|
241
|
+
}) as boolean;
|
|
242
|
+
|
|
243
|
+
utxoInfos.push({
|
|
244
|
+
index,
|
|
245
|
+
amount: formatEther(utxo.amount),
|
|
246
|
+
amountWei: utxo.amount.toString(),
|
|
247
|
+
isSpent,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (isSpent) {
|
|
251
|
+
spentCount++;
|
|
252
|
+
} else {
|
|
253
|
+
unspentCount++;
|
|
254
|
+
totalBalance += utxo.amount;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
privateBalance: formatEther(totalBalance),
|
|
260
|
+
privateBalanceWei: totalBalance.toString(),
|
|
261
|
+
utxoCount: decryptedUtxos.length,
|
|
262
|
+
spentCount,
|
|
263
|
+
unspentCount,
|
|
264
|
+
utxos: utxoInfos,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balance CLI command - Show both queue and private balances
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { getQueueBalance, getPrivateBalance } from '../../balance.js';
|
|
7
|
+
import { Keypair } from '../../keypair.js';
|
|
8
|
+
import { getAddress } from '../wallet.js';
|
|
9
|
+
import { formatEther } from 'viem';
|
|
10
|
+
import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
|
|
11
|
+
|
|
12
|
+
export function createBalanceCommand(): Command {
|
|
13
|
+
const balance = new Command('balance')
|
|
14
|
+
.description('Show queue and private balances')
|
|
15
|
+
.option('--wallet-key <key>', 'Ethereum wallet key (or set WALLET_KEY env)')
|
|
16
|
+
.option('--address <address>', 'Address to check (or derived from wallet key)')
|
|
17
|
+
.option('--veil-key <key>', 'Veil private key (or set VEIL_KEY env)')
|
|
18
|
+
.option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
|
|
19
|
+
.option('--quiet', 'Suppress progress output')
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
try {
|
|
22
|
+
// Get address
|
|
23
|
+
let address: `0x${string}`;
|
|
24
|
+
if (options.address) {
|
|
25
|
+
address = options.address as `0x${string}`;
|
|
26
|
+
} else {
|
|
27
|
+
const walletKey = options.walletKey || process.env.WALLET_KEY;
|
|
28
|
+
if (!walletKey) {
|
|
29
|
+
throw new CLIError(ErrorCode.WALLET_KEY_MISSING, 'Must provide --address or --wallet-key (or set WALLET_KEY env)');
|
|
30
|
+
}
|
|
31
|
+
address = getAddress(walletKey as `0x${string}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get keypair for private balance
|
|
35
|
+
const veilKey = options.veilKey || process.env.VEIL_KEY;
|
|
36
|
+
const keypair = veilKey ? new Keypair(veilKey) : null;
|
|
37
|
+
|
|
38
|
+
const rpcUrl = options.rpcUrl || process.env.RPC_URL;
|
|
39
|
+
|
|
40
|
+
// Progress callback
|
|
41
|
+
const onProgress = options.quiet
|
|
42
|
+
? undefined
|
|
43
|
+
: (stage: string, detail?: string) => {
|
|
44
|
+
const msg = detail ? `${stage}: ${detail}` : stage;
|
|
45
|
+
process.stderr.write(`\r\x1b[K${msg}`);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Get queue balance
|
|
49
|
+
const queueResult = await getQueueBalance({ address, rpcUrl, onProgress });
|
|
50
|
+
|
|
51
|
+
// Get private balance if keypair available
|
|
52
|
+
let privateResult = null;
|
|
53
|
+
if (keypair) {
|
|
54
|
+
privateResult = await getPrivateBalance({ keypair, rpcUrl, onProgress });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Clear progress line
|
|
58
|
+
if (!options.quiet) {
|
|
59
|
+
process.stderr.write('\r\x1b[K');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Calculate total balance
|
|
63
|
+
const queueBalanceWei = BigInt(queueResult.queueBalanceWei);
|
|
64
|
+
const privateBalanceWei = privateResult ? BigInt(privateResult.privateBalanceWei) : 0n;
|
|
65
|
+
const totalBalanceWei = queueBalanceWei + privateBalanceWei;
|
|
66
|
+
|
|
67
|
+
// Get deposit key if available
|
|
68
|
+
const depositKey = process.env.DEPOSIT_KEY || (keypair ? keypair.depositKey() : null);
|
|
69
|
+
|
|
70
|
+
// Build output structure
|
|
71
|
+
const output: Record<string, unknown> = {
|
|
72
|
+
address,
|
|
73
|
+
depositKey: depositKey || null,
|
|
74
|
+
totalBalance: formatEther(totalBalanceWei),
|
|
75
|
+
totalBalanceWei: totalBalanceWei.toString(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Private balance first
|
|
79
|
+
if (privateResult) {
|
|
80
|
+
const unspentUtxos = privateResult.utxos.filter(u => !u.isSpent);
|
|
81
|
+
output.private = {
|
|
82
|
+
balance: privateResult.privateBalance,
|
|
83
|
+
balanceWei: privateResult.privateBalanceWei,
|
|
84
|
+
utxoCount: privateResult.unspentCount,
|
|
85
|
+
utxos: unspentUtxos.map(u => ({
|
|
86
|
+
index: u.index,
|
|
87
|
+
amount: u.amount,
|
|
88
|
+
})),
|
|
89
|
+
};
|
|
90
|
+
} else {
|
|
91
|
+
output.private = {
|
|
92
|
+
balance: null,
|
|
93
|
+
note: 'Set VEIL_KEY to see private balance',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Queue details second
|
|
98
|
+
output.queue = {
|
|
99
|
+
balance: queueResult.queueBalance,
|
|
100
|
+
balanceWei: queueResult.queueBalanceWei,
|
|
101
|
+
count: queueResult.pendingCount,
|
|
102
|
+
deposits: queueResult.pendingDeposits.map(d => ({
|
|
103
|
+
nonce: d.nonce,
|
|
104
|
+
amount: d.amount,
|
|
105
|
+
status: d.status,
|
|
106
|
+
timestamp: d.timestamp,
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
console.log(JSON.stringify(output, null, 2));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
process.stderr.write('\r\x1b[K');
|
|
113
|
+
handleCLIError(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return balance;
|
|
118
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deposit CLI command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { buildDepositETHTx } from '../../deposit.js';
|
|
7
|
+
import { sendTransaction, getAddress, getBalance } from '../wallet.js';
|
|
8
|
+
import { getConfig } from '../config.js';
|
|
9
|
+
import { parseEther, formatEther } from 'viem';
|
|
10
|
+
import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
|
|
11
|
+
|
|
12
|
+
// Minimum deposit: 0.01 ETH (net after 0.3% fee)
|
|
13
|
+
// To deposit 0.01 ETH net, you need to send: 0.01 / (1 - 0.003) ≈ 0.01003 ETH
|
|
14
|
+
const MINIMUM_DEPOSIT_ETH = 0.01;
|
|
15
|
+
const DEPOSIT_FEE_PERCENT = 0.3;
|
|
16
|
+
const MINIMUM_DEPOSIT_WITH_FEE = MINIMUM_DEPOSIT_ETH / (1 - DEPOSIT_FEE_PERCENT / 100);
|
|
17
|
+
|
|
18
|
+
// Progress helper - writes to stderr so JSON output stays clean
|
|
19
|
+
function progress(msg: string, quiet?: boolean) {
|
|
20
|
+
if (!quiet) {
|
|
21
|
+
process.stderr.write(`\r\x1b[K${msg}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createDepositCommand(): Command {
|
|
26
|
+
const deposit = new Command('deposit')
|
|
27
|
+
.description('Deposit ETH into Veil')
|
|
28
|
+
.argument('<amount>', 'Amount to deposit (e.g., 0.1)')
|
|
29
|
+
.option('--deposit-key <key>', 'Your Veil deposit key (or set DEPOSIT_KEY env)')
|
|
30
|
+
.option('--wallet-key <key>', 'Ethereum wallet key for signing (or set WALLET_KEY env)')
|
|
31
|
+
.option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
|
|
32
|
+
.option('--unsigned', 'Output unsigned transaction payload (Bankr-compatible format)')
|
|
33
|
+
.option('--quiet', 'Suppress progress output')
|
|
34
|
+
.action(async (amount: string, options) => {
|
|
35
|
+
try {
|
|
36
|
+
const amountNum = parseFloat(amount);
|
|
37
|
+
|
|
38
|
+
// Check minimum deposit
|
|
39
|
+
if (amountNum < MINIMUM_DEPOSIT_WITH_FEE) {
|
|
40
|
+
throw new CLIError(
|
|
41
|
+
ErrorCode.INVALID_AMOUNT,
|
|
42
|
+
`Minimum deposit is ${MINIMUM_DEPOSIT_ETH} ETH (net). ` +
|
|
43
|
+
`With ${DEPOSIT_FEE_PERCENT}% fee, send at least ${MINIMUM_DEPOSIT_WITH_FEE.toFixed(5)} ETH.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get deposit key from option or env
|
|
48
|
+
const depositKey = options.depositKey || process.env.DEPOSIT_KEY;
|
|
49
|
+
if (!depositKey) {
|
|
50
|
+
throw new CLIError(ErrorCode.DEPOSIT_KEY_MISSING, 'Deposit key required. Use --deposit-key or set DEPOSIT_KEY in .env (run: veil init)');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
progress('Building transaction...', options.quiet);
|
|
54
|
+
|
|
55
|
+
// Build the transaction
|
|
56
|
+
const tx = buildDepositETHTx({
|
|
57
|
+
depositKey,
|
|
58
|
+
amount,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Handle --unsigned mode (no wallet required, just build payload)
|
|
62
|
+
if (options.unsigned) {
|
|
63
|
+
progress('', options.quiet); // Clear line
|
|
64
|
+
// Output Bankr-compatible format
|
|
65
|
+
const payload = {
|
|
66
|
+
to: tx.to,
|
|
67
|
+
data: tx.data,
|
|
68
|
+
value: tx.value ? tx.value.toString() : '0',
|
|
69
|
+
chainId: 8453, // Base mainnet
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Regular mode: sign and send
|
|
77
|
+
const config = getConfig(options);
|
|
78
|
+
const address = getAddress(config.privateKey);
|
|
79
|
+
|
|
80
|
+
progress('Checking balance...', options.quiet);
|
|
81
|
+
|
|
82
|
+
// Check balance
|
|
83
|
+
const balance = await getBalance(address, config.rpcUrl);
|
|
84
|
+
const amountWei = parseEther(amount);
|
|
85
|
+
|
|
86
|
+
if (balance < amountWei) {
|
|
87
|
+
progress('', options.quiet);
|
|
88
|
+
throw new CLIError(ErrorCode.INSUFFICIENT_BALANCE, `Insufficient ETH balance. Have: ${formatEther(balance)} ETH, Need: ${amount} ETH`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
progress('Sending transaction...', options.quiet);
|
|
92
|
+
|
|
93
|
+
// Send the transaction
|
|
94
|
+
const result = await sendTransaction(config, tx);
|
|
95
|
+
|
|
96
|
+
progress('Confirming...', options.quiet);
|
|
97
|
+
|
|
98
|
+
// Clear progress line
|
|
99
|
+
progress('', options.quiet);
|
|
100
|
+
|
|
101
|
+
console.log(JSON.stringify({
|
|
102
|
+
success: result.receipt.status === 'success',
|
|
103
|
+
hash: result.hash,
|
|
104
|
+
amount,
|
|
105
|
+
blockNumber: result.receipt.blockNumber.toString(),
|
|
106
|
+
gasUsed: result.receipt.gasUsed.toString(),
|
|
107
|
+
}, null, 2));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
progress('', options.quiet); // Clear progress line
|
|
110
|
+
handleCLIError(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return deposit;
|
|
115
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init CLI command - Generate and save a Veil keypair
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { dirname } from 'path';
|
|
9
|
+
import { mkdirSync } from 'fs';
|
|
10
|
+
import { Keypair } from '../../keypair.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Prompt user for yes/no confirmation
|
|
14
|
+
*/
|
|
15
|
+
async function confirm(question: string): Promise<boolean> {
|
|
16
|
+
const rl = createInterface({
|
|
17
|
+
input: process.stdin,
|
|
18
|
+
output: process.stdout,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(`${question} (y/n): `, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the default path to .env.veil file (current directory)
|
|
31
|
+
*/
|
|
32
|
+
function getDefaultEnvPath(): string {
|
|
33
|
+
return '.env.veil';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if VEIL_KEY exists in an env file
|
|
38
|
+
*/
|
|
39
|
+
function veilKeyExistsAt(envPath: string): boolean {
|
|
40
|
+
if (!existsSync(envPath)) return false;
|
|
41
|
+
|
|
42
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
43
|
+
return /^VEIL_KEY=/m.test(content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Update or add a key in .env content
|
|
48
|
+
*/
|
|
49
|
+
function updateEnvVar(content: string, key: string, value: string): string {
|
|
50
|
+
const regex = new RegExp(`^${key}=.*`, 'm');
|
|
51
|
+
if (regex.test(content)) {
|
|
52
|
+
return content.replace(regex, `${key}=${value}`);
|
|
53
|
+
} else {
|
|
54
|
+
if (content && !content.endsWith('\n')) {
|
|
55
|
+
content += '\n';
|
|
56
|
+
}
|
|
57
|
+
return content + `${key}=${value}\n`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Save Veil keypair to a file
|
|
63
|
+
*/
|
|
64
|
+
function saveVeilKeypair(veilKey: string, depositKey: string, envPath: string): void {
|
|
65
|
+
// Ensure directory exists
|
|
66
|
+
const dir = dirname(envPath);
|
|
67
|
+
if (dir && dir !== '.' && !existsSync(dir)) {
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let content = '';
|
|
72
|
+
|
|
73
|
+
if (existsSync(envPath)) {
|
|
74
|
+
content = readFileSync(envPath, 'utf-8');
|
|
75
|
+
} else {
|
|
76
|
+
content = '# Veil Cash Configuration\n# Generated by: veil init\n\n';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
content = updateEnvVar(content, 'VEIL_KEY', veilKey);
|
|
80
|
+
content = updateEnvVar(content, 'DEPOSIT_KEY', depositKey);
|
|
81
|
+
|
|
82
|
+
writeFileSync(envPath, content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createInitCommand(): Command {
|
|
86
|
+
const init = new Command('init')
|
|
87
|
+
.description('Generate a new Veil keypair')
|
|
88
|
+
.option('--force', 'Overwrite existing keypair without prompting')
|
|
89
|
+
.option('--json', 'Output as JSON (no prompts, no file save)')
|
|
90
|
+
.option('--out <path>', 'Save to custom path instead of .env.veil')
|
|
91
|
+
.option('--no-save', 'Print keypair without saving to file')
|
|
92
|
+
.action(async (options) => {
|
|
93
|
+
const envPath = options.out || getDefaultEnvPath();
|
|
94
|
+
|
|
95
|
+
// JSON mode: no prompts, no save, just output JSON
|
|
96
|
+
if (options.json) {
|
|
97
|
+
const kp = new Keypair();
|
|
98
|
+
console.log(JSON.stringify({
|
|
99
|
+
veilKey: kp.privkey,
|
|
100
|
+
depositKey: kp.depositKey(),
|
|
101
|
+
}, null, 2));
|
|
102
|
+
process.exit(0);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// No-save mode: print but don't save
|
|
107
|
+
if (!options.save) {
|
|
108
|
+
const kp = new Keypair();
|
|
109
|
+
console.log('\nGenerated new Veil keypair:\n');
|
|
110
|
+
console.log('Veil Private Key:');
|
|
111
|
+
console.log(` ${kp.privkey}\n`);
|
|
112
|
+
console.log('Deposit Key (register this on-chain):');
|
|
113
|
+
console.log(` ${kp.depositKey()}\n`);
|
|
114
|
+
console.log('(Not saved - use --out <path> to save to a file)');
|
|
115
|
+
process.exit(0);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if VEIL_KEY already exists (unless --force)
|
|
120
|
+
const keyExists = veilKeyExistsAt(envPath);
|
|
121
|
+
|
|
122
|
+
if (keyExists && !options.force) {
|
|
123
|
+
console.log(`\nWARNING: A Veil key already exists in ${envPath}`);
|
|
124
|
+
const proceed = await confirm('Create a new key? This will overwrite the existing one');
|
|
125
|
+
if (!proceed) {
|
|
126
|
+
console.log('Aborted. Existing key preserved.');
|
|
127
|
+
process.exit(0);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const kp = new Keypair();
|
|
133
|
+
|
|
134
|
+
console.log('\nGenerated new Veil keypair:\n');
|
|
135
|
+
console.log('Veil Private Key:');
|
|
136
|
+
console.log(` ${kp.privkey}\n`);
|
|
137
|
+
console.log('Deposit Key (register this on-chain):');
|
|
138
|
+
console.log(` ${kp.depositKey()}\n`);
|
|
139
|
+
|
|
140
|
+
saveVeilKeypair(kp.privkey!, kp.depositKey(), envPath);
|
|
141
|
+
console.log(`Saved to ${envPath}`);
|
|
142
|
+
console.log('\nNext step: veil register');
|
|
143
|
+
process.exit(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return init;
|
|
147
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keypair CLI command - Show current keypair as JSON
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { Keypair } from '../../keypair.js';
|
|
7
|
+
|
|
8
|
+
export function createKeypairCommand(): Command {
|
|
9
|
+
const keypair = new Command('keypair')
|
|
10
|
+
.description('Show current Veil keypair as JSON')
|
|
11
|
+
.action(() => {
|
|
12
|
+
const veilKey = process.env.VEIL_KEY;
|
|
13
|
+
|
|
14
|
+
if (!veilKey) {
|
|
15
|
+
console.log(JSON.stringify({
|
|
16
|
+
success: false,
|
|
17
|
+
error: 'No keypair found. Run "veil init" first.'
|
|
18
|
+
}, null, 2));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const kp = new Keypair(veilKey);
|
|
23
|
+
|
|
24
|
+
console.log(JSON.stringify({
|
|
25
|
+
veilPrivateKey: kp.privkey,
|
|
26
|
+
depositKey: kp.depositKey(),
|
|
27
|
+
}, null, 2));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return keypair;
|
|
31
|
+
}
|