@veil-cash/sdk 0.6.5 → 0.7.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 CHANGED
@@ -8,6 +8,8 @@ SDK and CLI for interacting with [Veil Cash](https://veil.cash) privacy pools on
8
8
 
9
9
  Generate keypairs, register, deposit, withdraw, transfer, and merge ETH and USDC privately.
10
10
 
11
+ `0.7.0` adds Coinbase-compatible x402 payments from private USDC balances via deterministic fresh payer EOAs.
12
+
11
13
  `0.6.2` adds `mergeSubaccount` — transfer a subaccount's private pool balance back to the main wallet via a ZK proof. Also adds `veil subaccount merge` CLI command.
12
14
 
13
15
  `0.6.0` adds SDK-first subaccount support for deterministic slot derivation, forwarder status, relay-backed deploy/sweep, and direct recovery.
@@ -91,7 +93,10 @@ veil subaccount sweep --slot 0 --asset eth
91
93
  veil subaccount merge --slot 0 --pool eth
92
94
  veil subaccount recover --slot 0 --asset usdc --to 0xRecipientAddress --amount 25
93
95
 
94
- # 9. Use JSON or unsigned modes when you need automation
96
+ # 9. Pay an x402 resource from private USDC in application code
97
+ # See SDK.md for payX402Resource().
98
+
99
+ # 10. Use JSON or unsigned modes when you need automation
95
100
  veil status --json
96
101
  veil deposit ETH 0.1 --unsigned --address 0x...
97
102
  veil subaccount status --slot 0 --json
@@ -142,11 +147,11 @@ SIGNER_ADDRESS=0x... veil register --unsigned
142
147
 
143
148
  In `--unsigned` mode, `--address` is optional when `SIGNER_ADDRESS` is set. `veil register --force` checks on-chain state first and emits a `changeDepositKey` payload only if the address is already registered; otherwise it emits a normal `register` payload.
144
149
 
145
- Deposit ETH or USDC into Veil. The amount you specify is the **net** amount that arrives in your Veil balance. A 0.3% protocol fee is normally added on top, but each address gets free daily deposits (fee waived). The CLI checks automatically:
150
+ Deposit ETH or USDC into Veil. The amount you specify is the **net** amount that arrives in your Veil balance. A 0.3% protocol fee is added on top:
146
151
 
147
152
  ```bash
148
- veil deposit ETH 0.1 # 0.1 ETH lands in pool (free or ~0.1003 ETH sent)
149
- veil deposit USDC 100 # 100 USDC lands in pool (free or ~100.30 USDC sent)
153
+ veil deposit ETH 0.1 # 0.1 ETH lands in pool (~0.1003 ETH sent)
154
+ veil deposit USDC 100 # 100 USDC lands in pool (~100.30 USDC sent)
150
155
  veil deposit ETH 0.1 --json
151
156
  veil deposit ETH 0.1 --unsigned --address 0x...
152
157
  SIGNER_ADDRESS=0x... veil deposit ETH 0.1 --unsigned
@@ -205,6 +210,43 @@ veil merge USDC 100
205
210
  veil merge ETH 0.1 --json
206
211
  ```
207
212
 
213
+ Pay a Coinbase-compatible x402 resource from private USDC:
214
+
215
+ ```typescript
216
+ import { payX402Resource } from '@veil-cash/sdk';
217
+
218
+ const result = await payX402Resource({
219
+ url: 'https://merchant.example/paid-resource',
220
+ rootPrivateKey: process.env.VEIL_KEY as `0x${string}`,
221
+ payerIndex: 0n,
222
+ maxPayment: '0.10', // reject if the resource demands more than 0.10 USDC
223
+ relayUrl: process.env.X402_RELAY_URL,
224
+ rpcUrl: process.env.RPC_URL,
225
+ });
226
+
227
+ console.log(result.response.status, result.payerAddress);
228
+ ```
229
+
230
+ The helper withdraws the exact x402 amount to a deterministic fresh payer EOA,
231
+ then signs a standard x402 v2 Base USDC `exact` payment from that EOA. Use a
232
+ new `payerIndex` per payment. Pass `maxPayment` (a decimal USDC string) to cap
233
+ exposure; the payment is rejected before any funds move if the requirement
234
+ exceeds the cap. The withdrawal and payment legs are public and can be correlated
235
+ by amount and timing, but the source private balance remains hidden.
236
+
237
+ Use `quoteX402Resource({ url, rpcUrl, maxPayment, init })` to probe a resource
238
+ for its price and payment requirement without funding a payer or paying. It
239
+ returns the parsed requirement for a supported `402` or the raw status/body
240
+ otherwise.
241
+
242
+ If a payment is funded but delivery fails, the USDC stays on the payer EOA. Retry
243
+ with `reuseExistingBalance: true` and the same `payerIndex` to pay from that
244
+ balance without a second withdrawal; it throws if the balance is insufficient.
245
+
246
+ To surface funds left on a payer after a failed payment, use
247
+ `getX402PayerBalances({ rootPrivateKey, startIndex, count, nonZeroOnly })`, which
248
+ returns the Base USDC balance of each derived payer EOA. It is read-only.
249
+
208
250
  ### Subaccounts
209
251
 
210
252
  Subaccounts are deterministic child slots derived from your main `VEIL_KEY`:
@@ -297,7 +339,7 @@ The CLI is the main entrypoint for most users. If you are integrating Veil progr
297
339
  2. **Derive Keypair**: Run `veil init` to derive and save your Veil keypair
298
340
  3. **Register**: Run `veil register` to link your deposit key on-chain (one-time)
299
341
  4. **Check Status**: Run `veil status` to verify your setup
300
- 5. **Deposit**: Run `veil deposit <asset> <amount>` — the amount is what lands in your balance (e.g., `veil deposit ETH 0.1` deposits 0.1 ETH; the fee is waived if you have daily free deposits remaining, otherwise 0.3% is added automatically)
342
+ 5. **Deposit**: Run `veil deposit <asset> <amount>` — the amount is what lands in your balance (e.g., `veil deposit ETH 0.1` deposits 0.1 ETH; a 0.3% protocol fee is added on top automatically)
301
343
  6. **Wait**: The Veil deposit engine processes your deposit
302
344
  7. **Done**: Your deposit is accepted into the privacy pool
303
345
 
package/SDK.md CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  Keypair, buildRegisterTx, buildDepositETHTx,
12
12
  buildDepositUSDCTx, buildApproveUSDCTx,
13
13
  withdraw, transfer, getSubaccountStatus, mergeSubaccount,
14
+ payX402Resource,
14
15
  } from '@veil-cash/sdk';
