# zerc20-client-sdk

Client SDK for interacting with zERC20 privacy-preserving token system from browser or Node.js environments.

## Overview

zERC20 is a privacy-preserving, cross-chain ERC-20 token system that enables:

- Private transfers using zero-knowledge proofs (Nova and Groth16 circuits)
- Stealth transactions via a messaging layer on the Internet Computer (ICP)
- Cross-chain functionality using LayerZero as the interoperability protocol

This SDK provides TypeScript bindings for all client-side operations.

## Installation

```bash
npm install zerc20-client-sdk
```

## Requirements

- Node.js >= 18
- Browser with WebAssembly support

## Compatibility Notes

- This package is **ESM-only** (`"type": "module"`). CommonJS (`require`) is not supported.
- HTTP-based modules (e.g. Decider client, LayerZero Scan) rely on `fetch`.
  - Node.js 18+ includes `fetch` globally.
  - In custom runtimes, provide a compatible `fetch` implementation where needed.

## Provider Abstraction (Library/Framework Agnostic)

The SDK public API is designed to be wallet/provider agnostic:

- Read paths accept `EvmReadProvider`
- Write paths accept `EvmWriteProvider`
- UI frameworks are not part of the SDK surface

This lets integrators use viem, ethers, web3.js, or custom adapters without depending on React/Vue internals.

```typescript
import type { EvmReadProvider, EvmWriteProvider } from "zerc20-client-sdk";
```

## Quick Start

```typescript
import { createSdk, preparePrivateSend, scanReceivings } from "zerc20-client-sdk";

// Initialize SDK
const sdk = createSdk({
  // configuration options
});

// Create a stealth client for ICP communication
const client = sdk.createStealthClient({
  agent,
  storageCanisterId,
  keyManagerCanisterId,
});

// Prepare a private send
const prepared = await preparePrivateSend({
  client,
  seed,
  recipientChainId,
  recipientAddress,
  // ...
});

// Scan for received payments
const announcements = await scanReceivings({
  client,
  vetKey,
  // ...
});
```

## Architecture

```
src/
├── ic/           # Internet Computer (ICP) integration
│   ├── client.ts       # StealthCanisterClient for canister communication
│   ├── idl.ts          # Candid IDL factory definitions
│   ├── encryption.ts   # IBE + AES-GCM encryption
│   ├── authorization.ts # EIP-191 signing & VetKD authorization
│   └── keys.ts         # View key derivation
├── operations/   # High-level protocol operations
│   ├── privateSend.ts  # Private payment flow
│   ├── receive/        # Receive/scan announcements
│   ├── invoice.ts      # Invoice creation
│   ├── teleport.ts     # Single teleport proof
│   ├── teleportProof.ts # Batch teleport proof (Nova + Decider)
│   ├── relay/          # Relay node HTTP client (redeem/swap)
│   ├── liquidityManager/ # Wrap/unwrap with slippage protection
│   └── layerzeroScan/   # LayerZero message tracking & decoding
├── wasm/         # WASM runtime & bindings
│   ├── index.ts        # WasmRuntime class
│   ├── loader.ts       # WASM bindings loader (browser/Node.js)
│   └── serialization.ts # Type conversion utilities
├── zkp/          # Zero-knowledge proof orchestration
├── decider/      # Decider proof generation (HTTP client)
├── chain/        # Chain metadata (names, explorers, aliases)
├── registry/     # Token & hub configuration
├── onchain/      # Contract interaction (ABI decoding, token reads)
└── utils/        # Shared utilities
```

## Key Modules

### WASM Configuration

Configure the WASM runtime for cryptographic operations:

```typescript
import { configureWasmLocator } from "zerc20-client-sdk";

// Point to your hosted WASM binary
configureWasmLocator({ url: "/zerc20_wasm_bg.wasm" });
```

### StealthCanisterClient

Communicates with ICP canisters for announcements and invoices:

```typescript
import { StealthCanisterClient } from "zerc20-client-sdk";

const client = new StealthCanisterClient(agent, storageCanisterId, keyManagerCanisterId);

// Submit an announcement
await client.submitAnnouncement(announcementInput);

// List announcements
const page = await client.listAnnouncements(startAfter, limit);
```

### Liquidity Manager

Wrap and unwrap tokens with slippage protection:

