Skip to content

Latest commit

 

History

History
507 lines (401 loc) · 15.7 KB

File metadata and controls

507 lines (401 loc) · 15.7 KB

SDK Guide

This package ships both a CLI and a TypeScript SDK.

If you are looking for the CLI first-run flow, go back to the main README.

SDK Quick Start

import {
  Keypair, buildRegisterTx, buildDepositETHTx,
  buildDepositUSDCTx, buildApproveUSDCTx,
  withdraw, transfer, getSubaccountStatus, mergeSubaccount,
  payX402Resource,
} from '@veil-cash/sdk';
import { createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

// 1. Generate a Veil keypair (do this once, save securely!)
const keypair = new Keypair();
console.log('Veil Private Key:', keypair.privkey);  // SAVE THIS!
console.log('Deposit Key:', keypair.depositKey());  // Register this on-chain

// 2. Setup your Ethereum wallet
const account = privateKeyToAccount('0x...');
const client = createWalletClient({
  account,
  chain: base,
  transport: http(),
});

// 3. Register your deposit key (one-time)
const registerTx = buildRegisterTx(keypair.depositKey(), account.address);
await client.sendTransaction(registerTx);

// 4. Deposit ETH
const depositTx = buildDepositETHTx({
  depositKey: keypair.depositKey(),
  amount: '0.1',
});
await client.sendTransaction({ ...depositTx, value: depositTx.value });

// 4b. Deposit USDC (approve first)
const approveTx = buildApproveUSDCTx({ amount: '100' });
await client.sendTransaction(approveTx);
const usdcTx = buildDepositUSDCTx({
  depositKey: keypair.depositKey(),
  amount: '100',
});
await client.sendTransaction(usdcTx);

// 5. Withdraw (sent via relayer, no wallet signing needed)
const withdrawResult = await withdraw({
  amount: '0.05',
  recipient: '0xRecipientAddress',
  keypair,
  pool: 'eth', // 'eth' | 'usdc' (default: 'eth')
});

// 6. Transfer privately
const transferResult = await transfer({
  amount: '0.02',
  recipientAddress: '0xRecipientAddress',
  senderKeypair: keypair,
  pool: 'eth', // 'eth' | 'usdc' (default: 'eth')
});

// 7. Inspect a deterministic subaccount slot
const subaccount = await getSubaccountStatus({
  rootPrivateKey: keypair.privkey as `0x${string}`,
  slot: 0,
});
console.log(subaccount.slot.forwarderAddress);

// 8. Pay an x402 resource from private USDC
const paid = await payX402Resource({
  url: 'https://merchant.example/paid-resource',
  rootPrivateKey: keypair.privkey as `0x${string}`,
  payerIndex: 0n,
  relayUrl: process.env.X402_RELAY_URL,
});
console.log(paid.response.status, paid.payerAddress);

SDK API Reference

Keypair

import { Keypair, VEIL_SIGNED_MESSAGE } from '@veil-cash/sdk';
import type { MessageSigner } from '@veil-cash/sdk';

// Generate random keypair
const keypair = new Keypair();

// Restore from saved Veil private key
const restored = new Keypair(savedVeilKey);

// Derive from wallet key (same keypair as frontend login)
const derived = await Keypair.fromWalletKey('0xYOUR_WALLET_KEY');

// Derive from a raw EIP-191 signature
const fromSig = Keypair.fromSignature('0xSIGNATURE...');

// Derive from any external signer (agent framework, MPC, custodial, etc.)
const fromSigner = await Keypair.fromSigner(async (message) => {
  // Sign `message` using any personal_sign provider and return the signature
  return await mySigningService.personalSign(message);
});

// Get deposit key (for registration)
keypair.depositKey(); // '0x...' (130 hex chars)

// Veil private key (store securely!)
keypair.privkey; // '0x...'

Transaction Builders

The CLI treats deposit amounts as net amounts and adds the 0.3% protocol fee automatically. The low-level transaction builders use the amount you pass as the gross amount sent to the Entry contract.

import {
  buildRegisterTx, buildChangeDepositKeyTx, buildDepositETHTx, buildDepositTx,
  buildDepositUSDCTx, buildApproveUSDCTx,
} from '@veil-cash/sdk';

// Register deposit key (first time)
const registerTx = buildRegisterTx(depositKey, ownerAddress);
// -> { to: '0x...', data: '0x...' }

// Change deposit key (must already be registered)
const changeTx = buildChangeDepositKeyTx(newDepositKey, ownerAddress);
// -> { to: '0x...', data: '0x...' }

// Deposit ETH
const depositTx = buildDepositETHTx({
  depositKey: keypair.depositKey(),
  amount: '0.1',
});
// -> { to: '0x...', data: '0x...', value: 100000000000000000n }

// Deposit USDC (approve + deposit)
const approveUsdcTx = buildApproveUSDCTx({ amount: '100' });
const depositUsdcTx = buildDepositUSDCTx({
  depositKey: keypair.depositKey(),
  amount: '100',
});

// Generic builder (routes by token)
const tx = buildDepositTx({
  depositKey: keypair.depositKey(),
  amount: '0.1',
  token: 'ETH', // 'ETH' | 'USDC'
});

Withdraw & Transfer

All withdraw, transfer, and merge functions accept an optional pool parameter ('eth' | 'usdc'), defaulting to 'eth'.

import { withdraw, transfer, mergeUtxos } from '@veil-cash/sdk';

// Withdraw ETH to public address
const withdrawResult = await withdraw({
  amount: '0.05',
  recipient: '0xRecipientAddress',
  keypair,
  pool: 'eth', // default
  onProgress: (stage, detail) => console.log(stage, detail),
});

// Withdraw USDC
const withdrawUsdc = await withdraw({
  amount: '50',
  recipient: '0xRecipientAddress',
  keypair,
  pool: 'usdc',
});

// Merge UTXOs (consolidate small balances)
const mergeResult = await mergeUtxos({
  amount: '0.1',
  keypair,
  pool: 'eth',
});

x402 Payments

payX402Resource() pays standard x402 v2 Base USDC exact resources from a private Veil USDC balance while remaining compatible with Coinbase-facilitated merchants.

import {
  deriveX402PayerAddress,
  payX402Resource,
  usdcAtomicToDecimalString,
} from '@veil-cash/sdk';

const payerAddress = deriveX402PayerAddress(
  process.env.VEIL_KEY as `0x${string}`,
  42n,
);

const amount = usdcAtomicToDecimalString('1000'); // "0.001"

const result = await payX402Resource({
  url: 'https://merchant.example/paid-resource',
  rootPrivateKey: process.env.VEIL_KEY as `0x${string}`,
  payerIndex: 42n,
  maxPayment: '0.10', // cap exposure; reject if the resource demands more
  relayUrl: process.env.X402_RELAY_URL,
  rpcUrl: process.env.RPC_URL,
  onProgress: (stage, detail) => console.log(stage, detail),
});

console.log({
  status: result.response.status,
  payerAddress: result.payerAddress,
  relayTx: result.relayTransactionHash,
  paymentTx: result.paymentTransactionHash,
});

The payer key uses the veil-x402-payer derivation domain, separate from subaccounts. Use a fresh, persisted payerIndex for each payment. Set maxPayment (a decimal USDC string) to cap exposure; the payment is rejected before any funds move if the requirement exceeds the cap. The helper currently supports x402 v2 exact requirements on Base mainnet USDC only; it rejects other assets, networks, and schemes.

quoteX402Resource({ url, rpcUrl, maxPayment, init }) probes a resource without funding a payer or signing a payment. For a supported 402 it returns the price and requirement (amount, amountAtomic, payTo, network, asset, and exceedsMax when maxPayment is set); for any other status it returns the raw status and parsed body. Use it to validate a request and confirm cost before committing a withdrawal. A merchant that validates the request body only after payment will still return 402 here, so a quote cannot catch post-payment errors.

To retry a payment whose funding succeeded but whose delivery failed (the USDC is still on the payer EOA), pass reuseExistingBalance: true with the same payerIndex. When the payer already holds at least the required amount, payX402Resource skips the withdrawal and pays directly from that balance (relayTransactionHash is empty); it throws if the balance is insufficient.

getX402PayerBalances({ rootPrivateKey, startIndex, count, nonZeroOnly }) inspects the Base USDC balance held by each derived payer EOA, which is useful for surfacing funds left on a payer after a failed payment. It is read-only and does not move funds.

Browser Proof Generation

withdraw(), transfer(), mergeUtxos(), mergeSubaccount(), buildWithdrawProof(), buildTransferProof(), and prepareTransaction() can generate proofs in browser runtimes.

In Node, the SDK uses the packaged keys/ directory by default. In browsers, the SDK defaults to same-origin hosted proving keys:

/keys/transaction2.wasm
/keys/transaction2.zkey
/keys/transaction16.wasm
/keys/transaction16.zkey

For most web apps, copy the SDK keys/ files into your app's public assets directory. For example, in Next.js/Vite, serving them from public/keys makes the default path work.

If you host keys elsewhere, pass provingKeyPath:

await withdraw({
  amount: '0.05',
  recipient: '0xRecipientAddress',
  keypair,
  provingKeyPath: 'https://cdn.example.com/veil-keys',
});

await transfer({
  amount: '0.02',
  recipientAddress: '0xRecipientAddress',
  senderKeypair: keypair,
  provingKeyPath: (circuitName) => `/static/zk/${circuitName}`,
});

provingKeyPath may be a directory/base URL (/keys) or a resolver returning the circuit base path without the .wasm / .zkey extension.

This removes Node fs key lookup from the browser proof path. Some bundlers may still require standard Node-core polyfills for existing legacy dependencies such as circomlib, eth-sig-util, and fixed-merkle-tree.

Balance Queries

Balance functions accept an optional pool parameter ('eth' | 'usdc'), defaulting to 'eth'.

import { getQueueBalance, getPrivateBalance } from '@veil-cash/sdk';

// Check ETH queue balance (pending deposits)
const queueBalance = await getQueueBalance({
  address: '0x...',
  pool: 'eth', // default
});

// Check USDC private balance (requires keypair)
const privateBalance = await getPrivateBalance({
  keypair,
  pool: 'usdc',
});

Subaccounts

Subaccounts are deterministic child slots derived from your main Veil key:

root key -> slot -> child key -> child deposit key -> forwarder

Base mainnet only. Status shows the child slot's forwarder wallet balances, private pool balances, and queue state. Deploy and sweep are relay-backed. Merge transfers the subaccount's private pool balance back to the main wallet via a ZK proof. Recovery signs a forwarder withdraw request with the child key and returns a direct transaction for your gas payer to submit.

import {
  deriveSubaccountSlot,
  getSubaccountPrivateBalance,
  getSubaccountStatus,
  deploySubaccountForwarder,
  sweepSubaccountForwarder,
  mergeSubaccount,
  buildSubaccountRecoveryTx,
} from '@veil-cash/sdk';

const slot = await deriveSubaccountSlot({
  rootPrivateKey: veilKey,
  slot: 0,
});

const status = await getSubaccountStatus({
  rootPrivateKey: veilKey,
  slot: 0,
});
// status.privateBalances.eth.privateBalance, status.privateBalances.usdc.privateBalance

const privateBalance = await getSubaccountPrivateBalance({
  rootPrivateKey: veilKey,
  slot: 0,
  pool: 'eth',
});
// privateBalance.privateBalance, privateBalance.unspentCount

await deploySubaccountForwarder({
  rootPrivateKey: veilKey,
  slot: 0,
});

await sweepSubaccountForwarder({
  forwarderAddress: slot.forwarderAddress,
  asset: 'eth',
});

// Merge subaccount's private pool balance back to the main wallet
const mergeResult = await mergeSubaccount({
  rootPrivateKey: veilKey,
  slot: 0,
  pool: 'eth', // 'eth' | 'usdc' (default: 'eth')
});
// mergeResult.amount, mergeResult.transactionHash

const recovery = await buildSubaccountRecoveryTx({
  rootPrivateKey: veilKey,
  slot: 0,
  asset: 'usdc',
  to: '0xRecipientAddress',
  amount: '25',
});
// Send recovery.transaction with your wallet client

Addresses

import { getAddresses, getPoolAddress, getQueueAddress } from '@veil-cash/sdk';

const addresses = getAddresses();
console.log(addresses.entry);     // Entry contract
console.log(addresses.ethPool);   // ETH pool
console.log(addresses.usdcPool);  // USDC pool

// Helper functions to resolve by pool name
console.log(getPoolAddress('eth'));   // ETH pool address
console.log(getPoolAddress('usdc')); // USDC pool address

For AI Agents

This SDK is designed to work with AI agent frameworks and external signers.

Non-Interactive CLI

Human-readable output is the CLI default. Use --json for machine-readable responses and --unsigned for transaction payload generation:

# Generate keypair as JSON (no prompts, no file save)
veil init --json

# Get unsigned transaction payloads for agent signing
SIGNER_ADDRESS=0x... veil register --unsigned
SIGNER_ADDRESS=0x... veil deposit ETH 0.1 --unsigned
veil deposit USDC 100 --unsigned --address 0x...    # Outputs approve + deposit payloads

# Request machine-readable status or balances
veil balance --json
veil balance --pool usdc --json
veil withdraw ETH 0.05 0xRecipient --json

SIGNER_ADDRESS can be used for address-only agent flows such as veil status, veil balance, and veil register --unsigned when the signer manages the wallet externally. WALLET_KEY and SIGNER_ADDRESS are mutually exclusive, and signed commands still require WALLET_KEY.

veil status shows a Signing row that reflects the active mode (local (WALLET_KEY), external (SIGNER_ADDRESS), or not configured), plus public ETH balance when it can be resolved.

Bankr Integration

Keypair Derivation via Bankr Sign API

Use Keypair.fromSigner() with Bankr's POST /agent/sign endpoint to derive the same keypair as the frontend:

import { Keypair } from '@veil-cash/sdk';

const keypair = await Keypair.fromSigner(async (message) => {
  const res = await fetch('https://api.bankr.bot/agent/sign', {
    method: 'POST',
    headers: { 'X-API-Key': BANKR_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ signatureType: 'personal_sign', message }),
  });
  return (await res.json()).signature;
});