15
16
  import { createWalletClient, http } from 'viem';
16
17
  import { base } from 'viem/chains';
@@ -72,6 +73,14 @@ const subaccount = await getSubaccountStatus({
72
73
  });
73
74
  console.log(subaccount.slot.forwarderAddress);
74
75
 
76
+ // 8. Pay an x402 resource from private USDC
77
+ const paid = await payX402Resource({
78
+ url: 'https://merchant.example/paid-resource',
79
+ rootPrivateKey: keypair.privkey as `0x${string}`,
80
+ payerIndex: 0n,
81
+ relayUrl: process.env.X402_RELAY_URL,
82
+ });
83
+ console.log(paid.response.status, paid.payerAddress);
75
84
  ```
76
85
 
77
86
  ## SDK API Reference
@@ -109,10 +118,9 @@ keypair.privkey; // '0x...'
109
118
 
110
119
  ### Transaction Builders
111
120
 
112
- > **Daily free deposits**: Each address gets a configurable number of fee-free
113
- > deposits per UTC day. The CLI handles this automatically. If you use the
114
- > builders programmatically, call `getDailyFreeRemaining()` first — when the
115
- > user has free slots, pass the net amount directly (no fee markup needed).
121
+ The CLI treats deposit amounts as net amounts and adds the 0.3% protocol fee
122
+ automatically. The low-level transaction builders use the amount you pass as the
123
+ gross amount sent to the Entry contract.
116
124
 
117
125
  ```typescript