```typescript
import {
  unwrapWithLiquidityManager,
  buildCrossUnwrapQuote,
  applySlippage,
} from "zerc20-client-sdk";

// Local unwrap with slippage protection
await unwrapWithLiquidityManager({
  writeProvider,
  readProvider,
  liquidityManagerAddress,
  zerc20TokenAddress,
  amount: 1000n,
  minAmountOut: 950n, // Reject if output < 950
});

// Cross-chain unwrap with slippage tolerance (basis points)
const quote = await buildCrossUnwrapQuote({
  sourceToken,
  destinationToken,
  amount: 1000n,
  account,
  readProviderSource: sourceReadProvider,
  readProviderDestination: destReadProvider,
  slippageBps: 50, // 0.5% slippage tolerance
});
```

### Relay Operations

Interact with a relay node for relayer-based redeem and token-to-native swap flows:

```typescript
import {
  estimateRelayFee,
  fetchSwapQuote,
  submitRelaySwap,
  submitRelayTeleport,
} from "zerc20-client-sdk";

const fee = await estimateRelayFee("https://relay.example", 1);

const quote = await fetchSwapQuote("https://relay.example", { chainId: 1, amount: 1_000_000n });
if (quote.priceFallback) {
  console.warn("Relay is using fallback oracle prices");
}

await submitRelaySwap("https://relay.example", {
  chainId: 1,
  tokenAmount: 1_000_000n,
  minNativeAmount: 900_000n,
  maxNativeAmount: quote.nativeAmount,
  recipient: "0x...",
  owner: "0x...",
  permitDeadline: 1_700_000_000n,
  permitV: 27,
  permitR: "0x...",
  permitS: "0x...",
});
```

### Blocklist

Check whether an address is on the OFAC sanctions blocklist before executing a private send. This prevents funds from being permanently locked when the recipient cannot redeem:

```typescript
import { isBlockedAddress } from "zerc20-client-sdk";

const blocked = await isBlockedAddress(readProvider, blocklistAddress, recipientAddress);
if (blocked) {
  throw new Error("Recipient is on the OFAC sanctions blocklist");
}
```

### Adaptor Withdraw (Stuck Fund Recovery)

When a cross-chain unwrap fails (e.g. due to Stargate liquidity shortage), user funds may remain in the destination chain's Adaptor contract. The SDK provides functions to detect and recover these stuck funds:

```typescript
import {
  fetchAdaptorBalances,
  hasStuckFunds,
  withdrawFromAdaptor,
  NATIVE_TOKEN_ADDRESS,
} from "zerc20-client-sdk";

// Check if a user has stuck funds in an adaptor
const stuck = await hasStuckFunds({
  provider: readProvider,
  account: "0xUser...",
  adaptorAddress: "0xAdaptor...",
});

// Fetch detailed balances
const balances = await fetchAdaptorBalances({
  provider: readProvider,
  account: "0xUser...",
  adaptorAddress: "0xAdaptor...",
});
// balances.underlyingTokenBalance, balances.zerc20Balance, balances.nativeBalance

// Withdraw stuck funds
const result = await withdrawFromAdaptor({
  writeProvider,
  adaptorAddress: "0xAdaptor...",
  token: balances.underlyingTokenAddress, // or zerc20TokenAddress, or NATIVE_TOKEN_ADDRESS
  amount: balances.underlyingTokenBalance,
});
```

### LayerZero Scan

Track and decode LayerZero cross-chain messages. Configuration and providers are injected as parameters, making this module framework-agnostic:

```typescript
import {
  fetchWalletStatus,
  type FetchWalletStatusParams,
  type LayerZeroScanConfig,
} from "zerc20-client-sdk";

const scanConfig: LayerZeroScanConfig = {
  baseUrl: "https://scan.layerzero-api.com/v1/",
  apiKey: "your-api-key", // optional
};

const result = await fetchWalletStatus({
  address: "0xuser",
  tokens,
  scanConfig,
  createReadProvider: (token) => createPublicClient({ chain: ... }),
  limit: 10,
  filterByToken: true,
});

// result.items: LayerZeroMessageSummary[]
// result.walletUrl: string (link to LZ Scan)
// result.nextToken: string | undefined (pagination cursor)
```

