From 7dcd5826788b088fd625e10b2180f3020e896f68 Mon Sep 17 00:00:00 2001 From: andresdefi Date: Sun, 15 Mar 2026 00:44:51 +0100 Subject: [PATCH] feat: add zerodust skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ZeroDust chain exit infrastructure — sweep 100% of native gas tokens from 25 EVM chains via EIP-7702 sponsored execution, leaving exactly zero balance. Includes SDK guide, REST API reference, MCP server integration, Agent API with batch operations, contract addresses for all 25 chains, error codes, troubleshooting, and runnable starter template. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude-plugin/marketplace.json | 6 + skills/zerodust/SKILL.md | 457 ++++++++++++++++++ skills/zerodust/docs/troubleshooting.md | 126 +++++ skills/zerodust/examples/agent-api/README.md | 106 ++++ .../zerodust/examples/batch-sweep/README.md | 69 +++ .../examples/check-balances/README.md | 59 +++ .../examples/sweep-single-chain/README.md | 72 +++ .../zerodust/resources/contract-addresses.md | 49 ++ skills/zerodust/resources/error-codes.md | 70 +++ skills/zerodust/templates/zerodust-client.ts | 175 +++++++ 10 files changed, 1189 insertions(+) create mode 100644 skills/zerodust/SKILL.md create mode 100644 skills/zerodust/docs/troubleshooting.md create mode 100644 skills/zerodust/examples/agent-api/README.md create mode 100644 skills/zerodust/examples/batch-sweep/README.md create mode 100644 skills/zerodust/examples/check-balances/README.md create mode 100644 skills/zerodust/examples/sweep-single-chain/README.md create mode 100644 skills/zerodust/resources/contract-addresses.md create mode 100644 skills/zerodust/resources/error-codes.md create mode 100644 skills/zerodust/templates/zerodust-client.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5d82c4f..fdfff21 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -585,6 +585,12 @@ "description": "HTTP 402 payment protocol for AI agent commerce — three-actor model (Client, Resource Server, Facilitator), ERC-3009 transferWithAuthorization, server middleware (@x402/express), client patterns in TypeScript and Python, facilitator integration, agent-to-agent payments, pricing strategies, and replay protection. Works on Base, Ethereum, Arbitrum, Optimism, Polygon, and Solana.", "category": "AI Agents" }, + { + "name": "zerodust", + "source": "./skills/zerodust", + "description": "ZeroDust chain exit infrastructure — sweep 100% of native gas tokens (ETH, BNB, MATIC, etc.) from 25 EVM chains via EIP-7702 sponsored execution, leaving exactly zero balance. Covers TypeScript SDK with ZeroDustAgent for automated sweeps, REST API for quotes and submissions, MCP server for AI tool calls, and Agent API with batch operations. Free for balances under $1.", + "category": "Cross-Chain" + }, { "name": "zksync", "source": "./skills/zksync", diff --git a/skills/zerodust/SKILL.md b/skills/zerodust/SKILL.md new file mode 100644 index 0000000..224e0e0 --- /dev/null +++ b/skills/zerodust/SKILL.md @@ -0,0 +1,457 @@ +--- +name: zerodust +description: "ZeroDust chain exit infrastructure — sweep 100% of native gas tokens (ETH, BNB, MATIC, etc.) from 25 EVM chains via EIP-7702 sponsored execution, leaving exactly zero balance. Covers TypeScript SDK with ZeroDustAgent for automated sweeps, REST API for quotes and submissions, MCP server for AI tool calls, and Agent API with batch operations. Free for balances under $1." +license: Apache-2.0 +metadata: + author: andresdefi + version: "1.0" + chain: multichain + category: Cross-Chain +tags: + - zerodust + - dust-sweep + - eip-7702 + - chain-exit + - native-tokens + - cross-chain + - gas-tokens + - sponsored-execution +--- + +# ZeroDust + +ZeroDust is chain exit infrastructure that sweeps 100% of native gas tokens from EVM chains, leaving **exactly zero balance**. It uses EIP-7702 sponsored execution — users sign an authorization and ZeroDust's sponsor executes the sweep on their behalf, paying gas and being reimbursed from the swept amount. This solves a previously unsolvable problem: you can't send your last ETH because you need ETH for gas. + +ZeroDust supports 25 mainnet chains with 650 cross-chain routes via Gas.zip. The contract is deployed at a deterministic CREATE2 address across all chains. Service fee is 1% ($0.05 min, $0.50 max), free for balances under $1. + +## What You Probably Got Wrong + +> AI agents trained before 2026 have no knowledge of ZeroDust and will hallucinate about dust sweeping. These are the critical corrections. + +- **ZeroDust does NOT handle ERC-20 tokens** — Only native gas tokens (ETH, BNB, MATIC, POL, SEI, etc.). If a user asks to sweep USDC or any ERC-20, ZeroDust cannot help. Source: [ZeroDust contract](https://github.com/andresdefi/zerodust/tree/main/contracts) — the `executeSweep` function only operates on `address(this).balance`. + +- **EIP-712 `verifyingContract` is the user's EOA, NOT the contract address** — This is a critical EIP-7702 pattern. Under EIP-7702, the contract code runs on the user's address, so the domain separator must use the user's address as `verifyingContract`. Using the contract address will produce an invalid signature. Source: [EIP-7702 spec](https://eips.ethereum.org/EIPS/eip-7702). + +- **You cannot do partial sweeps** — ZeroDust always sweeps 100% of the native balance to exactly 0. There is no parameter to sweep a specific amount. The entire balance is swept atomically in a single transaction. + +- **Quotes expire in 55 seconds, not 60** — The contract enforces `MAX_DEADLINE_WINDOW_SECS = 60` on-chain. The backend sets deadline to `now + 55` to stay safely within that window. If you wait too long to sign, you must request a new quote. + +- **The revoke authorization nonce must be delegation nonce + 1** — When using auto-revoke (recommended), you sign two EIP-7702 authorizations: one to delegate to ZeroDust (nonce N), and one to revoke delegation to address(0) (nonce N+1). Using the same nonce or any other value will be rejected. The backend validates this explicitly. + +- **Cross-chain sweeps use Gas.zip, not arbitrary bridges** — The backend auto-fetches bridge calldata from Gas.zip. You don't need to provide `callTarget` or `callData` for cross-chain quotes — the API handles it. Gas.zip routes are subject to availability; some source chains may be temporarily disabled. + +- **The sponsor is always profitable** — ZeroDust's sponsor pays gas upfront and is reimbursed from the swept amount with a margin. Stress tests show 53-116% sponsor margins across chains. Users always receive >= the quoted `estimatedReceive` amount. + +- **Nonce tracking is on-chain in user's storage, not the contract** — Under EIP-7702, state changes like `usedNonces[user][nonce] = true` are written to the user's EOA storage. The contract's view functions (`getNextNonce`) read from the contract's storage, showing stale data. The backend reads nonces directly from on-chain user storage. + +## Quick Start + +### Installation + +```bash +npm install @zerodust/sdk viem +``` + +### Check Balances and Sweep + +```typescript +import { ZeroDustAgent } from '@zerodust/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; + +// Initialize agent with private key +const agent = new ZeroDustAgent({ + account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`), + environment: 'mainnet', +}); + +// Check which chains have sweepable dust +const sweepable = await agent.getSweepableBalances(); +console.log('Sweepable chains:', sweepable.map(b => `${b.name}: ${b.balanceFormatted} ${b.nativeToken}`)); + +// Sweep Arbitrum -> Base +const result = await agent.sweep({ + fromChainId: 42161, // Arbitrum + toChainId: 8453, // Base +}); + +if (result.success) { + console.log('Swept! TX:', result.txHash); +} else { + console.log('Failed:', result.error); +} +``` + +### REST API Quick Start + +```bash +# Check balances across all chains +curl "https://api.zerodust.xyz/balances/0xYOUR_ADDRESS?testnet=false" + +# Get a sweep quote (Arbitrum -> Base) +curl "https://api.zerodust.xyz/quote?fromChainId=42161&toChainId=8453&userAddress=0x...&destination=0x..." +``` + +## Core Concepts + +### EIP-7702 Sponsored Execution + +ZeroDust uses EIP-7702 to temporarily delegate a user's EOA to the ZeroDust contract. This enables the sponsor (relayer) to execute the sweep on the user's behalf. The user never pays gas directly. + +The flow: +1. User signs an EIP-7702 authorization (delegates EOA to ZeroDust contract) +2. User signs an EIP-712 SweepIntent (authorizes the specific sweep parameters) +3. User optionally signs a revoke authorization (removes delegation after sweep) +4. Sponsor submits the transaction, paying gas +5. Contract atomically: sweeps user balance -> reimburses sponsor -> sends remainder to destination +6. Sponsor executes revoke transaction (user's EOA returns to normal) + +``` +User signs 2 messages (batch auth + sweep intent) + | + v +Sponsor validates -> simulates -> executes + | + v +Contract: sweep user -> reimburse sponsor -> send to destination + | + v +Sponsor: revoke delegation (user's EOA is normal again) +``` + +### Sweep Flow (Quote -> Sign -> Submit -> Execute) + +Every sweep follows this sequence: + +```typescript +// 1. GET /quote - Get fee breakdown and signing parameters +const quote = await fetch('/quote?fromChainId=42161&toChainId=8453&userAddress=0x...&destination=0x...'); + +// 2. POST /authorization - Get EIP-712 typed data for signing +const auth = await fetch('/authorization', { body: { quoteId: quote.quoteId } }); + +// 3. User signs: +// a. EIP-7702 delegation authorization +// b. EIP-712 SweepIntent typed data +// c. EIP-7702 revoke authorization (nonce = delegation nonce + 1) + +// 4. POST /sweep - Submit all signatures +const sweep = await fetch('/sweep', { + body: { + quoteId: quote.quoteId, + signature: eip712Signature, + eip7702Authorization: delegationAuth, + revokeAuthorization: revokeAuth, + } +}); + +// 5. GET /sweep/:id - Poll for completion +// Status: pending -> simulating -> executing -> broadcasted -> completed +``` + +### Cross-Chain via Gas.zip + +Cross-chain sweeps use Gas.zip as the bridge. When `fromChainId !== toChainId`: + +- The API automatically fetches Gas.zip calldata during the quote step +- Mode is set to `MODE_CALL` (1) instead of `MODE_TRANSFER` (0) +- The contract calls the Gas.zip deposit address with the bridge calldata +- `routeHash = keccak256(callData)` binds the signature to the specific bridge route +- 650 cross-chain routes available (25 x 25, minus same-chain) +- Bridge latency is typically ~5 seconds + +### Fee Structure + +| Component | Description | +|-----------|-------------| +| **Service Fee** | 1% of balance ($0.05 min, $0.50 max). Free under $1. | +| **Gas Reimbursement** | Actual gas cost + 20% buffer. Sponsor keeps the margin. | +| **Bridge Fee** | Near-zero Gas.zip fee (cross-chain only) | +| **Revoke Gas** | ~50k gas units for auto-revoke tx, included in fees | + +```typescript +// Fee calculation +const serviceFee = balance < $1 ? 0 : clamp(balance * 0.01, $0.05, $0.50); +const gasCost = (overheadGas + measuredGas) * gasPrice * 1.20; // 20% buffer +const totalFee = gasCost + serviceFee + revokeGasCost; +const userReceives = balance - totalFee; // Always >= estimatedReceive +``` + +### Agent API + +For AI agents, dedicated endpoints reduce round trips: + +```bash +# Register for API key (300/min, 1000/day limits) +POST /agent/register { "name": "My Agent" } +# Returns: { "apiKey": "zd_..." } + +# Combined quote + auth data in one call +POST /agent/sweep { "fromChainId": 42161, "toChainId": 8453, "userAddress": "0x..." } + +# Batch sweep multiple chains +POST /agent/batch-sweep { "sweeps": [...], "destination": "0x...", "consolidateToChainId": 8453 } + +# Check usage stats +GET /agent/me +``` + +All agent endpoints require `Authorization: Bearer ` or `X-API-Key: `. + +### MCP Server + +ZeroDust exposes an MCP server at `https://api.zerodust.xyz/mcp` (JSON-RPC 2.0, MCP version 2024-11-05): + +| Tool | Description | +|------|-------------| +| `check_balances` | Check native balances across all 25 chains | +| `get_sweep_quote` | Get quote with fee breakdown | +| `get_supported_chains` | List supported chains | +| `get_service_info` | Pricing, features, integration info | + +## Contract Addresses + +**Mainnet (CREATE2 deterministic):** `0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2` + +Same address on all 25 chains. Last verified: January 28, 2026 via block explorer source verification. + +| Chain | Chain ID | Explorer | +|-------|----------|----------| +| Ethereum | 1 | [etherscan.io](https://etherscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Optimism | 10 | [optimistic.etherscan.io](https://optimistic.etherscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| BSC | 56 | [bscscan.com](https://bscscan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Gnosis | 100 | [gnosisscan.io](https://gnosisscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Unichain | 130 | [uniscan.xyz](https://uniscan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Polygon | 137 | [polygonscan.com](https://polygonscan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Sonic | 146 | [sonicscan.org](https://sonicscan.org/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| X Layer | 196 | [oklink.com/xlayer](https://www.oklink.com/xlayer/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Fraxtal | 252 | [fraxscan.com](https://fraxscan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| World Chain | 480 | [worldscan.org](https://worldscan.org/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Sei | 1329 | [seitrace.com](https://seitrace.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Story | 1514 | [storyscan.xyz](https://storyscan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Soneium | 1868 | [soneium.blockscout.com](https://soneium.blockscout.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Mantle | 5000 | [mantlescan.xyz](https://mantlescan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Superseed | 5330 | [superseed.explorer.caldera.xyz](https://superseed.explorer.caldera.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Base | 8453 | [basescan.org](https://basescan.org/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Plasma | 9745 | [plasma-explorer.genesys.network](https://plasma-explorer.genesys.network/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Mode | 34443 | [modescan.io](https://modescan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Arbitrum | 42161 | [arbiscan.io](https://arbiscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Celo | 42220 | [celoscan.io](https://celoscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Ink | 57073 | [inkscan.xyz](https://inkscan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| BOB | 60808 | [explorer.gobob.xyz](https://explorer.gobob.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Berachain | 80094 | [berascan.com](https://berascan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Scroll | 534352 | [scrollscan.com](https://scrollscan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Zora | 7777777 | [zorascan.xyz](https://zorascan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | + +## Common Patterns + +### SDK: Sweep All Chains to Base + +```typescript +import { ZeroDustAgent } from '@zerodust/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; + +const agent = new ZeroDustAgent({ + account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`), + environment: 'mainnet', +}); + +// Discover and sweep all chains with dust +const results = await agent.sweepAll({ + toChainId: 8453, // Consolidate everything to Base + continueOnError: true, +}); + +console.log(`Swept ${results.successful}/${results.total} chains`); +for (const r of results.results) { + if (r.success) { + console.log(` ${r.fromChainId} -> ${r.toChainId}: TX ${r.txHash}`); + } else { + console.log(` ${r.fromChainId}: FAILED - ${r.error}`); + } +} +``` + +### REST API: Full Sweep Flow + +```typescript +const BASE_URL = 'https://api.zerodust.xyz'; + +// 1. Get quote +const quoteRes = await fetch( + `${BASE_URL}/quote?fromChainId=42161&toChainId=8453&userAddress=${address}&destination=${address}` +); +const quote = await quoteRes.json(); + +// 2. Get typed data +const authRes = await fetch(`${BASE_URL}/authorization`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ quoteId: quote.quoteId }), +}); +const { typedData, contractAddress } = await authRes.json(); + +// 3. Sign (using viem wallet client) +const signature = await walletClient.signTypedData({ + domain: typedData.domain, + types: typedData.types, + primaryType: typedData.primaryType, + message: typedData.message, +}); + +const delegationAuth = await walletClient.signAuthorization({ + contractAddress, + chainId: 42161, +}); + +const revokeAuth = await walletClient.signAuthorization({ + contractAddress: '0x0000000000000000000000000000000000000000', + chainId: 42161, + nonce: delegationAuth.nonce + 1, +}); + +// 4. Submit +const sweepRes = await fetch(`${BASE_URL}/sweep`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + quoteId: quote.quoteId, + signature, + eip7702Authorization: { + chainId: delegationAuth.chainId, + contractAddress: delegationAuth.address, + nonce: Number(delegationAuth.nonce), + yParity: delegationAuth.yParity ?? 0, + r: delegationAuth.r, + s: delegationAuth.s, + }, + revokeAuthorization: { + chainId: revokeAuth.chainId, + contractAddress: revokeAuth.address, + nonce: Number(revokeAuth.nonce), + yParity: revokeAuth.yParity ?? 0, + r: revokeAuth.r, + s: revokeAuth.s, + }, + }), +}); +const sweep = await sweepRes.json(); + +// 5. Poll for completion +let status; +do { + await new Promise(r => setTimeout(r, 3000)); + const statusRes = await fetch(`${BASE_URL}/sweep/${sweep.sweepId}`); + status = await statusRes.json(); +} while (!['completed', 'failed'].includes(status.status)); +``` + +### MCP: Check Balances via JSON-RPC + +```bash +curl -X POST https://api.zerodust.xyz/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "check_balances", + "arguments": { "address": "0x1234567890abcdef1234567890abcdef12345678" } + } + }' +``` + +## Error Handling + +```typescript +import { + ZeroDustError, + BalanceTooLowError, + QuoteExpiredError, + NetworkError, + ChainNotSupportedError, + BridgeError, +} from '@zerodust/sdk'; + +try { + const result = await agent.sweep({ fromChainId: 42161, toChainId: 8453 }); +} catch (e) { + if (e instanceof BalanceTooLowError) { + console.log('Balance too low to sweep on this chain'); + } else if (e instanceof QuoteExpiredError) { + console.log('Quote expired, retrying...'); + // Re-fetch quote and try again + } else if (e instanceof BridgeError) { + console.log('Cross-chain route unavailable, try same-chain sweep'); + } else if (e instanceof ChainNotSupportedError) { + console.log('Chain does not support EIP-7702'); + } else if (e instanceof NetworkError) { + console.log('Network issue, retry with backoff'); + } else if (e instanceof ZeroDustError) { + console.log(`ZeroDust error [${e.code}]: ${e.message}`); + } +} +``` + +See [resources/error-codes.md](resources/error-codes.md) for the complete error code reference. + +## Security and Best Practices + +1. **Never log or expose private keys** — Use environment variables or KMS. The SDK's `createAgentFromPrivateKey` accepts a hex string; never hardcode it. + +2. **Validate addresses before calling the API** — Use viem's `isAddress()` to validate user inputs. The API will reject invalid addresses but client-side validation improves UX. + +3. **Handle quote expiry gracefully** — Quotes are valid for 55 seconds. If signing takes longer, catch `QuoteExpiredError` and re-fetch. Don't cache quotes. + +4. **Use auto-revoke** — Always include `revokeAuthorization` in sweep submissions. This removes the EIP-7702 delegation after the sweep, returning the user's EOA to normal state. + +5. **Respect rate limits** — Agent API: 300 requests/minute, 1000 sweeps/day. Use `GET /agent/me` to check remaining quota. Back off on 429 responses. + +6. **Check `canSweep` before quoting** — The balance endpoint returns `canSweep: boolean` for each chain. Don't request quotes for chains where `canSweep` is false. + +7. **Handle cross-chain failures** — Gas.zip may temporarily disable routes. Catch `SOURCE_CHAIN_DISABLED` / `DEST_CHAIN_DISABLED` errors and fall back to same-chain sweeps or different destinations. + +8. **Don't assume all chains support EIP-7702** — Only 25 mainnet chains are supported. Check `GET /chains` for the current list. Chains like zkSync, Avalanche, and others do not support EIP-7702. + +## Skill Structure + +``` +skills/zerodust/ +├── SKILL.md # This file +├── docs/ +│ └── troubleshooting.md # Common issues and solutions +├── examples/ +│ ├── check-balances/README.md # Balance checking patterns +│ ├── sweep-single-chain/README.md # Single chain sweep flow +│ ├── batch-sweep/README.md # Multi-chain batch sweeps +│ └── agent-api/README.md # Agent API with API keys +├── resources/ +│ ├── contract-addresses.md # All 25 chain addresses +│ └── error-codes.md # Complete error reference +└── templates/ + └── zerodust-client.ts # Runnable starter template +``` + +## Guidelines + +Use this skill when: +- User mentions "sweep", "dust", "exit chain", "consolidate", "clean up wallet" +- User has small native token balances scattered across EVM chains +- User wants to fully exit a blockchain (balance to exactly 0) +- Agent needs to consolidate funds from multiple chains to one +- User asks about EIP-7702 sponsored execution for dust collection + +Do NOT use this skill when: +- User wants to swap or transfer ERC-20 tokens (use Uniswap, 1inch, etc.) +- User wants partial transfers (ZeroDust only does full balance sweeps) +- User is on chains that don't support EIP-7702 (zkSync, Avalanche, etc.) + +## References + +- **API Docs (Swagger)**: https://api.zerodust.xyz/docs +- **MCP Server**: https://api.zerodust.xyz/mcp +- **GitHub**: https://github.com/andresdefi/zerodust +- **SDK**: `npm install @zerodust/sdk` +- **ERC-8004 Agent**: https://www.8004scan.io/agents/base/1435 +- **A2A Agent Card**: https://api.zerodust.xyz/.well-known/agent-card.json +- **Website**: https://zerodust.xyz diff --git a/skills/zerodust/docs/troubleshooting.md b/skills/zerodust/docs/troubleshooting.md new file mode 100644 index 0000000..15e882a --- /dev/null +++ b/skills/zerodust/docs/troubleshooting.md @@ -0,0 +1,126 @@ +# ZeroDust Troubleshooting + +## 1. "Quote expired" (QUOTE_EXPIRED) + +**Symptoms:** POST /sweep returns `{ "error": "Quote expired", "code": "QUOTE_EXPIRED" }`. + +**Cause:** Quotes are valid for 55 seconds. The contract enforces a 60-second deadline window, and the backend uses 55 to provide a safety margin. If signing takes longer than 55 seconds, the quote becomes invalid. + +**Solution:** +- Request a new quote with `GET /quote` +- Sign immediately after receiving the quote +- For SDK users, the `agent.sweep()` method handles the full flow automatically + +**Debug checklist:** +- [ ] Check system clock is synchronized (NTP) +- [ ] Ensure signing flow completes within 55 seconds +- [ ] Don't cache quotes — always request fresh ones + +--- + +## 2. "Balance too low" (BALANCE_TOO_LOW) + +**Symptoms:** `GET /quote` returns `{ "error": "Balance too low...", "code": "BALANCE_TOO_LOW" }`. + +**Cause:** Each chain has a minimum sweepable balance that covers gas fees. If the balance is below this threshold, the sweep would result in a negative user receive amount. + +**Solution:** +- Check `canSweep` field from `GET /balances/:address` before requesting a quote +- Use `getSweepableBalances()` in the SDK to filter automatically +- Very small dust (< $0.01) may not be sweepable — this is expected + +--- + +## 3. "EIP-7702 not supported" / Chain not in list + +**Symptoms:** Chain ID returns 404 from `/chains/:chainId` or is not in the supported chains list. + +**Cause:** Not all EVM chains support EIP-7702. 41 chains have been tested and confirmed as incompatible, including zkSync, Avalanche, Blast, Abstract, Lens, and others. + +**Solution:** +- Check `GET /chains?testnet=false` for current supported chains +- Only 25 mainnet chains support EIP-7702 sweeps +- If a chain was recently added, ensure the backend has been updated + +--- + +## 4. "Revoke nonce mismatch" (REVOKE_NONCE_MISMATCH) + +**Symptoms:** POST /sweep returns `{ "error": "Revoke authorization nonce must be N", "code": "REVOKE_NONCE_MISMATCH" }`. + +**Cause:** The revoke authorization must have nonce = delegation nonce + 1. This is because the delegation transaction increments the account nonce, so the revoke transaction (executed after) needs the next nonce. + +**Solution:** +```typescript +// Correct: revoke nonce = delegation nonce + 1 +const delegationAuth = await walletClient.signAuthorization({ + contractAddress: ZERODUST_CONTRACT, + chainId: 42161, +}); + +const revokeAuth = await walletClient.signAuthorization({ + contractAddress: '0x0000000000000000000000000000000000000000', + chainId: 42161, + nonce: delegationAuth.nonce + 1, // Must be +1 +}); +``` + +--- + +## 5. "Invalid signature" (INVALID_SIGNATURE / EIP7702_INVALID_SIGNATURE) + +**Symptoms:** POST /sweep returns signature-related error. + +**Cause:** Most commonly, the EIP-712 typed data was modified or the `verifyingContract` in the domain is wrong. Under EIP-7702, the `verifyingContract` must be the **user's EOA address**, not the ZeroDust contract address. + +**Solution:** +- Use the `typedData` returned by `POST /authorization` exactly as-is +- Do not modify any field in the typed data before signing +- Ensure the wallet is signing with the correct account (matches `userAddress` from the quote) +- For EIP-7702 auth: ensure `contractAddress` matches the ZeroDust contract + +**Debug checklist:** +- [ ] `typedData.domain.verifyingContract` === user's EOA address +- [ ] Signing account matches the `userAddress` used in the quote +- [ ] Signature is 65 bytes (130 hex chars + 0x prefix) + +--- + +## 6. "Cross-chain unavailable" (SOURCE_CHAIN_DISABLED / DEST_CHAIN_DISABLED) + +**Symptoms:** Cross-chain quote fails with chain disabled error. + +**Cause:** Gas.zip may temporarily disable certain source or destination chains. This is outside ZeroDust's control. + +**Solution:** +- Fall back to same-chain sweep (`toChainId === fromChainId`) +- Try a different destination chain +- Check Gas.zip status for route availability +- The error message includes the specific chain name + +--- + +## 7. "Rate limited" (429 Too Many Requests) + +**Symptoms:** API returns HTTP 429. + +**Cause:** Agent API has per-minute (300) and daily (1000) rate limits. Public endpoints also have rate limits. + +**Solution:** +- Check remaining quota: `GET /agent/me` returns `rateLimits.dailyRemaining` +- Implement exponential backoff on 429 responses +- For batch operations, use `POST /agent/batch-sweep` instead of individual calls +- Contact team for higher limits if needed for production integration + +--- + +## 8. "Insufficient for fees" (INSUFFICIENT_FOR_FEES) + +**Symptoms:** Quote returns `INSUFFICIENT_FOR_FEES` even though balance shows a non-zero amount. + +**Cause:** The balance exists but is less than the total fees (gas + service fee + revoke gas). Cross-chain sweeps have higher overhead than same-chain due to bridge gas. + +**Solution:** +- Try a same-chain sweep instead (lower fees) +- Wait for gas prices to decrease on the source chain +- Very small balances (< $0.10) may not be sweepable on expensive chains like Ethereum mainnet diff --git a/skills/zerodust/examples/agent-api/README.md b/skills/zerodust/examples/agent-api/README.md new file mode 100644 index 0000000..f3a607f --- /dev/null +++ b/skills/zerodust/examples/agent-api/README.md @@ -0,0 +1,106 @@ +# Agent API (REST) + +Use the Agent API for server-side integrations or when not using the TypeScript SDK. + +## Register for an API Key + +```bash +curl -X POST https://api.zerodust.xyz/agent/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "My Sweep Agent", + "agentId": "agent-001", + "contactEmail": "dev@example.com" + }' +``` + +Response: +```json +{ + "apiKey": "zd_abc123...", + "keyPrefix": "zd_abc", + "keyId": "uuid", + "keyType": "agent", + "rateLimits": { "perMinute": 300, "daily": 1000 }, + "message": "IMPORTANT: Save your API key now - it will not be shown again!" +} +``` + +## Single Sweep (Combined Quote + Auth) + +```bash +curl -X POST https://api.zerodust.xyz/agent/sweep \ + -H "Authorization: Bearer zd_abc123..." \ + -H "Content-Type: application/json" \ + -d '{ + "fromChainId": 42161, + "toChainId": 8453, + "userAddress": "0x1234...", + "destination": "0x1234..." + }' +``` + +Returns quote, typed data, and EIP-7702 parameters in a single response. The agent still needs to sign and submit via `POST /sweep`. + +## Batch Sweep + +```bash +curl -X POST https://api.zerodust.xyz/agent/batch-sweep \ + -H "Authorization: Bearer zd_abc123..." \ + -H "Content-Type: application/json" \ + -d '{ + "sweeps": [ + { "fromChainId": 42161 }, + { "fromChainId": 10 }, + { "fromChainId": 137 } + ], + "destination": "0x1234...", + "consolidateToChainId": 8453 + }' +``` + +## Check Usage Stats + +```bash +curl https://api.zerodust.xyz/agent/me \ + -H "Authorization: Bearer zd_abc123..." +``` + +Response: +```json +{ + "keyId": "uuid", + "keyType": "agent", + "rateLimits": { + "perMinute": 300, + "daily": 1000, + "dailyUsed": 42, + "dailyRemaining": 958 + } +} +``` + +## TypeScript Client + +```typescript +const API_KEY = process.env.ZERODUST_API_KEY; +const BASE_URL = 'https://api.zerodust.xyz'; + +async function agentSweep(fromChainId: number, toChainId: number, userAddress: string) { + const res = await fetch(`${BASE_URL}/agent/sweep`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ fromChainId, toChainId, userAddress }), + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(`Agent sweep failed: ${err.error} (${err.code})`); + } + + return res.json(); +} +``` diff --git a/skills/zerodust/examples/batch-sweep/README.md b/skills/zerodust/examples/batch-sweep/README.md new file mode 100644 index 0000000..e0bb5c0 --- /dev/null +++ b/skills/zerodust/examples/batch-sweep/README.md @@ -0,0 +1,69 @@ +# Batch Sweep Multiple Chains + +Sweep dust from multiple chains and consolidate to a single destination. + +## Prerequisites + +```bash +npm install @zerodust/sdk viem +``` + +## Sweep Specific Chains + +```typescript +import { ZeroDustAgent } from '@zerodust/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; +import type { Hex } from 'viem'; + +const agent = new ZeroDustAgent({ + account: privateKeyToAccount(process.env.PRIVATE_KEY as Hex), + environment: 'mainnet', +}); + +const results = await agent.batchSweep({ + sweeps: [ + { fromChainId: 42161 }, // Arbitrum + { fromChainId: 10 }, // Optimism + { fromChainId: 137 }, // Polygon + { fromChainId: 56 }, // BSC + ], + consolidateToChainId: 8453, // All to Base + continueOnError: true, // Don't stop if one chain fails +}); + +console.log(`Results: ${results.successful}/${results.total} succeeded`); + +for (const r of results.results) { + const status = r.success ? `OK (${r.txHash})` : `FAILED: ${r.error}`; + console.log(` Chain ${r.fromChainId} -> ${r.toChainId}: ${status}`); +} +``` + +## Sweep All Sweepable Chains + +```typescript +// Automatically discovers all chains with sweepable dust +const results = await agent.sweepAll({ + toChainId: 8453, // Everything to Base + continueOnError: true, +}); + +console.log(`Swept ${results.successful} chains, ${results.failed} failed`); +``` + +## Error Handling + +With `continueOnError: true`, failed chains don't stop the batch. Check individual results: + +```typescript +const failed = results.results.filter(r => !r.success); +for (const f of failed) { + if (f.error?.includes('BALANCE_TOO_LOW')) { + console.log(`Chain ${f.fromChainId}: balance too small, skipping`); + } else if (f.error?.includes('SOURCE_CHAIN_DISABLED')) { + console.log(`Chain ${f.fromChainId}: bridge temporarily down`); + } else { + console.log(`Chain ${f.fromChainId}: unexpected error: ${f.error}`); + } +} +``` diff --git a/skills/zerodust/examples/check-balances/README.md b/skills/zerodust/examples/check-balances/README.md new file mode 100644 index 0000000..00c8858 --- /dev/null +++ b/skills/zerodust/examples/check-balances/README.md @@ -0,0 +1,59 @@ +# Check Balances Across Chains + +Check native token dust balances across all 25 supported chains. + +## Prerequisites + +```bash +npm install @zerodust/sdk viem +``` + +## SDK Example + +```typescript +import { ZeroDust } from '@zerodust/sdk'; + +const zerodust = new ZeroDust({ environment: 'mainnet' }); + +const address = '0x1234567890abcdef1234567890abcdef12345678'; +const { chains } = await zerodust.getBalances(address); + +// Filter to sweepable chains +const sweepable = chains.filter(c => c.canSweep); + +console.log(`Found ${sweepable.length} sweepable chains:`); +for (const chain of sweepable) { + console.log(` ${chain.name} (${chain.chainId}): ${chain.balanceFormatted} ${chain.nativeToken}`); +} +``` + +## REST API Example + +```bash +# All chains (mainnet) +curl "https://api.zerodust.xyz/balances/0x1234...?testnet=false" + +# Specific chain +curl "https://api.zerodust.xyz/balances/0x1234.../42161" +``` + +## Response + +```json +{ + "address": "0x...", + "chains": [ + { + "chainId": 42161, + "name": "Arbitrum", + "nativeToken": "ETH", + "balance": "800000000000000", + "balanceFormatted": "0.0008", + "canSweep": true, + "minBalance": "10000000000000" + } + ] +} +``` + +`canSweep` is `true` when the balance exceeds the chain's minimum (covers gas fees). Only request quotes for chains where `canSweep` is `true`. diff --git a/skills/zerodust/examples/sweep-single-chain/README.md b/skills/zerodust/examples/sweep-single-chain/README.md new file mode 100644 index 0000000..7122672 --- /dev/null +++ b/skills/zerodust/examples/sweep-single-chain/README.md @@ -0,0 +1,72 @@ +# Sweep Single Chain + +Sweep all native tokens from Arbitrum to Base using the ZeroDustAgent SDK. + +## Prerequisites + +```bash +npm install @zerodust/sdk viem +``` + +## Complete Example + +```typescript +import { ZeroDustAgent } from '@zerodust/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; +import type { Hex } from 'viem'; + +async function sweepArbitrumToBase() { + // Initialize agent + const agent = new ZeroDustAgent({ + account: privateKeyToAccount(process.env.PRIVATE_KEY as Hex), + environment: 'mainnet', + }); + + console.log(`Agent address: ${agent.address}`); + + // Check balance on Arbitrum + const balance = await agent.getBalance(42161); + console.log(`Arbitrum balance: ${balance.balanceFormatted} ETH`); + + if (!balance.canSweep) { + console.log('Balance too low to sweep'); + return; + } + + // Sweep Arbitrum -> Base + const result = await agent.sweep( + { + fromChainId: 42161, // Arbitrum + toChainId: 8453, // Base + }, + { + waitForCompletion: true, + timeoutMs: 120_000, + onStatusChange: (status) => { + console.log(`Status: ${status.status}`); + }, + } + ); + + if (result.success) { + console.log(`Sweep completed!`); + console.log(` TX: ${result.txHash}`); + console.log(` Amount: ${result.status?.amountSent}`); + } else { + console.log(`Sweep failed: ${result.error}`); + } +} + +sweepArbitrumToBase(); +``` + +## What Happens Under the Hood + +The `agent.sweep()` method handles 6 steps automatically: + +1. `GET /quote` - Fetches quote with fee breakdown +2. `POST /authorization` - Gets EIP-712 typed data +3. Signs EIP-712 SweepIntent (using agent's private key) +4. Signs EIP-7702 delegation + revoke authorizations +5. `POST /sweep` - Submits all signatures +6. Polls `GET /sweep/:id` until completed or failed diff --git a/skills/zerodust/resources/contract-addresses.md b/skills/zerodust/resources/contract-addresses.md new file mode 100644 index 0000000..bf97369 --- /dev/null +++ b/skills/zerodust/resources/contract-addresses.md @@ -0,0 +1,49 @@ +# ZeroDust Contract Addresses + +## Mainnet (CREATE2 Deterministic) + +**Address:** `0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2` + +Same address on all 25 supported chains. Deployed via CREATE2 for deterministic addressing. + +Last verified: January 28, 2026. Verification method: source code verified on each chain's block explorer. + +| Chain | Chain ID | Native Token | Explorer Link | +|-------|----------|--------------|---------------| +| Ethereum | 1 | ETH | [etherscan.io](https://etherscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Optimism | 10 | ETH | [optimistic.etherscan.io](https://optimistic.etherscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| BSC | 56 | BNB | [bscscan.com](https://bscscan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Gnosis | 100 | xDAI | [gnosisscan.io](https://gnosisscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Unichain | 130 | ETH | [uniscan.xyz](https://uniscan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Polygon | 137 | POL | [polygonscan.com](https://polygonscan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Sonic | 146 | S | [sonicscan.org](https://sonicscan.org/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| X Layer | 196 | OKB | [oklink.com/xlayer](https://www.oklink.com/xlayer/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Fraxtal | 252 | frxETH | [fraxscan.com](https://fraxscan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| World Chain | 480 | ETH | [worldscan.org](https://worldscan.org/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Sei | 1329 | SEI | [seitrace.com](https://seitrace.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Story | 1514 | IP | [storyscan.xyz](https://storyscan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Soneium | 1868 | ETH | [soneium.blockscout.com](https://soneium.blockscout.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Mantle | 5000 | MNT | [mantlescan.xyz](https://mantlescan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Superseed | 5330 | ETH | [superseed.explorer.caldera.xyz](https://superseed.explorer.caldera.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Base | 8453 | ETH | [basescan.org](https://basescan.org/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Plasma | 9745 | XPL | [plasma-explorer.genesys.network](https://plasma-explorer.genesys.network/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Mode | 34443 | ETH | [modescan.io](https://modescan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Arbitrum | 42161 | ETH | [arbiscan.io](https://arbiscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Celo | 42220 | CELO | [celoscan.io](https://celoscan.io/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Ink | 57073 | ETH | [inkscan.xyz](https://inkscan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| BOB | 60808 | ETH | [explorer.gobob.xyz](https://explorer.gobob.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Berachain | 80094 | BERA | [berascan.com](https://berascan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Scroll | 534352 | ETH | [scrollscan.com](https://scrollscan.com/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | +| Zora | 7777777 | ETH | [zorascan.xyz](https://zorascan.xyz/address/0x3732398281d0606aCB7EC1D490dFB0591BE4c4f2) | + +## Sponsor Wallet + +**Address:** `0x01eD5c94DE39E73C986b98B85C2c0A3d1BEDff7D` (AWS KMS protected) + +The sponsor executes sweeps on behalf of users and is reimbursed from the swept amount. + +## Treasury + +**Address:** `0x94De5DfAadEF11427519e346C9535C55039cC60F` (Safe multisig on Base) + +Receives accumulated service fees via weekly automatic transfers. diff --git a/skills/zerodust/resources/error-codes.md b/skills/zerodust/resources/error-codes.md new file mode 100644 index 0000000..5900447 --- /dev/null +++ b/skills/zerodust/resources/error-codes.md @@ -0,0 +1,70 @@ +# ZeroDust Error Codes + +All API errors return JSON with `error` (message) and `code` (machine-readable) fields. + +## Quote Errors + +| Code | HTTP | Cause | Fix | +|------|------|-------|-----| +| `BALANCE_TOO_LOW` | 400 | Balance below chain minimum | Check `canSweep` from balances endpoint first | +| `INSUFFICIENT_FOR_FEES` | 400 | Balance doesn't cover gas + service fee | Try same-chain sweep (lower fees) or wait for lower gas | +| `INVALID_FROM_CHAIN` | 400 | Source chain not supported | Check `GET /chains` for supported chains | +| `INVALID_TO_CHAIN` | 400 | Destination chain not supported | Check `GET /chains` for supported chains | +| `INVALID_CALL_TARGET` | 400 | Invalid bridge contract address | Let the API auto-fetch Gas.zip calldata | +| `INVALID_CALL_DATA` | 400 | Invalid hex-encoded bridge data | Let the API auto-fetch Gas.zip calldata | + +## Bridge Errors + +| Code | HTTP | Cause | Fix | +|------|------|-------|-----| +| `BRIDGE_UNAVAILABLE` | 400 | Gas.zip route not available | Try different destination or same-chain | +| `SOURCE_CHAIN_DISABLED` | 400 | Gas.zip disabled this source chain | Use same-chain sweep or wait | +| `DEST_CHAIN_DISABLED` | 400 | Gas.zip disabled this destination | Try different destination chain | +| `NO_ROUTE` | 400 | No bridge path exists | Try different chain combination | + +## Sweep Submission Errors + +| Code | HTTP | Cause | Fix | +|------|------|-------|-----| +| `QUOTE_EXPIRED` | 400 | Quote deadline passed (55s) | Request new quote and sign immediately | +| `INVALID_SIGNATURE` | 400 | EIP-712 SweepIntent signature invalid | Ensure verifyingContract = user's EOA | +| `EIP7702_INVALID_SIGNATURE` | 400 | EIP-7702 auth not signed by user | Verify signing account matches userAddress | +| `CHAIN_ID_MISMATCH` | 400 | Auth chainId != quote chainId | Use same chainId as the quote | +| `CONTRACT_ADDRESS_MISMATCH` | 400 | Auth contract != ZeroDust contract | Use contractAddress from /authorization | +| `CONTRACT_NOT_DEPLOYED` | 400 | No contract on requested chain | Check supported chains | +| `INVALID_REVOKE_TARGET` | 400 | Revoke auth not delegating to address(0) | Set contractAddress to 0x000...000 | +| `REVOKE_CHAIN_ID_MISMATCH` | 400 | Revoke chainId != quote chainId | Use same chainId | +| `REVOKE_NONCE_MISMATCH` | 400 | Revoke nonce != delegation nonce + 1 | Set nonce to delegationAuth.nonce + 1 | +| `MODE_MISMATCH` | 400 | MODE_TRANSFER used for cross-chain | MODE_TRANSFER only for same-chain | +| `MISSING_CALL_DATA` | 400 | Cross-chain quote missing bridge data | Re-fetch quote (Gas.zip may have failed) | +| `MISSING_CALL_TARGET` | 400 | Cross-chain quote missing bridge address | Re-fetch quote | + +## Validation Errors + +| Code | HTTP | Cause | Fix | +|------|------|-------|-----| +| `INVALID_ADDRESS` | 400 | Malformed Ethereum address | Validate with `isAddress()` from viem | +| `SIG_NOT_HEX` | 400 | Signature not valid hex | Ensure 0x prefix and hex characters | +| `SIG_BAD_LENGTH` | 400 | Signature not 64 or 65 bytes | Standard ECDSA signatures are 65 bytes | +| `INVALID_RS` | 400 | EIP-7702 r/s values not valid hex | Check wallet signing output | + +## Server Errors + +| Code | HTTP | Cause | Fix | +|------|------|-------|-----| +| `DB_ERROR` | 500 | Database connection issue | Retry after brief delay | +| `INTERNAL_ERROR` | 500 | Unexpected server error | Retry; if persistent, contact support | +| `DATA_INTEGRITY_ERROR` | 500 | Quote data corrupted | Re-fetch quote | + +## SDK Error Classes + +| Class | Corresponding API Code(s) | +|-------|--------------------------| +| `BalanceTooLowError` | `BALANCE_TOO_LOW`, `INSUFFICIENT_FOR_FEES` | +| `QuoteExpiredError` | `QUOTE_EXPIRED` | +| `ChainNotSupportedError` | `INVALID_FROM_CHAIN`, `INVALID_TO_CHAIN`, `CONTRACT_NOT_DEPLOYED` | +| `SignatureError` | `INVALID_SIGNATURE`, `EIP7702_INVALID_SIGNATURE` | +| `BridgeError` | `BRIDGE_UNAVAILABLE`, `SOURCE_CHAIN_DISABLED`, `DEST_CHAIN_DISABLED` | +| `InvalidAddressError` | `INVALID_ADDRESS` | +| `NetworkError` | Connection/timeout issues | +| `TimeoutError` | Request exceeded timeout | diff --git a/skills/zerodust/templates/zerodust-client.ts b/skills/zerodust/templates/zerodust-client.ts new file mode 100644 index 0000000..5aea3ff --- /dev/null +++ b/skills/zerodust/templates/zerodust-client.ts @@ -0,0 +1,175 @@ +/** + * ZeroDust Client Template + * + * A complete, runnable starter for integrating ZeroDust dust sweeping. + * Handles balance checking, single sweeps, and batch sweeps. + * + * Prerequisites: + * npm install @zerodust/sdk viem + * + * Usage: + * PRIVATE_KEY=0x... npx tsx zerodust-client.ts + */ + +import { + ZeroDustAgent, + ZeroDustError, + BalanceTooLowError, + QuoteExpiredError, + BridgeError, + type AgentSweepResult, +} from '@zerodust/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; +import { formatUnits, type Hex } from 'viem'; + +// ============ Configuration ============ + +const PRIVATE_KEY = process.env.PRIVATE_KEY as Hex; +if (!PRIVATE_KEY) { + console.error('Set PRIVATE_KEY environment variable'); + process.exit(1); +} + +// Destination chain for consolidation (Base by default) +const DESTINATION_CHAIN_ID = 8453; + +// Whether to actually execute sweeps (set false for dry run) +const EXECUTE_SWEEPS = process.env.DRY_RUN !== 'true'; + +// ============ Agent Setup ============ + +const agent = new ZeroDustAgent({ + account: privateKeyToAccount(PRIVATE_KEY), + environment: 'mainnet', + // Optional: custom RPC URLs for better reliability + // rpcUrls: { + // 42161: 'https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY', + // 8453: 'https://base-mainnet.g.alchemy.com/v2/YOUR_KEY', + // }, +}); + +console.log(`Agent address: ${agent.address}`); +console.log(`Destination chain: ${DESTINATION_CHAIN_ID}`); +console.log(`Mode: ${EXECUTE_SWEEPS ? 'LIVE' : 'DRY RUN'}`); +console.log('---'); + +// ============ Check Balances ============ + +async function checkBalances() { + console.log('Checking balances across all chains...'); + + const sweepable = await agent.getSweepableBalances(); + + if (sweepable.length === 0) { + console.log('No sweepable balances found.'); + return []; + } + + console.log(`Found ${sweepable.length} sweepable chains:`); + for (const b of sweepable) { + console.log(` ${b.name} (${b.chainId}): ${b.balanceFormatted} ${b.nativeToken}`); + } + + return sweepable; +} + +// ============ Single Chain Sweep ============ + +async function sweepChain(fromChainId: number, toChainId: number): Promise { + console.log(`\nSweeping chain ${fromChainId} -> ${toChainId}...`); + + try { + const result = await agent.sweep( + { fromChainId, toChainId }, + { + waitForCompletion: true, + timeoutMs: 120_000, + onStatusChange: (status) => { + console.log(` Status: ${status.status}`); + }, + } + ); + + if (result.success) { + console.log(` Completed! TX: ${result.txHash}`); + } else { + console.log(` Failed: ${result.error}`); + } + + return result; + } catch (error) { + if (error instanceof BalanceTooLowError) { + console.log(` Skipped: balance too low`); + } else if (error instanceof QuoteExpiredError) { + console.log(` Quote expired, would need to retry`); + } else if (error instanceof BridgeError) { + console.log(` Bridge unavailable, trying same-chain...`); + // Fall back to same-chain sweep + return agent.sweep( + { fromChainId, toChainId: fromChainId }, + { waitForCompletion: true, timeoutMs: 120_000 } + ); + } else if (error instanceof ZeroDustError) { + console.log(` ZeroDust error [${error.code}]: ${error.message}`); + } else { + console.log(` Unexpected error: ${error}`); + } + + return { success: false, error: String(error) }; + } +} + +// ============ Batch Sweep All ============ + +async function sweepAll() { + console.log(`\nSweeping all chains to chain ${DESTINATION_CHAIN_ID}...`); + + const results = await agent.sweepAll({ + toChainId: DESTINATION_CHAIN_ID, + continueOnError: true, + }); + + console.log(`\nResults: ${results.successful}/${results.total} succeeded, ${results.failed} failed`); + + for (const r of results.results) { + const status = r.success + ? `OK (TX: ${r.txHash})` + : `FAILED: ${r.error}`; + console.log(` Chain ${r.fromChainId} -> ${r.toChainId}: ${status}`); + } + + return results; +} + +// ============ Main ============ + +async function main() { + // Step 1: Check what's available + const sweepable = await checkBalances(); + + if (sweepable.length === 0) { + return; + } + + if (!EXECUTE_SWEEPS) { + console.log('\nDry run complete. Set DRY_RUN=false to execute sweeps.'); + return; + } + + // Step 2: Sweep everything to destination chain + const results = await sweepAll(); + + // Step 3: Summary + console.log('\n=== Summary ==='); + console.log(`Total chains swept: ${results.successful}`); + console.log(`Failed: ${results.failed}`); + + if (results.failed > 0) { + console.log('\nFailed chains:'); + for (const r of results.results.filter(r => !r.success)) { + console.log(` ${r.fromChainId}: ${r.error}`); + } + } +} + +main().catch(console.error);