118
126
  import {
@@ -182,6 +190,70 @@ const mergeResult = await mergeUtxos({
182
190
  });
183
191
  ```
184
192
 
193
+ ### x402 Payments
194
+
195
+ `payX402Resource()` pays standard x402 v2 Base USDC `exact` resources from a
196
+ private Veil USDC balance while remaining compatible with Coinbase-facilitated
197
+ merchants.
198
+
199
+ ```typescript
200
+ import {
201
+ deriveX402PayerAddress,
202
+ payX402Resource,
203
+ usdcAtomicToDecimalString,
204
+ } from '@veil-cash/sdk';
205
+
206
+ const payerAddress = deriveX402PayerAddress(
207
+ process.env.VEIL_KEY as `0x${string}`,
208
+ 42n,
209
+ );
210
+
211
+ const amount = usdcAtomicToDecimalString('1000'); // "0.001"
212
+
213
+ const result = await payX402Resource({
214
+ url: 'https://merchant.example/paid-resource',
215
+ rootPrivateKey: process.env.VEIL_KEY as `0x${string}`,
216
+ payerIndex: 42n,
217
+ maxPayment: '0.10', // cap exposure; reject if the resource demands more
218
+ relayUrl: process.env.X402_RELAY_URL,
219
+ rpcUrl: process.env.RPC_URL,
220
+ onProgress: (stage, detail) => console.log(stage, detail),
221
+ });
222
+
223
+ console.log({
224
+ status: result.response.status,
225
+ payerAddress: result.payerAddress,
226
+ relayTx: result.relayTransactionHash,
227
+ paymentTx: result.paymentTransactionHash,
228
+ });
229
+ ```
230
+
231
+ The payer key uses the `veil-x402-payer` derivation domain, separate from
232
+ subaccounts. Use a fresh, persisted `payerIndex` for each payment. Set
233
+ `maxPayment` (a decimal USDC string) to cap exposure; the payment is rejected
234
+ before any funds move if the requirement exceeds the cap. The helper currently
235
+ supports x402 v2 `exact` requirements on Base mainnet USDC only; it rejects other
236
+ assets, networks, and schemes.
237
+
238
+ `quoteX402Resource({ url, rpcUrl, maxPayment, init })` probes a resource without
239
+ funding a payer or signing a payment. For a supported `402` it returns the price
240
+ and requirement (`amount`, `amountAtomic`, `payTo`, `network`, `asset`, and
241
+ `exceedsMax` when `maxPayment` is set); for any other status it returns the raw
242
+ `status` and parsed `body`. Use it to validate a request and confirm cost before
243
+ committing a withdrawal. A merchant that validates the request body only after
244
+ payment will still return `402` here, so a quote cannot catch post-payment errors.
245
+
246
+ To retry a payment whose funding succeeded but whose delivery failed (the USDC is
247
+ still on the payer EOA), pass `reuseExistingBalance: true` with the same
248
+ `payerIndex`. When the payer already holds at least the required amount,
249
+ `payX402Resource` skips the withdrawal and pays directly from that balance
250
+ (`relayTransactionHash` is empty); it throws if the balance is insufficient.
251
+
252
+ `getX402PayerBalances({ rootPrivateKey, startIndex, count, nonZeroOnly })`
253
+ inspects the Base USDC balance held by each derived payer EOA, which is useful
254
+ for surfacing funds left on a payer after a failed payment. It is read-only and
255
+ does not move funds.
256
+
185
257
  ### Browser Proof Generation
186
258
 
187
259
  `withdraw()`, `transfer()`, `mergeUtxos()`, `mergeSubaccount()`, `buildWithdrawProof()`,
@@ -232,7 +304,7 @@ such as `circomlib`, `eth-sig-util`, and `fixed-merkle-tree`.
232
304
  Balance functions accept an optional `pool` parameter (`'eth'` | `'usdc'`), defaulting to `'eth'`.
233
305
 
234
306
  ```typescript
235
- import { getQueueBalance, getPrivateBalance, getDailyFreeRemaining } from '@veil-cash/sdk';
307
+ import { getQueueBalance, getPrivateBalance } from '@veil-cash/sdk';
236
308
 
237
309
  // Check ETH queue balance (pending deposits)
238
310
  const queueBalance = await getQueueBalance({
@@ -246,12 +318,6 @@ const privateBalance = await getPrivateBalance({
246
318
  pool: 'usdc',
247
319
  });
248
320
 
249
- // Check how many fee-free deposits the user has left today
250
- const freeRemaining = await getDailyFreeRemaining({
251
- address: '0x...',
252
- pool: 'eth', // default
253
- });
254
- // freeRemaining: number — 0 when all free slots are used or the feature is disabled
255
321
  ```
256
322
 
257
323
  ### Subaccounts
@@ -4204,14 +4204,6 @@ var QUEUE_ABI = [
4204
4204
  outputs: [{ name: "count", type: "uint256" }],
4205
4205
  stateMutability: "view",
4206
4206
  type: "function"
4207
- },
4208
- // Get remaining daily free deposits for an address (V3+)
4209
- {
4210
- inputs: [{ name: "_depositor", type: "address" }],
4211
- name: "getDailyFreeRemaining",
4212
- outputs: [{ name: "remaining", type: "uint256" }],
4213
- stateMutability: "view",
4214
- type: "function"
4215
4207
  }
4216
4208
  ];
4217
4209
  var POOL_ABI = [
@@ -5126,7 +5118,9 @@ var poseidonHash2 = (a, b) => poseidonHash([a, b]);
5126
5118
  var randomBN = (nbytes = 31) => {
5127
5119
  const cryptoApi = globalThis.crypto;
5128
5120
  if (!cryptoApi?.getRandomValues) {
5129
- throw new Error("Secure random number generation is unavailable in this runtime");
5121
+ throw new Error(
5122
+ "Secure random number generation is unavailable. Provide globalThis.crypto.getRandomValues in this runtime."
5123
+ );
5130
5124
  }
5131
5125
  const bytes = cryptoApi.getRandomValues(new Uint8Array(nbytes));
5132
5126
  let hex = "0x";
@@ -5803,6 +5797,195 @@ function createRegisterCommand() {
5803
5797
  });
5804
5798
  return register;
5805
5799
  }
5800
+ var MINIMUM_NET = {
5801
+ ETH: 0.01,
5802
+ USDC: 10
5803
+ };
5804
+ async function getGrossAmount(netWei, rpcUrl) {
5805
+ const publicClient = viem.createPublicClient({
5806
+ chain: chains.base,
5807
+ transport: viem.http(rpcUrl)
5808
+ });
5809
+ const grossWei = await publicClient.readContract({
5810
+ address: getAddresses().entry,
5811
+ abi: ENTRY_ABI,
5812
+ functionName: "getDepositAmountWithFee",
5813
+ args: [netWei]
5814
+ });
5815
+ return { grossWei, feeWei: grossWei - netWei };
5816
+ }
5817
+ var SUPPORTED_ASSETS = ["ETH", "USDC"];
5818
+ function createDepositCommand() {
5819
+ const deposit = new Command("deposit").description("Deposit ETH or USDC into Veil").argument("<asset>", "Asset to deposit (ETH or USDC)").argument("<amount>", "Amount to deposit \u2014 this is what arrives in your Veil balance").option("--address <address>", "Signer address (required in --unsigned mode unless SIGNER_ADDRESS or WALLET_KEY is set)").option("--unsigned", "Output unsigned transaction payload instead of sending").option("--json", "Output as JSON").addHelpText("after", `
5820
+ The amount you specify is the net amount that lands in your Veil balance.
5821
+ A 0.3% protocol fee is added on top.
5822
+
5823
+ Examples:
5824
+ veil deposit ETH 0.1 # deposits 0.1 ETH (~0.1003 ETH sent)
5825
+ veil deposit USDC 100 # deposits 100 USDC (~100.30 USDC sent)
5826
+ veil deposit ETH 0.1 --unsigned --address 0x...
5827
+ SIGNER_ADDRESS=0x... veil deposit ETH 0.1 --unsigned
5828
+ veil deposit ETH 0.1 --json
5829
+ `).action(async (asset, amount, options) => {
5830
+ try {
5831
+ const assetUpper = asset.toUpperCase();
5832
+ if (!SUPPORTED_ASSETS.includes(assetUpper)) {
5833
+ throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported asset: ${asset}. Supported: ${SUPPORTED_ASSETS.join(", ")}`);
5834
+ }
5835
+ const amountNum = parseFloat(amount);
5836
+ const minimumNet = MINIMUM_NET[assetUpper];
5837
+ if (amountNum < minimumNet) {
5838
+ throw new CLIError(
5839
+ ErrorCode.INVALID_AMOUNT,
5840
+ `Minimum deposit is ${minimumNet} ${assetUpper}.`
5841
+ );
5842
+ }
5843
+ const rpcUrl = process.env.RPC_URL;
5844
+ const pool = assetUpper.toLowerCase();
5845
+ const poolConfig = POOL_CONFIG[pool];
5846
+ const netWei = assetUpper === "ETH" ? viem.parseEther(amount) : viem.parseUnits(amount, poolConfig.decimals);
5847
+ const progress = createProgressReporter();
5848
+ let config = null;
5849
+ let address;
5850
+ let feeRpcUrl = rpcUrl;
5851
+ if (options.unsigned) {
5852
+ const resolved = resolveAddress({ address: options.address }, { required: true });
5853
+ if (!resolved) {
5854
+ throw new CLIError(
5855
+ ErrorCode.WALLET_KEY_MISSING,
5856
+ "Must provide --address, set SIGNER_ADDRESS, or set WALLET_KEY env."
5857
+ );
5858
+ }
5859
+ address = resolved.address;
5860
+ } else {
5861
+ config = getConfig(options);
5862
+ address = getAddress(config.privateKey);
5863
+ feeRpcUrl = config.rpcUrl;
5864
+ }
5865
+ progress("Checking deposit fee...");
5866
+ const { grossWei, feeWei } = await getGrossAmount(netWei, feeRpcUrl);
5867
+ const grossStr = assetUpper === "ETH" ? viem.formatEther(grossWei) : viem.formatUnits(grossWei, poolConfig.decimals);
5868
+ const feeStr = assetUpper === "ETH" ? viem.formatEther(feeWei) : viem.formatUnits(feeWei, poolConfig.decimals);
5869
+ const depositKey = process.env.DEPOSIT_KEY;
5870
+ if (!depositKey) {
5871
+ throw new CLIError(ErrorCode.DEPOSIT_KEY_MISSING, 'DEPOSIT_KEY not set. Run "veil init" first.');
5872
+ }
5873
+ progress("Building transaction...");
5874
+ let tx;
5875
+ let approveTx = null;
5876
+ if (assetUpper === "USDC") {
5877
+ approveTx = buildApproveUSDCTx({ amount: grossStr });
5878
+ tx = buildDepositUSDCTx({ depositKey, amount: grossStr });
5879
+ } else {
5880
+ tx = buildDepositETHTx({ depositKey, amount: grossStr });
5881
+ }
5882
+ if (options.unsigned) {
5883
+ clearProgress();
5884
+ const payloads = [];
5885
+ if (approveTx) {
5886
+ payloads.push({
5887
+ step: "approve",
5888
+ to: approveTx.to,
5889
+ data: approveTx.data,
5890
+ value: "0",
5891
+ chainId: 8453
5892
+ });
5893
+ }
5894
+ payloads.push({
5895
+ step: "deposit",
5896
+ to: tx.to,
5897
+ data: tx.data,
5898
+ value: tx.value ? tx.value.toString() : "0",
5899
+ chainId: 8453
5900
+ });
5901
+ printJson(payloads.length === 1 ? payloads[0] : payloads);
5902
+ return;
5903
+ }
5904
+ if (!config) {
5905
+ throw new CLIError(ErrorCode.WALLET_KEY_MISSING, "WALLET_KEY env var required. Set it before running this command.");
5906
+ }
5907
+ if (assetUpper === "ETH") {
5908
+ progress("Checking balance...");
5909
+ const balance = await getBalance(address, config.rpcUrl);
5910
+ if (balance < grossWei) {
5911
+ clearProgress();
5912
+ throw new CLIError(
5913
+ ErrorCode.INSUFFICIENT_BALANCE,
5914
+ `Insufficient ETH balance. Have: ${viem.formatEther(balance)} ETH, Need: ${grossStr} ETH (${amount} + fee)`
5915
+ );
5916
+ }
5917
+ }
5918
+ if (approveTx) {
5919
+ progress(`Approving ${assetUpper}...`);
5920
+ const approvalResult = await sendTransaction(config, approveTx);
5921
+ if (assetUpper === "USDC") {
5922
+ const publicClient = viem.createPublicClient({
5923
+ chain: chains.base,
5924
+ transport: viem.http(config.rpcUrl)
5925
+ });
5926
+ const addresses = getAddresses();
5927
+ let allowance = await publicClient.readContract({
5928
+ address: getAddresses().usdcToken,
5929
+ abi: ERC20_ABI,
5930
+ functionName: "allowance",
5931
+ args: [address, addresses.entry]
5932
+ });
5933
+ for (let confirmations = 2; allowance < grossWei && confirmations <= 3; confirmations++) {
5934
+ await publicClient.waitForTransactionReceipt({
5935
+ hash: approvalResult.hash,
5936
+ confirmations
5937
+ });
5938
+ allowance = await publicClient.readContract({
5939
+ address: addresses.usdcToken,
5940
+ abi: ERC20_ABI,
5941
+ functionName: "allowance",
5942
+ args: [address, addresses.entry]
5943
+ });
5944
+ }
5945
+ if (allowance < grossWei) {
5946
+ throw new CLIError(
5947
+ ErrorCode.CONTRACT_ERROR,
5948
+ `USDC approval is not yet visible on RPC after confirmation. Allowance ${allowance.toString()} < required ${grossWei.toString()}.`
5949
+ );
5950
+ }
5951
+ }
5952
+ }
5953
+ progress("Sending deposit transaction...");
5954
+ const result = await sendTransaction(config, tx);
5955
+ progress("Confirming...");
5956
+ clearProgress();
5957
+ const output = {
5958
+ success: result.receipt.status === "success",
5959
+ hash: result.hash,
5960
+ asset: assetUpper,
5961
+ amount,
5962
+ fee: feeStr,
5963
+ totalSent: grossStr,
5964
+ blockNumber: result.receipt.blockNumber.toString()
5965
+ };
5966
+ if (options.json) {
5967
+ printJson(output);
5968
+ return;
5969
+ }
5970
+ const feeLabel = `${feeStr} ${assetUpper} (0.3%)`;
5971
+ printHeader("Deposit Submitted");
5972
+ printFields([
5973
+ { label: "Asset", value: assetUpper },
5974
+ { label: "Amount", value: `${amount} ${assetUpper}` },
5975
+ { label: "Fee", value: feeLabel },
5976
+ { label: "Total sent", value: `${grossStr} ${assetUpper}` },
5977
+ { label: "From", value: address },
5978
+ { label: "Transaction", value: txUrl(result.hash) },
5979
+ { label: "Block", value: result.receipt.blockNumber }
5980
+ ]);
5981
+ printLine();
5982
+ } catch (error) {
5983
+ clearProgress();
5984
+ handleCLIError(error);
5985
+ }
5986
+ });
5987
+ return deposit;
5988
+ }
5806
5989
  var Utxo = class _Utxo {
5807
5990
  amount;
5808
5991
  blinding;
@@ -5933,25 +6116,6 @@ async function getQueueBalance(options) {
5933
6116
  pendingCount: pendingDeposits.length
5934
6117
  };
5935
6118
  }