Notes:
- `fetchWalletStatus` can use transaction input (`getTransaction`) and receipt logs (`getTransactionReceipt`) when available.
- If those methods are unavailable, decoding still falls back to payload-based paths where possible.

### Seed Derivation

Derive a seed from a wallet signature. The SDK handles the message retrieval, signature validation, and keccak256 hashing internally, while accepting a wallet-agnostic sign function:

```typescript
import { deriveSeed } from "zerc20-client-sdk";

// Works with any wallet library (wagmi, ethers.js, web3.js, etc.)
const seed = await deriveSeed(async (message) => {
  // Return the signature as a 0x-prefixed hex string (65 bytes for personal_sign)
  return wallet.signMessage(message);
});
```

The function validates that the returned signature is a valid hex string of exactly 65 bytes before hashing. Invalid signatures throw descriptive errors to prevent silent seed misderivation.

### Chain Metadata

Look up chain display names, short labels, block explorer URLs, and resolve chain name aliases — all from a single source of truth shared across products:

```typescript
import {
  getChainMetadata,
  getChainDisplayName,
  getExplorerTxUrl,
  resolveChainId,
  resolveNetworkDisplayName,
} from "zerc20-client-sdk";

getChainDisplayName(42161);                 // "Arbitrum"
getChainDisplayName(999999);                // "Chain 999999"

getExplorerTxUrl(42161, "0xabc...");        // "https://arbiscan.io/tx/0xabc..."

resolveChainId("arb-sepolia");              // 421614
resolveNetworkDisplayName("arb-sepolia");   // "Arbitrum Sepolia"
```

### Token Loading with RPC Overrides

Load and normalize token configuration with runtime RPC URL overrides (e.g. environment-specific Alchemy/Infura endpoints):

```typescript
import { normalizeTokensWithOverrides } from "zerc20-client-sdk";
import tokensJson from "./tokens.json";

const tokens = normalizeTokensWithOverrides(
  tokensJson as TokensFile, // snake_case JSON is accepted as-is
  {
    tokens: {
      "arb-mainnet": ["https://arb-mainnet.g.alchemy.com/v2/KEY"],
      "base-mainnet": ["https://base-mainnet.g.alchemy.com/v2/KEY"],
    },
    hub: ["https://base-mainnet.g.alchemy.com/v2/KEY"],
  },
);
```

Without overrides, `normalizeTokensWithOverrides(file)` behaves identically to `normalizeTokens(file)`.

### Redeem Flow

Prepare a redeem transaction from a collected redeem context. The SDK handles single vs batch proof selection, `GeneralRecipient` construction, and contract call parameter assembly internally:

```typescript
import {
  collectRedeemContext,
  prepareRedeemTransaction,
  createTeleportProofClient,
  HttpDeciderClient,
} from "zerc20-client-sdk";

// Initialize proof client and decider
const teleportProofClient = createTeleportProofClient();
const decider = new HttpDeciderClient("https://decider.example.com");

// 1. Collect redeem context (eligible events, proofs, etc.)
const redeemContext = await collectRedeemContext({ burn, tokens, hub, verifierContract, indexerUrl });

// 2. Prepare the transaction (SDK picks single vs batch automatically)
const tx = await prepareRedeemTransaction({
  redeemContext,
  burn,
  teleportProofClient,
  decider, // required only when eligible.length > 1
});

// 3. Submit via your wallet library
const hash = await walletClient.writeContract({
  address: tx.address as `0x${string}`,
  abi: tx.abi,
  functionName: tx.functionName,
  args: tx.args,
  account,
  chain,
});
```

The returned `RedeemTransaction` is pure data (`{ address, abi, functionName, args, mode }`) with no wallet dependency.

#### Manual batch flow with UI progress

For batch redeems, callers can use the 2-step proof API to insert UI updates between Nova and Decider steps:

```typescript
import {
  collectRedeemContext,
  buildBatchRedeemTransaction,
  createTeleportProofClient,
  HttpDeciderClient,
} from "zerc20-client-sdk";

const teleportProofClient = createTeleportProofClient();
const decider = new HttpDeciderClient("https://decider.example.com");
const redeemContext = await collectRedeemContext({ burn, tokens, hub, verifierContract, indexerUrl });

// Step 1: Nova proof
const novaResult = await teleportProofClient.createNovaProof({
  aggregationState: redeemContext.aggregationState,
  recipientFr: burn.generalRecipient.fr,
  secretHex: burn.secret,
  events: redeemContext.events.eligible,
  proofs: redeemContext.globalProofs,
});

updateUI("Requesting decider proof…"); // ← insert UI update here

// Step 2: Decider proof
const deciderProof = await teleportProofClient.requestDeciderProof(decider, novaResult.ivcProof);

// Step 3: Build transaction
const tx = buildBatchRedeemTransaction({ redeemContext, burn, deciderProof });
```

