npm package: @sahilw/dust-pool
Description: Dust Protocol DustPool V2 operations — deposits, withdrawals, transfers, splits, ZK proofs
Author: Dust Protocol
Latest version: 0.1.0
Deprecated: Moved to dust-pool (unscoped). Install: npm i dust-pool

# @dust/pool

[![npm version](https://img.shields.io/npm/v/@dust/pool)](https://www.npmjs.com/package/@dust/pool)

DustPool V2 operations for the Dust Protocol. Provides deposits, withdrawals, transfers, denomination splits, FFLONK zero-knowledge proof generation, note storage, view key management, and compliance screening.

DustPool V2 is a ZK-UTXO privacy pool using Poseidon commitments and a depth-20 Merkle tree. Amounts are hidden on-chain. Deposits and withdrawals are unlinkable.

## Installation

```bash
npm install @dust/pool
```

Circuit files (`.wasm`, `.zkey`) are not included in the package — download them separately from the GitHub Release (see [Circuit Artifacts](#circuit-artifacts) below).

## Circuit Artifacts

The ZK circuit files (~400MB total) are hosted as GitHub Release assets. You need these for proof generation.

### Automated Download

From the SDK monorepo root:

```bash
npm run circuits:download
```

Or run the script directly with a custom output directory:

```bash
./scripts/download-circuits.sh ./my-circuits
```

### Manual Download

Download from the [v0.1.0-circuits release](https://github.com/Dust-Protocol/dust-sdk/releases/tag/v0.1.0-circuits):

| File | Size | Purpose |
|------|------|---------|
| [`DustV2Transaction.wasm`](https://github.com/Dust-Protocol/dust-sdk/releases/download/v0.1.0-circuits/DustV2Transaction.wasm) | ~3 MB | WASM witness generator for deposits, withdrawals, transfers |
| [`DustV2Transaction.zkey`](https://github.com/Dust-Protocol/dust-sdk/releases/download/v0.1.0-circuits/DustV2Transaction.zkey) | ~112 MB | FFLONK proving key (12,420 constraints) |
| [`DustV2Split.wasm`](https://github.com/Dust-Protocol/dust-sdk/releases/download/v0.1.0-circuits/DustV2Split.wasm) | ~3 MB | WASM witness generator for denomination splits |
| [`DustV2Split.zkey`](https://github.com/Dust-Protocol/dust-sdk/releases/download/v0.1.0-circuits/DustV2Split.zkey) | ~283 MB | FFLONK proving key (32,074 constraints) |

### Configuring Proof Engine

Pass the local file paths to any operation that generates proofs:

```typescript
import { NodeProofEngine, withdraw, split } from '@dust/pool'

const proofEngine = new NodeProofEngine()

// Transaction circuit (withdraw, transfer)
const withdrawResult = await withdraw(
  {
    ...baseConfig,
    proofEngine,
    wasmPath: './circuits/DustV2Transaction.wasm',
    zkeyPath: './circuits/DustV2Transaction.zkey',
  },
  { amount: 50000000000000000n, recipient: '0x...' },
)

// Split circuit (denomination splits)
const splitResult = await split(
  {
    ...baseConfig,
    proofEngine,
    wasmPath: './circuits/DustV2Split.wasm',
    zkeyPath: './circuits/DustV2Split.zkey',
  },
  { amount: 150000000000000000n, token: 'ETH' },
)
```

## Quick Start

```typescript
import {
  deposit,
  withdraw,
  InMemoryStorage,
  NodeProofEngine,
  createPoolRelayer,
} from '@dust/pool'
import { fromRpcUrl, deriveV2Keys } from '@dust/core'

const provider = fromRpcUrl('https://sepolia.drpc.org')
const keys = await deriveV2Keys(walletSignature, '123456')
const storage = new InMemoryStorage()
const relayer = createPoolRelayer({ baseUrl: 'https://relayer.example.com' })
const proofEngine = new NodeProofEngine()

const config = {
  provider,
  storage,
  proofEngine,
  relayer,
  keys,
  walletAddress: '0xYourAddress',
  chainId: 11155111,
  wasmPath: './circuits/DustV2Transaction.wasm',
  zkeyPath: './circuits/DustV2Transaction.zkey',
}

// Deposit 0.1 ETH
const depositResult = await deposit(config, { amount: 100000000000000000n })
console.log('Committed:', depositResult.txHash)

// Withdraw 0.05 ETH
const withdrawResult = await withdraw(
  { storage, proofEngine, relayer, keys, walletAddress: '0xYourAddress', chainId: 11155111,
    wasmPath: './circuits/DustV2Transaction.wasm', zkeyPath: './circuits/DustV2Transaction.zkey' },
  { amount: 50000000000000000n, recipient: '0xRecipientAddress' },
)
console.log('Withdrawn:', withdrawResult.txHash)
```

## Deployed Contracts

| Chain | Chain ID | DustPool V2 |
|-------|----------|-------------|
| Ethereum Sepolia | 11155111 | `0x3cbf3459e7E0E9Fd2fd86a28c426CED2a60f023f` |
| Arbitrum Sepolia | 421614 | `0x07E961c0d881c1439be55e5157a3d92a3efE305d` |
| Base Sepolia | 84532 | `0x17f52f01ffcB6d3C376b2b789314808981cebb16` |

## API Reference

### Pool Operations

All operations share a similar structure: a `*Config` object containing infrastructure (provider, storage, proofEngine, relayer, keys), and a `*Params` object with operation-specific inputs.

---

#### `deposit(config: DepositConfig, params: DepositParams): Promise<DepositResult>`

Deposit ETH or an ERC-20 token into the privacy pool.

1. Computes the asset ID and owner public key
2. Creates a fresh note with a random blinding factor
3. Computes the Poseidon commitment
4. Sends the on-chain `deposit()` or `depositERC20()` transaction
5. Saves the note to local storage (status: pending)
6. Polls the relayer to confirm the leaf index

```typescript
import { deposit } from '@dust/pool'

// Deposit ETH
const result = await deposit(config, {
  amount: 100000000000000000n, // 0.1 ETH in wei
})

// Deposit ERC-20
const result = await deposit(config, {
  amount: 100000000n, // 100 USDC (6 decimals)
  tokenAddress: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
})

console.log(result.txHash)      // on-chain tx
console.log(result.commitment)  // bigint — Poseidon commitment
console.log(result.leafIndex)   // number — position in Merkle tree (-1 if not yet confirmed)
console.log(result.note)        // NoteV2 — the private note
```

```typescript
interface DepositConfig {
  provider: DustProvider
  storage: IStorageBackend
  proofEngine: IProofEngine
  relayer: PoolRelayer
  keys: V2Keys
  walletAddress: string
  chainId: number
  wasmPath: string
  zkeyPath: string
}

interface DepositParams {
  amount: bigint
  tokenAddress?: Address  // omit for native ETH
}

interface DepositResult {
  txHash: string
  commitment: bigint
  leafIndex: number
  note: NoteV2
}
```

---

#### `withdraw(config: WithdrawConfig, params: WithdrawParams): Promise<WithdrawResult>`

Withdraw ETH or tokens from the pool to an address.

1. Finds an unspent note with sufficient balance
2. Gets the Merkle proof from the relayer
3. Builds circuit inputs (with change note if `amount < note.amount`)
4. Generates a FFLONK proof via `NodeProofEngine`
5. Submits the proof to the relayer
6. Marks the input note as spent; saves the change note

```typescript
import { withdraw } from '@dust/pool'

const result = await withdraw(config, {
  amount: 50000000000000000n, // 0.05 ETH
  recipient: '0xRecipientAddress',
})

console.log(result.txHash)
console.log(result.changeNote) // NoteCommitmentV2 | null
```

```typescript
interface WithdrawParams {
  amount: bigint
  recipient: string
  tokenAddress?: string  // omit for native ETH
}

interface WithdrawResult {
  txHash: string
  blockNumber: number
  changeNote: NoteCommitmentV2 | null
}
```

---

#### `transfer(config: TransferConfig, params: TransferParams): Promise<TransferResult>`

Transfer value within the pool to another user (no on-chain withdrawal). The recipient receives a private note in the pool.

```typescript
import { transfer } from '@dust/pool'
import { computeOwnerPubKey } from '@dust/core'

const recipientKeys = await deriveV2Keys(recipientSignature, '000000')
const recipientOwner = await computeOwnerPubKey(recipientKeys.spendingKey)

const result = await transfer(config, {
  amount: 25000000000000000n, // 0.025 ETH
  recipientOwner,
})

console.log(result.txHash)
console.log(result.success)
console.log(result.changeNote)
```

```typescript
interface TransferParams {
  amount: bigint
  recipientOwner: bigint  // Poseidon(recipientSpendingKey)
}

interface TransferResult {
  txHash: string
  success: boolean
  changeNote: NoteCommitmentV2 | null
}
```

---

#### `split(config: SplitConfig, params: SplitParams): Promise<SplitResult>`

Split a note into denomination chunks using the 2-in-8-out split circuit. This mitigates amount fingerprinting by converting arbitrary amounts into standard denominations.

```typescript
import { split } from '@dust/pool'

const result = await split(
  {
    ...config,
    wasmPath: './circuits/DustV2Split.wasm',
    zkeyPath: './circuits/DustV2Split.zkey',
  },
  {
    amount: 150000000000000000n, // 0.15 ETH → chunks like [0.1, 0.05]
    token: 'ETH',
    maxChunks: 7,
  },
)

console.log(result.txHash)
console.log(result.chunks)       // bigint[] — the denomination amounts
console.log(result.outputNotes)  // SplitOutputNote[]
```

```typescript
interface SplitParams {
  amount: bigint
  token: string      // 'ETH' or 'USDC'
  maxChunks?: number // default 7 (leaves 1 slot for change)
}

interface SplitResult {
  txHash: string
  chunks: bigint[]
  outputNotes: SplitOutputNote[]
}
```

---

### Storage

The `IStorageBackend` interface abstracts note storage. Two implementations are provided.

#### `interface IStorageBackend`

```typescript
interface IStorageBackend {
  saveNote(note: StoredNoteV2): Promise<void>
  getNote(id: string): Promise<StoredNoteV2 | null>
  getNotes(filter: NoteFilter & { walletAddress: string }): Promise<StoredNoteV2[]>
  updateNote(id: string, updates: Partial<StoredNoteV2>): Promise<void>
  markSpent(id: string): Promise<void>
  deleteNote(id: string): Promise<void>
  clear(): Promise<void>
}
```

#### `InMemoryStorage`

In-memory storage. Notes are lost when the process exits. Useful for testing and short-lived scripts.

```typescript
import { InMemoryStorage } from '@dust/pool'

const storage = new InMemoryStorage()
```

#### `JsonFileStorage`

Persists notes to a JSON file on disk. Reads and writes synchronously.

```typescript
import { JsonFileStorage } from '@dust/pool'

const storage = new JsonFileStorage('./notes.json')
```

#### Storage utility functions

```typescript
import { bigintToHex, hexToBigint, storedToNoteCommitment, noteToStored, getBalance, getAllBalances } from '@dust/pool'

// Convert between storage format and runtime types
const nc = storedToNoteCommitment(storedNote)  // StoredNoteV2 → NoteCommitmentV2
const stored = noteToStored(nc, walletAddress) // NoteCommitmentV2 → StoredNoteV2

// Balance queries
const ethBal = await getBalance(storage, '0xYourAddress', 11155111, assetHex)
const all = await getAllBalances(storage, '0xYourAddress', 11155111)
// Map<assetHex, bigint>
```

---

### Storage Encryption

AES-256-GCM encryption for note payloads stored outside of trusted memory.

#### `deriveStorageKey(spendingKey: bigint): Promise<CryptoKey>`

Derive an AES-256-GCM key from the spending key via SHA-256.

#### `encryptNotePayload(payload: NotePayload, storageKey: CryptoKey): Promise<EncryptedPayload>`

Encrypt note fields.

#### `decryptNotePayload(encrypted: EncryptedPayload, storageKey: CryptoKey): Promise<NotePayload>`

Decrypt note fields.

```typescript
import { deriveStorageKey, encryptNotePayload, decryptNotePayload } from '@dust/pool'

const storageKey = await deriveStorageKey(keys.spendingKey)
const encrypted = await encryptNotePayload(
  { owner: '0x...', amount: '0x...', asset: '0x...', blinding: '0x...' },
  storageKey,
)
const decrypted = await decryptNotePayload(encrypted, storageKey)
```

---

### Proof Engine

#### `interface IProofEngine`

```typescript
interface IProofEngine {
  generateProof(
    inputs: ProofInputs | SplitProofInputs,
    circuitType: 'transaction' | 'split',
    wasmPath: string,
    zkeyPath: string,
  ): Promise<ProofResult>
}
```

#### `NodeProofEngine`

Generates FFLONK proofs using `snarkjs` directly. Suitable for Node.js servers, CLI tools, and agent processes. Not suitable for browsers (runs synchronously in the current thread).

```typescript
import { NodeProofEngine } from '@dust/pool'

const proofEngine = new NodeProofEngine()
const result = await proofEngine.generateProof(inputs, 'transaction', wasmPath, zkeyPath)
// result.proof           — raw proof object
// result.publicSignals   — string[] of 9 public signals
// result.proofCalldata   — '0x...' 768-byte FFLONK proof hex for on-chain submission
```

---

### Denominations

Tools for decomposing arbitrary amounts into standard denomination chunks to prevent amount fingerprinting.

#### `ETH_DENOMINATIONS: bigint[]`

ETH denomination table in wei: 100, 50, 20, 10, 5, 3, 2, 1, 0.5, 0.3, 0.2, 0.1, 0.05, 0.03, 0.02, 0.01 ETH.

#### `USDC_DENOMINATIONS: bigint[]`

USDC denomination table in base units (6 decimals): 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1 USDC.

#### `getDenominations(token: string): bigint[]`

Get the denomination table for `'ETH'` or `'USDC'`. Throws for unsupported tokens.

#### `decompose(amount: bigint, denominations: bigint[], maxChunks?: number): bigint[]`

Greedy largest-first decomposition. If the remainder is below the smallest denomination, it is appended as-is to preserve the full amount.

```typescript
import { decompose, ETH_DENOMINATIONS } from '@dust/pool'
import { parseEther } from 'viem'

const chunks = decompose(parseEther('0.15'), ETH_DENOMINATIONS)
// [100000000000000000n, 50000000000000000n] (0.1 + 0.05)
```

#### `decomposeForToken(amount: bigint, token: string): bigint[]`

Decompose using the token's denomination table.

#### `decomposeForSplit(amount: bigint, token: string, maxChunks?: number): bigint[]`

Decompose for the split circuit. Default `maxChunks` is 7, reserving one output slot for change.

#### `formatChunks(chunks: bigint[], token: string): string[]`

Format chunk amounts as human-readable decimal strings (`formatEther` for ETH, `formatUnits(c, 6)` for USDC).

#### `suggestRoundedAmounts(amount, token, maxSuggestions?): Array<{ amount: bigint; chunks: number; formatted: string }>`

Suggest nearby amounts that decompose into fewer chunks. Useful for user-facing UX that nudges toward privacy-optimal amounts.

```typescript
import { suggestRoundedAmounts } from '@dust/pool'
import { parseEther } from 'viem'

const suggestions = suggestRoundedAmounts(parseEther('0.13'), 'ETH')
// [{ amount: 100000000000000000n, chunks: 1, formatted: '0.1' }, ...]
```

---

### View Keys

View keys allow selective disclosure of pool activity without revealing the spending key.

#### `deriveViewKey(keys: V2Keys): Promise<ViewKey>`

Derive a view key from V2 keys. The view key contains `ownerPubKey` (allows matching notes) and `nullifierKey` (allows computing nullifiers).

```typescript
import { deriveViewKey, serializeViewKey, deserializeViewKey } from '@dust/pool'

const vk = await deriveViewKey(keys)
const serialized = serializeViewKey(vk)
// 'dvk1:<ownerPubKey_hex>:<nullifierKey_hex>'

const restored = deserializeViewKey(serialized)
```

#### `serializeViewKey(vk: ViewKey): string`

Encode as `dvk1:<ownerPubKey>:<nullifierKey>`.

#### `deserializeViewKey(encoded: string): ViewKey | ScopedViewKey`

Decode a `dvk1` or `dvk2` serialized key.

#### `serializeScopedViewKey(svk: ScopedViewKey): string`

Encode as `dvk2:<ownerPubKey>:<nullifierKey>:<startBlock>:<endBlock>`.

#### `deserializeScopedViewKey(encoded: string): ScopedViewKey`

Decode a `dvk2` scoped key.

#### `isScopedViewKey(vk: ViewKey | ScopedViewKey): vk is ScopedViewKey`

Type guard.

---

### Compliance

#### `checkDepositorCompliance(provider, depositor, poolAddress): Promise<ComplianceStatus>`

Check if a depositor address passes the pool's compliance oracle. Reads the oracle address from the contract; if oracle is `address(0)`, returns `'no-screening'`.

```typescript
import { checkDepositorCompliance } from '@dust/pool'

const status = await checkDepositorCompliance(provider, depositor, poolAddress)
if (status.status === 'blocked') {
  console.error('Blocked:', status.reason)
} else if (status.status === 'clear') {
  // proceed with deposit
}
```

```typescript
type ComplianceStatus =
  | { status: 'no-screening' }
  | { status: 'clear' }
  | { status: 'blocked'; reason: string }
  | { status: 'error'; reason: string }
```

#### `checkCooldownStatus(provider, commitment, poolAddress): Promise<CooldownStatus | null>`

Check if a commitment is in the 1-hour compliance cooldown period. During cooldown, withdrawals are restricted to the original depositor address.

```typescript
interface CooldownStatus {
  inCooldown: boolean
  originator: Address
  remainingSeconds: number
}
```

---

### Pool Relayer

The relayer maintains the off-chain Merkle tree, processes proof submissions, and submits on-chain transactions on behalf of users.

#### `createPoolRelayer(config: PoolRelayerConfig): PoolRelayer`

Create a relayer client.

```typescript
import { createPoolRelayer } from '@dust/pool'

const relayer = createPoolRelayer({
  baseUrl: 'https://relayer.example.com',
  chainId: 11155111, // optional default chain
})

const root = await relayer.getTreeRoot(11155111)
const proof = await relayer.getMerkleProof(leafIndex, 11155111)
```

#### `PoolRelayer` interface

```typescript
interface PoolRelayer {
  // Merkle tree
  getTreeRoot(chainId?: number): Promise<bigint>
  getMerkleProof(leafIndex: number, chainId?: number): Promise<MerkleProof>

  // Proof submission
  submitWithdrawal(proofCalldata, publicSignals, targetChainId, tokenAddress): Promise<RelayerWithdrawalResult>
  submitBatchWithdrawal(proofs, targetChainId): Promise<BatchWithdrawalResult>
  submitBatchSwap(swaps, targetChainId): Promise<BatchSwapResult>
  submitSplitWithdrawal(proofCalldata, publicSignals, targetChainId, tokenAddress): Promise<RelayerWithdrawalResult>
  submitTransfer(proofCalldata, publicSignals, targetChainId): Promise<RelayerTransferResult>

  // Status
  getDepositStatus(commitment: string, chainId?: number): Promise<DepositStatus>

  // Compliance
  getComplianceWitness(commitment: bigint, chainId?: number): Promise<ComplianceWitness>
  submitComplianceProof(proof, exclusionRoot, nullifier, targetChainId): Promise<{ txHash: string; verified: boolean }>
}
```

```typescript
interface MerkleProof {
  pathElements: bigint[]
  pathIndices: number[]
}

interface RelayerWithdrawalResult {
  txHash: string
  blockNumber: number
  gasUsed: string
  fee: string
}

interface DepositStatus {
  confirmed: boolean
  leafIndex: number
}
```

---

### Contract Utilities

#### `DUST_POOL_V2_ABI`

The complete typed ABI for DustPool V2 — deposit, depositERC20, withdraw, withdrawSplit, batchDeposit, compliance, and cooldown functions, plus DepositQueued, Withdrawal, and RootUpdated events.

#### `getDustPoolV2Address(chainId: number): Address | null`

Get the DustPool V2 contract address for a chain. Returns `null` if not deployed.

#### `getDustPoolV2Config(chainId: number): { address: Address; abi: typeof DUST_POOL_V2_ABI } | null`

Get address and ABI together for use with viem `readContract` / `writeContract`.

```typescript
import { getDustPoolV2Config } from '@dust/pool'

const config = getDustPoolV2Config(11155111)
if (config) {
  const totalDeposited = await publicClient.readContract({
    ...config,
    functionName: 'totalDeposited',
    args: ['0x0000000000000000000000000000000000000000'],
  })
}
```

---

### Proof Inputs (Advanced)

These functions build the raw circuit inputs. You do not need to call them directly when using `deposit`, `withdraw`, `transfer`, or `split` — those functions call them internally.

```typescript
import {
  buildDepositInputs,
  buildWithdrawInputs,
  buildSwapInputs,
  buildTransferInputs,
  buildSplitInputs,
} from '@dust/pool'
```

## Types

```typescript
type CircuitType = 'transaction' | 'split'

interface NotePayload {
  amount: string
  blinding: string
}

interface EncryptedPayload {
  iv: string
  ciphertext: string
}

interface SplitOutputNote {
  owner: bigint
  amount: bigint
  asset: bigint
  blinding: bigint
  commitment: bigint
}

interface SplitBuildResult {
  circuitInputs: SplitProofInputs
  outputNotes: SplitOutputNote[]
}
```

## License

MIT