Or via CLI (two-step):

# 1. Get signature from Bankr sign API
SIG=$(curl -s -X POST "https://api.bankr.bot/agent/sign" \
  -H "X-API-Key: $BANKR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"signatureType\":\"personal_sign\",\"message\":\"$(node -e "const{VEIL_SIGNED_MESSAGE}=require('@veil-cash/sdk');console.log(VEIL_SIGNED_MESSAGE)")\"}" \
  | jq -r '.signature')

# 2. Derive keypair from signature
veil init --signature $SIG

Unsigned Transaction Payloads

Use --unsigned to get signer-compatible transaction payloads:

SIGNER_ADDRESS=0x... veil register --unsigned
SIGNER_ADDRESS=0x... veil register --unsigned --force
SIGNER_ADDRESS=0x... veil deposit ETH 0.1 --unsigned
veil deposit ETH 0.1 --unsigned --address 0x...
# {"to":"0x...","data":"0x...","value":"100000000000000000","chainId":8453}

The --unsigned flag outputs a standard {to, data, value, chainId} payload compatible with any signer that accepts arbitrary transactions. When SIGNER_ADDRESS is set, veil register --unsigned and veil deposit --unsigned no longer require the --address flag. For veil register --unsigned --force, the CLI checks on-chain registration state first and emits changeDepositKey only when the address is already registered; otherwise it falls back to register.

Programmatic SDK Usage

import { Keypair, buildDepositETHTx, buildDepositTx, withdraw } from '@veil-cash/sdk';

// For deposits: build transaction, let agent sign via your signer
const keypair = new Keypair(veilKey);
const tx = buildDepositETHTx({
  depositKey: keypair.depositKey(),
  amount: '0.1',
});
// -> { to, data, value } - pass to your signer

// Generic builder works for any asset
const usdcTx = buildDepositTx({
  depositKey: keypair.depositKey(),
  amount: '100',
  token: 'USDC',
});

// For withdrawals: SDK handles ZK proofs, submits to relayer
const result = await withdraw({
  amount: '0.05',
  recipient: '0xRecipient',
  keypair,
  pool: 'eth', // 'eth' | 'usdc'
});
// -> { success, transactionHash, blockNumber }