### LayerZero Decode Utilities

Decode LayerZero OFT transaction data — `send()` calldata, `OFTSent` event logs, and BridgeRequest compose messages:

```typescript
import {
  decodeSendPayload,
  extractOftSentAmount,
  decodeBridgeRequest,
} from "zerc20-client-sdk";

// Decode send() transaction input
const payload = decodeSendPayload(txData);
// → { dstEid, to, amountLD, minAmountLD, composeMsg }

// Extract amountReceivedLD from OFTSent event logs
const amount = extractOftSentAmount(receipt.logs);

// Decode a BridgeRequest compose message (returns null on failure)
const bridgeReq = decodeBridgeRequest(payload.composeMsg);
// → { dstEid, to, refundAddress, minAmountOut } | null
```

### Proof Generation

```typescript
import {
  HttpDeciderClient,
  createTeleportProofClient,
} from "zerc20-client-sdk";

const proofClient = createTeleportProofClient();

// Single teleport proof (Groth16)
const singleProof = await proofClient.createSingleTeleportProof(/* ... */);

// Batch teleport proof (Nova + Decider) — two-step API
const novaResult = await proofClient.createNovaProof({
  aggregationState,
  recipientFr,
  secretHex,
  events,
  proofs,
});

// Callers can update UI between steps (e.g. progress indicators)

const decider = new HttpDeciderClient("https://decider.example.com");
const deciderProof = await proofClient.requestDeciderProof(decider, novaResult.ivcProof);
```

## Development

### Build

```bash
npm run build
```

### Test

```bash
npm test
```

### Type Check

```bash
npm run typecheck
```

### Skipped Tests

1. **`src/zkp/__tests__/runNovaProver.test.ts`**
   - Skipped when artifact files are missing
   - Artifacts are expected under `public/artifacts/`:
     - `withdraw_local_groth16_pk.bin`
     - `withdraw_local_groth16_vk.bin`
     - `withdraw_global_groth16_pk.bin`
     - `withdraw_global_groth16_vk.bin`
     - `withdraw_local_nova_pp.bin`
     - `withdraw_local_nova_vp.bin`
     - `withdraw_global_nova_pp.bin`
     - `withdraw_global_nova_vp.bin`

2. **`src/ic/__tests__/storage_localnet.test.ts`**
   - Skipped by design
   - Requires running local ICP replica (`dfx start`)

## API Stability and Refactoring Policy

`zerc20-client-sdk` is published and used by downstream applications (e.g. `zerc20-frontend`). As a rule, prefer non-breaking refactors.

- Prefer importing from the package root (`import { ... } from "zerc20-client-sdk"`). Subpath imports are considered internal and may change.
- Avoid changing the public surface (notably `src/index.ts` exports and re-exports) unless necessary.
- Avoid breaking changes to the public surface (notably `src/index.ts` exports).
- When a rename/restructure is desired, keep the old entrypoint and mark it `@deprecated`, and introduce the new API in parallel.
- Deprecation guideline:
  - Keep deprecated entrypoints for at least one minor release (or a clearly communicated timeframe) before removal.
  - Removal (or behavioral changes) should only happen in a major version bump, with a migration note.
- If a breaking change is unavoidable:
  - Provide a migration note (old → new) in the release/PR description.
  - Bump the version appropriately (SemVer).
  - Validate against downstream usage (at minimum run TypeScript typecheck/build in `zerc20-frontend`).

### Downstream Verification (Recommended)

Before merging changes that touch public exports/types, validate against the downstream app:

```bash
# In zerc20-frontend
npm run typecheck
npm run build
```

## Dependencies

- `@dfinity/*` - Internet Computer SDK
- `viem` - Ethereum library
- `@noble/curves` - Cryptographic primitives
- `poseidon-lite` - Poseidon hash function
- `pako` - Compression (gzip)

## License

MIT
