@veil-cash/sdk 0.5.0 → 0.6.1

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.
@@ -3917,6 +3917,7 @@ var ErrorCode = {
3917
3917
  DEPOSIT_KEY_MISSING: "DEPOSIT_KEY_MISSING",
3918
3918
  CONFIG_CONFLICT: "CONFIG_CONFLICT",
3919
3919
  INVALID_ADDRESS: "INVALID_ADDRESS",
3920
+ INVALID_SLOT: "INVALID_SLOT",
3920
3921
  INVALID_AMOUNT: "INVALID_AMOUNT",
3921
3922
  INSUFFICIENT_BALANCE: "INSUFFICIENT_BALANCE",
3922
3923
  USER_NOT_REGISTERED: "USER_NOT_REGISTERED",
@@ -3952,6 +3953,9 @@ function inferErrorCode(message) {
3952
3953
  if (msg.includes("invalid") && msg.includes("address")) {
3953
3954
  return ErrorCode.INVALID_ADDRESS;
3954
3955
  }
3956
+ if (msg.includes("invalid") && msg.includes("slot")) {
3957
+ return ErrorCode.INVALID_SLOT;
3958
+ }
3955
3959
  if (msg.includes("insufficient balance") || msg.includes("not enough")) {
3956
3960
  return ErrorCode.INSUFFICIENT_BALANCE;
3957
3961
  }
@@ -4616,6 +4620,170 @@ var ERC20_ABI = [
4616
4620
  type: "function"
4617
4621
  }
4618
4622
  ];
4623
+ var FORWARDER_FACTORY_ABI = [
4624
+ {
4625
+ inputs: [],
4626
+ name: "CONTRACT_VERSION",
4627
+ outputs: [{ internalType: "string", name: "", type: "string" }],
4628
+ stateMutability: "view",
4629
+ type: "function"
4630
+ },
4631
+ {
4632
+ inputs: [
4633
+ { internalType: "bytes32", name: "_salt", type: "bytes32" },
4634
+ { internalType: "bytes", name: "_childDepositKey", type: "bytes" },
4635
+ { internalType: "address", name: "_owner", type: "address" }
4636
+ ],
4637
+ name: "computeAddress",
4638
+ outputs: [{ internalType: "address", name: "", type: "address" }],
4639
+ stateMutability: "view",
4640
+ type: "function"
4641
+ },
4642
+ {
4643
+ inputs: [
4644
+ { internalType: "bytes32", name: "_salt", type: "bytes32" },
4645
+ { internalType: "bytes", name: "_childDepositKey", type: "bytes" },
4646
+ { internalType: "address", name: "_owner", type: "address" }
4647
+ ],
4648
+ name: "deploy",
4649
+ outputs: [{ internalType: "address", name: "forwarder", type: "address" }],
4650
+ stateMutability: "nonpayable",
4651
+ type: "function"
4652
+ },
4653
+ {
4654
+ inputs: [],
4655
+ name: "relayer",
4656
+ outputs: [{ internalType: "address", name: "", type: "address" }],
4657
+ stateMutability: "view",
4658
+ type: "function"
4659
+ },
4660
+ {
4661
+ inputs: [],
4662
+ name: "veilEntry",
4663
+ outputs: [{ internalType: "address payable", name: "", type: "address" }],
4664
+ stateMutability: "view",
4665
+ type: "function"
4666
+ },
4667
+ {
4668
+ inputs: [],
4669
+ name: "usdc",
4670
+ outputs: [{ internalType: "address", name: "", type: "address" }],
4671
+ stateMutability: "view",
4672
+ type: "function"
4673
+ },
4674
+ {
4675
+ inputs: [],
4676
+ name: "owner",
4677
+ outputs: [{ internalType: "address", name: "", type: "address" }],
4678
+ stateMutability: "view",
4679
+ type: "function"
4680
+ }
4681
+ ];
4682
+ var FORWARDER_ABI = [
4683
+ {
4684
+ inputs: [],
4685
+ name: "CONTRACT_VERSION",
4686
+ outputs: [{ internalType: "string", name: "", type: "string" }],
4687
+ stateMutability: "view",
4688
+ type: "function"
4689
+ },
4690
+ {
4691
+ inputs: [
4692
+ { internalType: "address", name: "_token", type: "address" },
4693
+ { internalType: "address", name: "_to", type: "address" },
4694
+ { internalType: "uint256", name: "_amount", type: "uint256" },
4695
+ { internalType: "uint256", name: "_nonce", type: "uint256" },
4696
+ { internalType: "uint256", name: "_deadline", type: "uint256" },
4697
+ { internalType: "bytes", name: "_signature", type: "bytes" }
4698
+ ],
4699
+ name: "withdraw",
4700
+ outputs: [],
4701
+ stateMutability: "nonpayable",
4702
+ type: "function"
4703
+ },
4704
+ {
4705
+ inputs: [{ internalType: "uint256", name: "", type: "uint256" }],
4706
+ name: "usedNonces",
4707
+ outputs: [{ internalType: "bool", name: "", type: "bool" }],
4708
+ stateMutability: "view",
4709
+ type: "function"
4710
+ },
4711
+ {
4712
+ inputs: [],
4713
+ name: "owner",
4714
+ outputs: [{ internalType: "address", name: "", type: "address" }],
4715
+ stateMutability: "view",
4716
+ type: "function"
4717
+ },
4718
+ {
4719
+ inputs: [],
4720
+ name: "factory",
4721
+ outputs: [{ internalType: "address", name: "", type: "address" }],
4722
+ stateMutability: "view",
4723
+ type: "function"
4724
+ },
4725
+ {
4726
+ inputs: [],
4727
+ name: "childDepositKey",
4728
+ outputs: [{ internalType: "bytes", name: "", type: "bytes" }],
4729
+ stateMutability: "view",
4730
+ type: "function"
4731
+ },
4732
+ {
4733
+ inputs: [],
4734
+ name: "entry",
4735
+ outputs: [{ internalType: "address", name: "", type: "address" }],
4736
+ stateMutability: "view",
4737
+ type: "function"
4738
+ },
4739
+ {
4740
+ inputs: [],
4741
+ name: "usdc",
4742
+ outputs: [{ internalType: "address", name: "", type: "address" }],
4743
+ stateMutability: "view",
4744
+ type: "function"
4745
+ },
4746
+ {
4747
+ inputs: [],
4748
+ name: "sweepETH",
4749
+ outputs: [],
4750
+ stateMutability: "nonpayable",
4751
+ type: "function"
4752
+ },
4753
+ {
4754
+ inputs: [],
4755
+ name: "sweepUSDC",
4756
+ outputs: [],
4757
+ stateMutability: "nonpayable",
4758
+ type: "function"
4759
+ },
4760
+ {
4761
+ inputs: [],
4762
+ name: "eip712Domain",
4763
+ outputs: [
4764
+ { internalType: "bytes1", name: "fields", type: "bytes1" },
4765
+ { internalType: "string", name: "name", type: "string" },
4766
+ { internalType: "string", name: "version", type: "string" },
4767
+ { internalType: "uint256", name: "chainId", type: "uint256" },
4768
+ { internalType: "address", name: "verifyingContract", type: "address" },
4769
+ { internalType: "bytes32", name: "salt", type: "bytes32" },
4770
+ { internalType: "uint256[]", name: "extensions", type: "uint256[]" }
4771
+ ],
4772
+ stateMutability: "view",
4773
+ type: "function"
4774
+ },
4775
+ { type: "error", name: "ZeroAddress", inputs: [] },
4776
+ { type: "error", name: "ZeroAmount", inputs: [] },
4777
+ { type: "error", name: "InvalidDepositKey", inputs: [] },
4778
+ { type: "error", name: "NotRelayer", inputs: [] },
4779
+ { type: "error", name: "NoETHBalance", inputs: [] },
4780
+ { type: "error", name: "NoTokenBalance", inputs: [] },
4781
+ { type: "error", name: "TokenApproveFailed", inputs: [] },
4782
+ { type: "error", name: "ETHTransferFailed", inputs: [] },
4783
+ { type: "error", name: "NonceUsed", inputs: [] },
4784
+ { type: "error", name: "Unauthorized", inputs: [] },
4785
+ { type: "error", name: "DeadlineExpired", inputs: [] }
4786
+ ];
4619
4787
 
4620
4788
  // src/addresses.ts
4621
4789
  var ADDRESSES = {
@@ -4625,9 +4793,11 @@ var ADDRESSES = {
4625
4793
  usdcPool: "0x5c50d58E49C59d112680c187De2Bf989d2a91242",
4626
4794
  usdcQueue: "0x5530241b24504bF05C9a22e95A1F5458888e6a9B",
4627
4795
  usdcToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
4796
+ forwarderFactory: "0x2848Fd62293A1ff3b4a897E9FcD0e5962dcc8101",
4628
4797
  chainId: 8453,
4629
4798
  relayUrl: "https://veil-relay.up.railway.app"
4630
4799
  };
4800
+ var FORWARDER_CONTRACT_VERSION = "1";
4631
4801
  var POOL_CONFIG = {
4632
4802
  eth: {
4633
4803
  decimals: 18,
@@ -4667,6 +4837,9 @@ function getQueueAddress(pool) {
4667
4837
  throw new Error(`Unknown pool: ${pool}`);
4668
4838
  }
4669
4839
  }
4840
+ function getForwarderFactoryAddress() {
4841
+ return getAddresses().forwarderFactory;
4842
+ }
4670
4843
  function getRelayUrl() {
4671
4844
  return ADDRESSES.relayUrl;
4672
4845
  }
@@ -4731,7 +4904,7 @@ function decodeCustomError(error) {
4731
4904
  const possibleData = anyError.data || anyError.cause?.data || anyError.cause?.cause?.data;
4732
4905
  if (possibleData && typeof possibleData === "string" && possibleData.startsWith("0x")) {
4733
4906
  try {
4734
- for (const abi of [ENTRY_ABI]) {
4907
+ for (const abi of [ENTRY_ABI, FORWARDER_ABI]) {
4735
4908
  try {
4736
4909
  const decoded = viem.decodeErrorResult({
4737
4910
  abi,
@@ -6500,6 +6673,27 @@ var RelayError = class extends Error {
6500
6673
  this.network = network;
6501
6674
  }
6502
6675
  };
6676
+ async function postRelayJson(endpoint, body, relayUrl) {
6677
+ const url = relayUrl || getRelayUrl();
6678
+ const response = await fetch(`${url}${endpoint}`, {
6679
+ method: "POST",
6680
+ headers: {
6681
+ "Content-Type": "application/json"
6682
+ },
6683
+ body: JSON.stringify(body)
6684
+ });
6685
+ const data = await response.json();
6686
+ if (!response.ok) {
6687
+ const errorData = data;
6688
+ throw new RelayError(
6689
+ errorData.error || errorData.message || "Relay request failed",
6690
+ response.status,
6691
+ errorData.retryAfter,
6692
+ errorData.network
6693
+ );
6694
+ }
6695
+ return data;
6696
+ }
6503
6697
  async function submitRelay(options) {
6504
6698
  const {
6505
6699
  type,
@@ -6519,30 +6713,17 @@ async function submitRelay(options) {
6519
6713
  throw new RelayError("Missing proofArgs or extData", 400);
6520
6714
  }
6521
6715
  const relayUrl = customRelayUrl || getRelayUrl();
6522
- const endpoint = `${relayUrl}/relay/${pool}`;
6523
- const response = await fetch(endpoint, {
6524
- method: "POST",
6525
- headers: {
6526
- "Content-Type": "application/json"
6527
- },
6528
- body: JSON.stringify({
6716
+ const endpoint = `/relay/${pool}`;
6717
+ return postRelayJson(
6718
+ endpoint,
6719
+ {
6529
6720
  type,
6530
6721
  proofArgs,
6531
6722
  extData,
6532
6723
  metadata
6533
- })
6534
- });
6535
- const data = await response.json();
6536
- if (!response.ok) {
6537
- const errorData = data;
6538
- throw new RelayError(
6539
- errorData.error || errorData.message || "Relay request failed",
6540
- response.status,
6541
- errorData.retryAfter,
6542
- errorData.network
6543
- );
6544
- }
6545
- return data;
6724
+ },
6725
+ relayUrl
6726
+ );
6546
6727
  }
6547
6728
  async function checkRelayHealth(relayUrl) {
6548
6729
  const url = relayUrl || getRelayUrl();
@@ -7332,16 +7513,618 @@ function maskUrl(url) {
7332
7513
  return maskValue(url);
7333
7514
  }
7334
7515
  }
7516
+ var SUBACCOUNT_CHILD_DOMAIN = "veil-sua-child";
7517
+ var SUBACCOUNT_SALT_DOMAIN = "veil-sua-salt";
7518
+ var ETH_ADDRESS = "0x0000000000000000000000000000000000000000";
7519
+ var DEFAULT_WITHDRAW_DEADLINE_SECONDS = 3600n;
7520
+ var DEFAULT_MAX_NONCE_SCAN = 100n;
7521
+ var MAX_SUBACCOUNT_SLOTS = 3;
7522
+ function createBaseClient(rpcUrl) {
7523
+ return viem.createPublicClient({
7524
+ chain: chains.base,
7525
+ transport: viem.http(rpcUrl)
7526
+ });
7527
+ }
7528
+ function assertPrivateKey(value, label) {
7529
+ if (!/^0x[a-fA-F0-9]{64}$/.test(value)) {
7530
+ throw new Error(`${label} must be a 0x-prefixed 32-byte hex string`);
7531
+ }
7532
+ }
7533
+ function normalizeSlot(slot) {
7534
+ if (!Number.isInteger(slot) || slot < 0) {
7535
+ throw new Error("slot must be a non-negative integer");
7536
+ }
7537
+ if (slot >= MAX_SUBACCOUNT_SLOTS) {
7538
+ throw new Error(`slot must be less than ${MAX_SUBACCOUNT_SLOTS} (supported slots: 0-${MAX_SUBACCOUNT_SLOTS - 1})`);
7539
+ }
7540
+ return slot;
7541
+ }
7542
+ function normalizeAsset(asset) {
7543
+ if (asset !== "eth" && asset !== "usdc") {
7544
+ throw new Error('asset must be "eth" or "usdc"');
7545
+ }
7546
+ return asset;
7547
+ }
7548
+ function normalizeNonce(value) {
7549
+ const nonce = typeof value === "bigint" ? value : BigInt(value);
7550
+ if (nonce < 0n) {
7551
+ throw new Error("nonce must be non-negative");
7552
+ }
7553
+ return nonce;
7554
+ }
7555
+ function normalizeDeadline(deadline) {
7556
+ const nextDeadline = deadline === void 0 ? BigInt(Math.floor(Date.now() / 1e3)) + DEFAULT_WITHDRAW_DEADLINE_SECONDS : typeof deadline === "bigint" ? deadline : BigInt(deadline);
7557
+ if (nextDeadline <= 0n) {
7558
+ throw new Error("deadline must be greater than 0");
7559
+ }
7560
+ return nextDeadline;
7561
+ }
7562
+ function deriveSubaccountChildPrivateKey(rootPrivateKey, slot) {
7563
+ assertPrivateKey(rootPrivateKey, "rootPrivateKey");
7564
+ const normalizedSlot = normalizeSlot(slot);
7565
+ return viem.keccak256(
7566
+ viem.encodePacked(
7567
+ ["bytes32", "string", "uint256"],
7568
+ [rootPrivateKey, SUBACCOUNT_CHILD_DOMAIN, BigInt(normalizedSlot)]
7569
+ )
7570
+ );
7571
+ }
7572
+ function deriveSubaccountSalt(rootPrivateKey, slot) {
7573
+ assertPrivateKey(rootPrivateKey, "rootPrivateKey");
7574
+ const normalizedSlot = normalizeSlot(slot);
7575
+ return viem.keccak256(
7576
+ viem.encodePacked(
7577
+ ["bytes32", "string", "uint256"],
7578
+ [rootPrivateKey, SUBACCOUNT_SALT_DOMAIN, BigInt(normalizedSlot)]
7579
+ )
7580
+ );
7581
+ }
7582
+ function deriveSubaccountChildOwner(childPrivateKey) {
7583
+ assertPrivateKey(childPrivateKey, "childPrivateKey");
7584
+ return accounts.privateKeyToAddress(childPrivateKey);
7585
+ }
7586
+ function deriveSubaccountChildDepositKey(childPrivateKey) {
7587
+ assertPrivateKey(childPrivateKey, "childPrivateKey");
7588
+ return new Keypair(childPrivateKey).depositKey();
7589
+ }
7590
+ async function predictSubaccountForwarder(options) {
7591
+ const publicClient = createBaseClient(options.rpcUrl);
7592
+ return publicClient.readContract({
7593
+ abi: FORWARDER_FACTORY_ABI,
7594
+ address: getForwarderFactoryAddress(),
7595
+ functionName: "computeAddress",
7596
+ args: [options.salt, options.childDepositKey, options.childOwner]
7597
+ });
7598
+ }
7599
+ async function deriveSubaccountSlot(options) {
7600
+ const normalizedSlot = normalizeSlot(options.slot);
7601
+ const childPrivateKey = deriveSubaccountChildPrivateKey(options.rootPrivateKey, normalizedSlot);
7602
+ const salt = deriveSubaccountSalt(options.rootPrivateKey, normalizedSlot);
7603
+ const childOwner = deriveSubaccountChildOwner(childPrivateKey);
7604
+ const childDepositKey = deriveSubaccountChildDepositKey(childPrivateKey);
7605
+ const forwarderAddress = await predictSubaccountForwarder({
7606
+ salt,
7607
+ childDepositKey,
7608
+ childOwner,
7609
+ rpcUrl: options.rpcUrl
7610
+ });
7611
+ return {
7612
+ slot: normalizedSlot,
7613
+ childOwner,
7614
+ childDepositKey,
7615
+ salt,
7616
+ forwarderAddress
7617
+ };
7618
+ }
7619
+ async function isSubaccountForwarderDeployed(options) {
7620
+ if (!viem.isAddress(options.forwarderAddress)) {
7621
+ throw new Error("forwarderAddress must be a valid Ethereum address");
7622
+ }
7623
+ const publicClient = createBaseClient(options.rpcUrl);
7624
+ const code = await publicClient.getCode({ address: options.forwarderAddress });
7625
+ return !!code && code !== "0x";
7626
+ }
7627
+ async function deploySubaccountForwarder(options) {
7628
+ const slot = await deriveSubaccountSlot({
7629
+ rootPrivateKey: options.rootPrivateKey,
7630
+ slot: options.slot,
7631
+ rpcUrl: options.rpcUrl
7632
+ });
7633
+ return postRelayJson(
7634
+ "/stealth/deploy",
7635
+ {
7636
+ salt: slot.salt,
7637
+ childDepositKey: slot.childDepositKey,
7638
+ childOwner: slot.childOwner,
7639
+ expectedForwarder: slot.forwarderAddress
7640
+ },
7641
+ options.relayUrl
7642
+ );
7643
+ }
7644
+ async function sweepSubaccountForwarder(options) {
7645
+ const asset = normalizeAsset(options.asset);
7646
+ if (!viem.isAddress(options.forwarderAddress)) {
7647
+ throw new Error("forwarderAddress must be a valid Ethereum address");
7648
+ }
7649
+ return postRelayJson(
7650
+ "/stealth/sweep",
7651
+ {
7652
+ forwarder: options.forwarderAddress,
7653
+ asset
7654
+ },
7655
+ options.relayUrl
7656
+ );
7657
+ }
7658
+ function toQueueStatus(asset, result) {
7659
+ return {
7660
+ asset,
7661
+ queueBalance: result.queueBalance,
7662
+ queueBalanceWei: result.queueBalanceWei,
7663
+ pendingCount: result.pendingCount,
7664
+ pendingDeposits: result.pendingDeposits
7665
+ };
7666
+ }
7667
+ async function getSubaccountStatus(options) {
7668
+ const slot = await deriveSubaccountSlot(options);
7669
+ const publicClient = createBaseClient(options.rpcUrl);
7670
+ const addresses = getAddresses();
7671
+ const [deployed, ethWei, usdcWei, ethQueue, usdcQueue] = await Promise.all([
7672
+ isSubaccountForwarderDeployed({
7673
+ forwarderAddress: slot.forwarderAddress,
7674
+ rpcUrl: options.rpcUrl
7675
+ }),
7676
+ publicClient.getBalance({ address: slot.forwarderAddress }),
7677
+ publicClient.readContract({
7678
+ address: addresses.usdcToken,
7679
+ abi: ERC20_ABI,
7680
+ functionName: "balanceOf",
7681
+ args: [slot.forwarderAddress]
7682
+ }),
7683
+ getQueueBalance({
7684
+ address: slot.forwarderAddress,
7685
+ pool: "eth",
7686
+ rpcUrl: options.rpcUrl
7687
+ }),
7688
+ getQueueBalance({
7689
+ address: slot.forwarderAddress,
7690
+ pool: "usdc",
7691
+ rpcUrl: options.rpcUrl
7692
+ })
7693
+ ]);
7694
+ return {
7695
+ slot,
7696
+ deployed,
7697
+ balances: {
7698
+ eth: {
7699
+ balance: viem.formatEther(ethWei),
7700
+ balanceWei: ethWei.toString()
7701
+ },
7702
+ usdc: {
7703
+ balance: viem.formatUnits(usdcWei, 6),
7704
+ balanceWei: usdcWei.toString()
7705
+ }
7706
+ },
7707
+ queues: {
7708
+ eth: toQueueStatus("eth", ethQueue),
7709
+ usdc: toQueueStatus("usdc", usdcQueue)
7710
+ }
7711
+ };
7712
+ }
7713
+ var WITHDRAW_TYPES = {
7714
+ Withdraw: [
7715
+ { name: "token", type: "address" },
7716
+ { name: "to", type: "address" },
7717
+ { name: "amount", type: "uint256" },
7718
+ { name: "nonce", type: "uint256" },
7719
+ { name: "deadline", type: "uint256" }
7720
+ ]
7721
+ };
7722
+ function buildSubaccountWithdrawTypedData(options) {
7723
+ if (!viem.isAddress(options.forwarderAddress)) {
7724
+ throw new Error("forwarderAddress must be a valid Ethereum address");
7725
+ }
7726
+ if (!viem.isAddress(options.token)) {
7727
+ throw new Error("token must be a valid Ethereum address");
7728
+ }
7729
+ if (!viem.isAddress(options.to)) {
7730
+ throw new Error("to must be a valid Ethereum address");
7731
+ }
7732
+ return {
7733
+ domain: {
7734
+ name: "VeilForwarder",
7735
+ version: FORWARDER_CONTRACT_VERSION,
7736
+ chainId: getAddresses().chainId,
7737
+ verifyingContract: options.forwarderAddress
7738
+ },
7739
+ types: WITHDRAW_TYPES,
7740
+ primaryType: "Withdraw",
7741
+ message: {
7742
+ token: options.token,
7743
+ to: options.to,
7744
+ amount: options.amount,
7745
+ nonce: options.nonce,
7746
+ deadline: options.deadline
7747
+ }
7748
+ };
7749
+ }
7750
+ async function signSubaccountWithdraw(options) {
7751
+ assertPrivateKey(options.childPrivateKey, "childPrivateKey");
7752
+ const account = accounts.privateKeyToAccount(options.childPrivateKey);
7753
+ return account.signTypedData({
7754
+ domain: options.typedData.domain,
7755
+ types: options.typedData.types,
7756
+ primaryType: options.typedData.primaryType,
7757
+ message: options.typedData.message
7758
+ });
7759
+ }
7760
+ async function isSubaccountWithdrawNonceUsed(options) {
7761
+ if (!viem.isAddress(options.forwarderAddress)) {
7762
+ throw new Error("forwarderAddress must be a valid Ethereum address");
7763
+ }
7764
+ const publicClient = createBaseClient(options.rpcUrl);
7765
+ try {
7766
+ return await publicClient.readContract({
7767
+ abi: FORWARDER_ABI,
7768
+ address: options.forwarderAddress,
7769
+ functionName: "usedNonces",
7770
+ args: [normalizeNonce(options.nonce)]
7771
+ });
7772
+ } catch (error) {
7773
+ if (String(error).includes("returned no data")) {
7774
+ throw new Error("Subaccount forwarder is not deployed");
7775
+ }
7776
+ throw error;
7777
+ }
7778
+ }
7779
+ async function findNextSubaccountWithdrawNonce(options) {
7780
+ const startNonce = normalizeNonce(options.startNonce ?? 0n);
7781
+ const maxScan = normalizeNonce(options.maxScan ?? DEFAULT_MAX_NONCE_SCAN);
7782
+ const limit = startNonce + maxScan;
7783
+ let nonce = startNonce;
7784
+ while (await isSubaccountWithdrawNonceUsed({
7785
+ forwarderAddress: options.forwarderAddress,
7786
+ nonce,
7787
+ rpcUrl: options.rpcUrl
7788
+ })) {
7789
+ nonce += 1n;
7790
+ if (nonce > limit) {
7791
+ throw new Error("Unable to find an unused withdraw nonce within the scan limit");
7792
+ }
7793
+ }
7794
+ return nonce;
7795
+ }
7796
+ async function buildSubaccountRecoveryTx(options) {
7797
+ if (!viem.isAddress(options.to)) {
7798
+ throw new Error("to must be a valid Ethereum address");
7799
+ }
7800
+ const asset = normalizeAsset(options.asset);
7801
+ const slot = await deriveSubaccountSlot({
7802
+ rootPrivateKey: options.rootPrivateKey,
7803
+ slot: options.slot,
7804
+ rpcUrl: options.rpcUrl
7805
+ });
7806
+ const deployed = await isSubaccountForwarderDeployed({
7807
+ forwarderAddress: slot.forwarderAddress,
7808
+ rpcUrl: options.rpcUrl
7809
+ });
7810
+ if (!deployed) {
7811
+ throw new Error("Subaccount forwarder is not deployed");
7812
+ }
7813
+ const childPrivateKey = deriveSubaccountChildPrivateKey(options.rootPrivateKey, options.slot);
7814
+ const tokenAddress = asset === "eth" ? ETH_ADDRESS : getAddresses().usdcToken;
7815
+ const amountWei = asset === "eth" ? viem.parseEther(options.amount) : viem.parseUnits(options.amount, 6);
7816
+ const nonce = options.nonce === void 0 ? await findNextSubaccountWithdrawNonce({
7817
+ forwarderAddress: slot.forwarderAddress,
7818
+ rpcUrl: options.rpcUrl
7819
+ }) : normalizeNonce(options.nonce);
7820
+ const deadline = normalizeDeadline(options.deadline);
7821
+ const typedData = buildSubaccountWithdrawTypedData({
7822
+ forwarderAddress: slot.forwarderAddress,
7823
+ token: tokenAddress,
7824
+ to: options.to,
7825
+ amount: amountWei,
7826
+ nonce,
7827
+ deadline
7828
+ });
7829
+ const signature = await signSubaccountWithdraw({
7830
+ childPrivateKey,
7831
+ typedData
7832
+ });
7833
+ return {
7834
+ transaction: {
7835
+ to: slot.forwarderAddress,
7836
+ data: viem.encodeFunctionData({
7837
+ abi: FORWARDER_ABI,
7838
+ functionName: "withdraw",
7839
+ args: [tokenAddress, options.to, amountWei, nonce, deadline, signature]
7840
+ })
7841
+ },
7842
+ forwarderAddress: slot.forwarderAddress,
7843
+ asset,
7844
+ amount: options.amount,
7845
+ amountWei: amountWei.toString(),
7846
+ nonce: nonce.toString(),
7847
+ deadline: deadline.toString(),
7848
+ recipient: options.to,
7849
+ tokenAddress,
7850
+ signature
7851
+ };
7852
+ }
7853
+
7854
+ // src/cli/commands/subaccount.ts
7855
+ function parseSlotValue(raw) {
7856
+ const normalized = raw.trim();
7857
+ if (!/^\d+$/.test(normalized)) {
7858
+ throw new CLIError(ErrorCode.INVALID_SLOT, "--slot must be a non-negative integer");
7859
+ }
7860
+ const slot = Number(normalized);
7861
+ if (slot >= MAX_SUBACCOUNT_SLOTS) {
7862
+ throw new CLIError(
7863
+ ErrorCode.INVALID_SLOT,
7864
+ `--slot must be 0-${MAX_SUBACCOUNT_SLOTS - 1} (max ${MAX_SUBACCOUNT_SLOTS} subaccounts supported)`
7865
+ );
7866
+ }
7867
+ return slot;
7868
+ }
7869
+ function getRequiredVeilKey() {
7870
+ const veilKey = process.env.VEIL_KEY;
7871
+ if (!veilKey) {
7872
+ throw new CLIError(ErrorCode.VEIL_KEY_MISSING, "VEIL_KEY required. Set VEIL_KEY env");
7873
+ }
7874
+ if (!/^0x[a-fA-F0-9]{64}$/.test(veilKey)) {
7875
+ throw new CLIError(ErrorCode.VEIL_KEY_MISSING, "VEIL_KEY must be a 0x-prefixed 32-byte hex string");
7876
+ }
7877
+ return veilKey;
7878
+ }
7879
+ function parseAsset(raw) {
7880
+ const asset = raw.toLowerCase();
7881
+ if (asset !== "eth" && asset !== "usdc") {
7882
+ throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported asset: ${raw}. Supported: eth, usdc`);
7883
+ }
7884
+ return asset;
7885
+ }
7886
+ function printQueueHuman(title, queue) {
7887
+ printSection(title);
7888
+ printFields([
7889
+ { label: "Queue balance", value: queue.queueBalance },
7890
+ { label: "Pending", value: queue.pendingCount }
7891
+ ]);
7892
+ if (queue.pendingDeposits.length > 0) {
7893
+ printList(
7894
+ queue.pendingDeposits.map((deposit) => `nonce ${deposit.nonce}: ${deposit.amount} (${deposit.status})`)
7895
+ );
7896
+ }
7897
+ }
7898
+ function createSubaccountCommand() {
7899
+ const subaccount = new Command("subaccount").description("Manage Veil subaccounts").addHelpText("after", `
7900
+ Examples:
7901
+ veil subaccount derive --slot 0
7902
+ veil subaccount status --slot 0
7903
+ veil subaccount deploy --slot 0
7904
+ veil subaccount sweep --slot 0 --asset eth
7905
+ veil subaccount recover --slot 0 --asset usdc --to 0xRecipientAddress --amount 25
7906
+ veil subaccount address --slot 0
7907
+ `);
7908
+ subaccount.command("derive").description("Derive subaccount metadata for a slot").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).option("--json", "Output as JSON").action(async (options) => {
7909
+ try {
7910
+ const rootPrivateKey = getRequiredVeilKey();
7911
+ const rpcUrl = process.env.RPC_URL;
7912
+ const slot = await deriveSubaccountSlot({
7913
+ rootPrivateKey,
7914
+ slot: options.slot,
7915
+ rpcUrl
7916
+ });
7917
+ const deployed = await isSubaccountForwarderDeployed({
7918
+ forwarderAddress: slot.forwarderAddress,
7919
+ rpcUrl
7920
+ });
7921
+ const output = {
7922
+ ...slot,
7923
+ deployed
7924
+ };
7925
+ if (options.json) {
7926
+ printJson(output);
7927
+ return;
7928
+ }
7929
+ printHeader(`Subaccount Slot ${slot.slot}`);
7930
+ printFields([
7931
+ { label: "Child owner", value: slot.childOwner },
7932
+ { label: "Deposit key", value: slot.childDepositKey },
7933
+ { label: "Salt", value: slot.salt },
7934
+ { label: "Forwarder", value: slot.forwarderAddress },
7935
+ { label: "Deployed", value: deployed }
7936
+ ]);
7937
+ printLine();
7938
+ } catch (error) {
7939
+ handleCLIError(error);
7940
+ }
7941
+ });
7942
+ subaccount.command("status").description("Show subaccount deployment, balances, and queue state").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).option("--json", "Output as JSON").action(async (options) => {
7943
+ try {
7944
+ const rootPrivateKey = getRequiredVeilKey();
7945
+ const status = await getSubaccountStatus({
7946
+ rootPrivateKey,
7947
+ slot: options.slot,
7948
+ rpcUrl: process.env.RPC_URL
7949
+ });
7950
+ if (options.json) {
7951
+ printJson(status);
7952
+ return;
7953
+ }
7954
+ printHeader(`Subaccount Slot ${status.slot.slot}`);
7955
+ printFields([
7956
+ { label: "Forwarder", value: status.slot.forwarderAddress },
7957
+ { label: "Child owner", value: status.slot.childOwner },
7958
+ { label: "Deposit key", value: status.slot.childDepositKey },
7959
+ { label: "Salt", value: status.slot.salt },
7960
+ { label: "Deployed", value: status.deployed }
7961
+ ]);
7962
+ printSection("Forwarder Balances");
7963
+ printFields([
7964
+ { label: "ETH", value: `${status.balances.eth.balance} ETH` },
7965
+ { label: "USDC", value: `${status.balances.usdc.balance} USDC` }
7966
+ ]);
7967
+ printQueueHuman("ETH Queue", status.queues.eth);
7968
+ printQueueHuman("USDC Queue", status.queues.usdc);
7969
+ printLine();
7970
+ } catch (error) {
7971
+ handleCLIError(error);
7972
+ }
7973
+ });
7974
+ subaccount.command("deploy").description("Deploy a subaccount forwarder through the relay").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).option("--json", "Output as JSON").action(async (options) => {
7975
+ try {
7976
+ const rootPrivateKey = getRequiredVeilKey();
7977
+ const slot = await deriveSubaccountSlot({
7978
+ rootPrivateKey,
7979
+ slot: options.slot,
7980
+ rpcUrl: process.env.RPC_URL
7981
+ });
7982
+ const result = await deploySubaccountForwarder({
7983
+ rootPrivateKey,
7984
+ slot: options.slot,
7985
+ rpcUrl: process.env.RPC_URL,
7986
+ relayUrl: process.env.RELAY_URL
7987
+ });
7988
+ const output = {
7989
+ ...result,
7990
+ slot: options.slot,
7991
+ forwarderAddress: slot.forwarderAddress
7992
+ };
7993
+ if (options.json) {
7994
+ printJson(output);
7995
+ return;
7996
+ }
7997
+ printHeader("Subaccount Deploy Submitted");
7998
+ printFields([
7999
+ { label: "Slot", value: options.slot },
8000
+ { label: "Forwarder", value: slot.forwarderAddress },
8001
+ { label: "Transaction", value: txUrl(result.transactionHash) },
8002
+ { label: "Block", value: result.blockNumber }
8003
+ ]);
8004
+ printLine();
8005
+ } catch (error) {
8006
+ handleCLIError(error);
8007
+ }
8008
+ });
8009
+ subaccount.command("sweep").description("Sweep ETH or USDC from a subaccount forwarder through the relay").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).requiredOption("--asset <asset>", "Asset to sweep (eth or usdc)", parseAsset).option("--json", "Output as JSON").action(async (options) => {
8010
+ try {
8011
+ const rootPrivateKey = getRequiredVeilKey();
8012
+ const slot = await deriveSubaccountSlot({
8013
+ rootPrivateKey,
8014
+ slot: options.slot,
8015
+ rpcUrl: process.env.RPC_URL
8016
+ });
8017
+ const result = await sweepSubaccountForwarder({
8018
+ forwarderAddress: slot.forwarderAddress,
8019
+ asset: options.asset,
8020
+ relayUrl: process.env.RELAY_URL
8021
+ });
8022
+ const output = {
8023
+ ...result,
8024
+ slot: options.slot,
8025
+ asset: options.asset,
8026
+ forwarderAddress: slot.forwarderAddress
8027
+ };
8028
+ if (options.json) {
8029
+ printJson(output);
8030
+ return;
8031
+ }
8032
+ printHeader("Subaccount Sweep Submitted");
8033
+ printFields([
8034
+ { label: "Slot", value: options.slot },
8035
+ { label: "Asset", value: options.asset.toUpperCase() },
8036
+ { label: "Forwarder", value: slot.forwarderAddress },
8037
+ { label: "Transaction", value: txUrl(result.transactionHash) },
8038
+ { label: "Block", value: result.blockNumber }
8039
+ ]);
8040
+ printLine();
8041
+ } catch (error) {
8042
+ handleCLIError(error);
8043
+ }
8044
+ });
8045
+ subaccount.command("recover").description("Recover assets sitting on the subaccount forwarder with a direct withdraw transaction").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).requiredOption("--asset <asset>", "Asset to recover (eth or usdc)", parseAsset).requiredOption("--to <address>", "Recipient address").requiredOption("--amount <value>", "Amount to recover").option("--json", "Output as JSON").action(async (options) => {
8046
+ try {
8047
+ const rootPrivateKey = getRequiredVeilKey();
8048
+ if (!viem.isAddress(options.to)) {
8049
+ throw new CLIError(ErrorCode.INVALID_ADDRESS, `Invalid recipient address: ${options.to}`);
8050
+ }
8051
+ const config = getConfig({});
8052
+ const recovery = await buildSubaccountRecoveryTx({
8053
+ rootPrivateKey,
8054
+ slot: options.slot,
8055
+ asset: options.asset,
8056
+ to: options.to,
8057
+ amount: options.amount,
8058
+ rpcUrl: process.env.RPC_URL
8059
+ });
8060
+ const result = await sendTransaction(config, recovery.transaction);
8061
+ const output = {
8062
+ success: result.receipt.status === "success",
8063
+ slot: options.slot,
8064
+ asset: recovery.asset,
8065
+ amount: recovery.amount,
8066
+ amountWei: recovery.amountWei,
8067
+ forwarderAddress: recovery.forwarderAddress,
8068
+ recipient: recovery.recipient,
8069
+ nonce: recovery.nonce,
8070
+ deadline: recovery.deadline,
8071
+ signature: recovery.signature,
8072
+ transactionHash: result.hash,
8073
+ blockNumber: result.receipt.blockNumber.toString()
8074
+ };
8075
+ if (options.json) {
8076
+ printJson(output);
8077
+ return;
8078
+ }
8079
+ printHeader("Subaccount Recovery Submitted");
8080
+ printFields([
8081
+ { label: "Slot", value: options.slot },
8082
+ { label: "Asset", value: recovery.asset.toUpperCase() },
8083
+ { label: "Amount", value: recovery.amount },
8084
+ { label: "Recipient", value: recovery.recipient },
8085
+ { label: "Forwarder", value: recovery.forwarderAddress },
8086
+ { label: "Nonce", value: recovery.nonce },
8087
+ { label: "Transaction", value: txUrl(result.hash) },
8088
+ { label: "Block", value: result.receipt.blockNumber }
8089
+ ]);
8090
+ printLine();
8091
+ } catch (error) {
8092
+ handleCLIError(error);
8093
+ }
8094
+ });
8095
+ subaccount.command("address").description("Print the predicted forwarder address for a subaccount slot").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).option("--json", "Output as JSON").action(async (options) => {
8096
+ try {
8097
+ const rootPrivateKey = getRequiredVeilKey();
8098
+ const slot = await deriveSubaccountSlot({
8099
+ rootPrivateKey,
8100
+ slot: options.slot,
8101
+ rpcUrl: process.env.RPC_URL
8102
+ });
8103
+ if (options.json) {
8104
+ printJson({
8105
+ slot: options.slot,
8106
+ forwarderAddress: slot.forwarderAddress
8107
+ });
8108
+ return;
8109
+ }
8110
+ printLine(slot.forwarderAddress);
8111
+ } catch (error) {
8112
+ handleCLIError(error);
8113
+ }
8114
+ });
8115
+ return subaccount;
8116
+ }
7335
8117
 
7336
8118
  // src/cli/index.ts
7337
8119
  loadEnv();
7338
8120
  var program2 = new Command();
7339
- program2.name("veil").description("CLI for Veil Cash privacy pools on Base").version("0.5.0").addHelpText("after", `
8121
+ program2.name("veil").description("CLI for Veil Cash privacy pools on Base").version("0.6.0").addHelpText("after", `
7340
8122
  Getting started:
7341
8123
  veil init
7342
8124
  veil register
7343
8125
  veil deposit ETH 0.1
7344
8126
  veil balance
8127
+ veil subaccount status --slot 0
7345
8128
  `);
7346
8129
  program2.addCommand(createInitCommand());
7347
8130
  program2.addCommand(createKeypairCommand());
@@ -7354,6 +8137,7 @@ program2.addCommand(createWithdrawCommand());
7354
8137
  program2.addCommand(createTransferCommand());
7355
8138
  program2.addCommand(createMergeCommand());
7356
8139
  program2.addCommand(createStatusCommand());
8140
+ program2.addCommand(createSubaccountCommand());
7357
8141
  var knownTopLevelCommands = /* @__PURE__ */ new Set([
7358
8142
  ...program2.commands.map((command) => command.name()),
7359
8143
  "help"