5936
- async function getDailyFreeRemaining(options) {
5937
- const { address, pool = "eth", rpcUrl } = options;
5938
- const queueAddress = getQueueAddress(pool);
5939
- const publicClient = viem.createPublicClient({
5940
- chain: chains.base,
5941
- transport: viem.http(rpcUrl)
5942
- });
5943
- try {
5944
- const remaining = await publicClient.readContract({
5945
- address: queueAddress,
5946
- abi: QUEUE_ABI,
5947
- functionName: "getDailyFreeRemaining",
5948
- args: [address]
5949
- });
5950
- return Number(remaining);
5951
- } catch {
5952
- return 0;
5953
- }
5954
- }
5955
6119
  async function getPrivateBalance(options) {
5956
6120
  const { keypair, pool = "eth", rpcUrl, onProgress } = options;
5957
6121
  const poolAddress = getPoolAddress(pool);
@@ -6045,206 +6209,6 @@ async function getPrivateBalance(options) {
6045
6209
  utxos: utxoInfos
6046
6210
  };
6047
6211
  }
6048
- var MINIMUM_NET = {
6049
- ETH: 0.01,
6050
- USDC: 10
6051
- };
6052
- async function getGrossAmount(netWei, depositor, pool, rpcUrl) {
6053
- const freeRemaining = await getDailyFreeRemaining({ address: depositor, pool, rpcUrl });
6054
- if (freeRemaining > 0) {
6055
- return { grossWei: netWei, feeWei: 0n, dailyFreeUsed: true, dailyFreeRemaining: freeRemaining - 1 };
6056
- }
6057
- const publicClient = viem.createPublicClient({
6058
- chain: chains.base,
6059
- transport: viem.http(rpcUrl)
6060
- });
6061
- const grossWei = await publicClient.readContract({
6062
- address: getAddresses().entry,
6063
- abi: ENTRY_ABI,
6064
- functionName: "getDepositAmountWithFee",
6065
- args: [netWei]
6066
- });
6067
- return { grossWei, feeWei: grossWei - netWei, dailyFreeUsed: false, dailyFreeRemaining: 0 };
6068
- }
6069
- var SUPPORTED_ASSETS = ["ETH", "USDC"];
6070
- function createDepositCommand() {
6071
- const deposit = new Command("deposit").description("Deposit ETH or USDC into Veil").argument("<asset>", "Asset to deposit (ETH or USDC)").argument("<amount>", "Amount to deposit \u2014 this is what arrives in your Veil balance").option("--address <address>", "Signer address (required in --unsigned mode unless SIGNER_ADDRESS or WALLET_KEY is set)").option("--unsigned", "Output unsigned transaction payload instead of sending").option("--json", "Output as JSON").addHelpText("after", `
6072
- The amount you specify is the net amount that lands in your Veil balance.
6073
- A 0.3% protocol fee is normally added on top, but each address gets
6074
- free daily deposits (fee waived). The CLI checks automatically.
6075
-
6076
- Examples:
6077
- veil deposit ETH 0.1 # deposits 0.1 ETH (free or ~0.1003 ETH)
6078
- veil deposit USDC 100 # deposits 100 USDC (free or ~100.30 USDC)
6079
- veil deposit ETH 0.1 --unsigned --address 0x...
6080
- SIGNER_ADDRESS=0x... veil deposit ETH 0.1 --unsigned
6081
- veil deposit ETH 0.1 --json
6082
- `).action(async (asset, amount, options) => {
6083
- try {
6084
- const assetUpper = asset.toUpperCase();
6085
- if (!SUPPORTED_ASSETS.includes(assetUpper)) {
6086
- throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported asset: ${asset}. Supported: ${SUPPORTED_ASSETS.join(", ")}`);
6087
- }
6088
- const amountNum = parseFloat(amount);
6089
- const minimumNet = MINIMUM_NET[assetUpper];
6090
- if (amountNum < minimumNet) {
6091
- throw new CLIError(
6092
- ErrorCode.INVALID_AMOUNT,
6093
- `Minimum deposit is ${minimumNet} ${assetUpper}.`
6094
- );
6095
- }
6096
- const rpcUrl = process.env.RPC_URL;
6097
- const pool = assetUpper.toLowerCase();
6098
- const poolConfig = POOL_CONFIG[pool];
6099
- const netWei = assetUpper === "ETH" ? viem.parseEther(amount) : viem.parseUnits(amount, poolConfig.decimals);
6100
- const progress = createProgressReporter();
6101
- let config = null;
6102
- let address;
6103
- let feeRpcUrl = rpcUrl;
6104
- if (options.unsigned) {
6105
- const resolved = resolveAddress({ address: options.address }, { required: true });
6106
- if (!resolved) {
6107
- throw new CLIError(
6108
- ErrorCode.WALLET_KEY_MISSING,
6109
- "Must provide --address, set SIGNER_ADDRESS, or set WALLET_KEY env."
6110
- );
6111
- }
6112
- address = resolved.address;
6113
- } else {
6114
- config = getConfig(options);
6115
- address = getAddress(config.privateKey);
6116
- feeRpcUrl = config.rpcUrl;
6117
- }
6118
- progress("Checking deposit fee...");
6119
- const { grossWei, feeWei, dailyFreeUsed, dailyFreeRemaining } = await getGrossAmount(
6120
- netWei,
6121
- address,
6122
- pool,
6123
- feeRpcUrl
6124
- );
6125
- const grossStr = assetUpper === "ETH" ? viem.formatEther(grossWei) : viem.formatUnits(grossWei, poolConfig.decimals);
6126
- const feeStr = assetUpper === "ETH" ? viem.formatEther(feeWei) : viem.formatUnits(feeWei, poolConfig.decimals);
6127
- const depositKey = process.env.DEPOSIT_KEY;
6128
- if (!depositKey) {
6129
- throw new CLIError(ErrorCode.DEPOSIT_KEY_MISSING, 'DEPOSIT_KEY not set. Run "veil init" first.');
6130
- }
6131
- progress("Building transaction...");
6132
- let tx;
6133
- let approveTx = null;
6134
- if (assetUpper === "USDC") {
6135
- approveTx = buildApproveUSDCTx({ amount: grossStr });
6136
- tx = buildDepositUSDCTx({ depositKey, amount: grossStr });
6137
- } else {
6138
- tx = buildDepositETHTx({ depositKey, amount: grossStr });
6139
- }
6140
- if (options.unsigned) {
6141
- clearProgress();
6142
- const payloads = [];
6143
- if (approveTx) {
6144
- payloads.push({
6145
- step: "approve",
6146
- to: approveTx.to,
6147
- data: approveTx.data,
6148
- value: "0",
6149
- chainId: 8453
6150
- });
6151
- }
6152
- payloads.push({
6153
- step: "deposit",
6154
- to: tx.to,
6155
- data: tx.data,
6156
- value: tx.value ? tx.value.toString() : "0",
6157
- chainId: 8453
6158
- });
6159
- printJson(payloads.length === 1 ? payloads[0] : payloads);
6160
- return;
6161
- }
6162
- if (!config) {
6163
- throw new CLIError(ErrorCode.WALLET_KEY_MISSING, "WALLET_KEY env var required. Set it before running this command.");
6164
- }
6165
- if (assetUpper === "ETH") {
6166
- progress("Checking balance...");
6167
- const balance = await getBalance(address, config.rpcUrl);
6168
- if (balance < grossWei) {
6169
- clearProgress();
6170
- throw new CLIError(
6171
- ErrorCode.INSUFFICIENT_BALANCE,
6172
- `Insufficient ETH balance. Have: ${viem.formatEther(balance)} ETH, Need: ${grossStr} ETH (${amount} + fee)`
6173
- );
6174
- }
6175
- }
6176
- if (approveTx) {
6177
- progress(`Approving ${assetUpper}...`);
6178
- const approvalResult = await sendTransaction(config, approveTx);
6179
- if (assetUpper === "USDC") {
6180
- const publicClient = viem.createPublicClient({
6181
- chain: chains.base,
6182
- transport: viem.http(config.rpcUrl)
6183
- });
6184
- const addresses = getAddresses();
6185
- let allowance = await publicClient.readContract({
6186
- address: getAddresses().usdcToken,
6187
- abi: ERC20_ABI,
6188
- functionName: "allowance",
6189
- args: [address, addresses.entry]
6190
- });
6191
- for (let confirmations = 2; allowance < grossWei && confirmations <= 3; confirmations++) {
6192
- await publicClient.waitForTransactionReceipt({
6193
- hash: approvalResult.hash,
6194
- confirmations
6195
- });
6196
- allowance = await publicClient.readContract({
6197
- address: addresses.usdcToken,
6198
- abi: ERC20_ABI,
6199
- functionName: "allowance",
6200
- args: [address, addresses.entry]
6201
- });
6202
- }
6203
- if (allowance < grossWei) {
6204
- throw new CLIError(
6205
- ErrorCode.CONTRACT_ERROR,
6206
- `USDC approval is not yet visible on RPC after confirmation. Allowance ${allowance.toString()} < required ${grossWei.toString()}.`
6207
- );
6208
- }
6209
- }
6210
- }
6211
- progress("Sending deposit transaction...");
6212
- const result = await sendTransaction(config, tx);
6213
- progress("Confirming...");
6214
- clearProgress();
6215
- const output = {
6216
- success: result.receipt.status === "success",
6217
- hash: result.hash,
6218
- asset: assetUpper,
6219
- amount,
6220
- fee: feeStr,
6221
- dailyFreeUsed,
6222
- totalSent: grossStr,
6223
- blockNumber: result.receipt.blockNumber.toString()
6224
- };
6225
- if (options.json) {
6226
- printJson(output);
6227
- return;
6228
- }
6229
- const feeLabel = dailyFreeUsed ? `0 ${assetUpper} (free \u2014 ${dailyFreeRemaining} remaining today)` : `${feeStr} ${assetUpper} (0.3%)`;
6230
- printHeader("Deposit Submitted");
6231
- printFields([
6232
- { label: "Asset", value: assetUpper },
6233
- { label: "Amount", value: `${amount} ${assetUpper}` },
6234
- { label: "Fee", value: feeLabel },
6235
- { label: "Total sent", value: `${grossStr} ${assetUpper}` },
6236
- { label: "From", value: address },
6237
- { label: "Transaction", value: txUrl(result.hash) },
6238
- { label: "Block", value: result.receipt.blockNumber }
6239
- ]);
6240
- printLine();
6241
- } catch (error) {
6242
- clearProgress();
6243
- handleCLIError(error);
6244
- }
6245
- });
6246
- return deposit;
6247
- }
6248
6212
 
6249
6213
  // src/cli/commands/private-balance.ts
6250
6214
  var SUPPORTED_POOLS = ["eth", "usdc"];
@@ -6994,12 +6958,13 @@ async function buildWithdrawProof(options) {
6994
6958
  };
6995
6959
  }
6996
6960
  async function withdraw(options) {
6997
- const { amount, recipient, pool = "eth", onProgress } = options;
6961
+ const { amount, recipient, pool = "eth", onProgress, relayUrl } = options;
6998
6962
  const proof = await buildWithdrawProof(options);
6999
6963
  onProgress?.("Submitting to relay...");
7000
6964
  const relayResult = await submitRelay({
7001
6965
  type: "withdraw",
7002
6966
  pool,
6967
+ relayUrl,
7003
6968
  proofArgs: proof.proofArgs,
7004
6969
  extData: proof.extData,
7005
6970
  metadata: {