Shielded Transfers NPM package
  • TypeScript 98.2%
  • CSS 1%
  • JavaScript 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-13 20:21:20 +02:00
examples v7 zk + version 14 2026-07-14 01:18:46 +07:00
public v7 zk + version 14 2026-07-14 01:18:46 +07:00
scripts v1 2026-03-13 01:43:17 +07:00
src v7 zk + version 14 2026-07-14 01:18:46 +07:00
LICENSE Initial commit 2026-03-11 15:19:14 +01:00
LLM_AGENT_GUIDE.md v7 zk + version 14 2026-07-14 01:18:46 +07:00
package-lock.json v7 zk + version 14 2026-07-14 01:18:46 +07:00
package.json v7 zk + version 14 2026-07-14 01:18:46 +07:00
paseo_roundtrip_test.ts v7 zk + version 14 2026-07-14 01:18:46 +07:00
README.md v7 zk + version 14 2026-07-14 01:18:46 +07:00
tsconfig.json v1 2026-03-13 01:43:17 +07:00
tsconfig.test.json v1 2026-03-13 01:43:17 +07:00
tsconfig.tsbuildinfo first version 2026-05-14 01:25:12 +07:00
vite.config.ts v1 2026-03-13 01:43:17 +07:00

@kusama-shield/shielded-transfers

Zero-knowledge shielded transfer library for interacting with Kusama Shield

npm version License: MIT TypeScript

A comprehensive React library for implementing privacy-preserving token transfers using zero-knowledge proofs (zk-SNARKs). Built for the Kusama Shield protocol but adaptable to any EVM-compatible chain.

Features

Feature Description
🛡️ Shielded Deposits Convert transparent tokens into private shielded assets with ZK commitments
🔓 Private Withdrawals Withdraw shielded assets with Groth16 zero-knowledge proofs
🔒 Unlinkable v7 v7 pool removes deposit-withdrawal linkability — commitment never in withdrawal signals
🌲 Merkle Tree Client-side incremental LeanIMT with localStorage caching
Fast Proving WebAssembly-based proof generation with multi-threaded support
🔄 XCM Ready Cross-chain transfer capabilities for Polkadot ecosystem
💼 Wallet Support EVM wallets (MetaMask, WalletConnect) + optional Polkadot wallets
🔷 100% ethers.js Full compatibility with ethers.js v6 - use native Signer, Provider, parseUnits, etc.

Installation

npm install @kusama-shield/shielded-transfers
# or
yarn add @kusama-shield/shielded-transfers
# or
pnpm add @kusama-shield/shielded-transfers

Peer Dependencies

{
  "react": ">=17.0.0",
  "react-dom": ">=17.0.0"
}

Quick Start

1. Basic Setup

import { ShieldedTransferProvider } from '@kusama-shield/shielded-transfers';
import '@kusama-shield/shielded-transfers/styles.css';

function App() {
  return (
    <ShieldedTransferProvider
      contractAddress="0xDEB209D0a993A4ce495FB668698c08Eb5ca1F33d"
      rpcUrl="wss://moonbase-alpha.public.blastapi.io"
      zkeyPaths={{
        deposit: '/zk-assets/main_0000.zkey',
        withdraw: '/zk-assets/withdraw_0001.zkey',
      }}
      wasmPaths={{
        deposit: '/zk-assets/main.wasm',
        withdraw: '/zk-assets/withdraw.wasm',
      }}
    >
      <YourApp />
    </ShieldedTransferProvider>
  );
}

2. Using the Hook

import { useShieldedTransfer } from '@kusama-shield/shielded-transfers';
import { ethers } from 'ethers';

function ShieldForm() {
  const {
    initialize,
    shieldTokens,
    unshieldTokens,
    generateSecret,
    generateNullifier,
    getMerkleProof,
    isLoading,
    isReady,
    error,
    merkleRoot,
    treeSize,
  } = useShieldedTransfer();

  // Initialize on mount
  useEffect(() => {
    initialize({
      contractAddress: '0x...',
      rpcUrl: 'wss://...',
      zkeyPaths: { deposit: '/main.zkey', withdraw: '/withdraw.zkey' },
      wasmPaths: { deposit: '/main.wasm', withdraw: '/withdraw.wasm' },
    });
  }, []);

  const handleShield = async () => {
    try {
      const signer = await getSigner(); // Your wallet connection
      const secret = generateSecret();
      const amount = ethers.parseEther('1.0');
      
      const result = await shieldTokens(
        signer,
        amount,
        ethers.ZeroAddress, // Native token
        secret
      );
      
      console.log('Shielded! TX:', result.hash);
    } catch (err) {
      console.error('Shield failed:', err);
    }
  };

  if (!isReady) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <p>Merkle Root: {merkleRoot?.slice(0, 10)}...</p>
      <p>Tree Size: {treeSize}</p>
      <button onClick={handleShield} disabled={isLoading}>
        {isLoading ? 'Processing...' : 'Shield Tokens'}
      </button>
    </div>
  );
}

API Reference

Components

<ShieldedTransferProvider>

Context provider that initializes all shielded transfer functionality.

Prop Type Required Default Description
contractAddress string Yes - Shield contract address
rpcUrl string Yes - WebSocket RPC endpoint
zkeyPaths object Yes - Paths to .zkey files
wasmPaths object Yes - Paths to .wasm files
enableWasmsnark boolean No false Enable faster wasmsnark backend
wasmsnarkPkeyPath string No - Path to binary proving key
interface ZkeyPaths {
  deposit: string;
  withdraw: string;
  asset?: string;
}

interface WasmPaths {
  deposit: string;
  withdraw: string;
  asset?: string;
}

Hooks

useShieldedTransfer()

Main hook for shielded transfer operations.

Returns:

interface UseShieldedTransferReturn {
  // State
  isLoading: boolean;
  isReady: boolean;
  error: Error | null;
  merkleRoot: string | null;
  treeSize: number;
  provider: ethers.Provider | null;
  
  // Initialization
  initialize: (config: ShieldedTransferConfig) => Promise<void>;
  
  // Shield operations
  shieldTokens: (
    signer: ethers.Signer,
    amount: bigint,
    asset: string,
    secret: string,
    nullifier?: string
  ) => Promise<TransactionResult>;
  
  // Unshield operations
  unshieldTokens: (
    signer: ethers.Signer,
    leafIndex: number,
    withdrawnAmount: bigint,
    recipient: string,
    existingSecret: string,
    existingNullifier: string,
    newSecret: string,
    newNullifier: string,
    asset?: string
  ) => Promise<TransactionResult>;
  
  // Merkle tree
  buildMerkleTree: () => Promise<void>;
  refreshTree: () => Promise<void>;
  getMerkleProof: (leafIndex: number) => MerkleProof | null;
  
  // Utilities
  generateSecret: () => string;
  generateNullifier: () => string;
  clearError: () => void;
}

useWalletConnection()

Wallet connection hook for EVM and Polkadot wallets.

const {
  isConnecting,
  isConnected,
  accounts,
  activeAccount,
  error,
  connect,
  disconnect,
  switchAccount,
} = useWalletConnection();

useZKPService()

Direct access to the ZK proof service.

const zkService = useZKPService();

const commitment = zkService.generateFixedIlopCommitment(
  nullifier,
  secret,
  asset,
  amount
);

useMerkleTree()

Access Merkle tree state and proofs.

const { tree, root, size, getProof } = useMerkleTree();

const proof = getProof(leafIndex);
console.log(proof.siblings); // string[254]

Core Classes

ZKPService

Zero-knowledge proof service for commitment and nullifier generation.

import { ZKPService } from '@kusama-shield/shielded-transfers';

const zkService = new ZKPService();

// Generate random values
const secret = zkService.generateRandomSecret();
const nullifier = zkService.generateRandomNullifier();

// Generate commitment (FixedIlop pattern)
const commitment = zkService.generateFixedIlopCommitment(
  nullifier,    // string
  secret,       // string
  asset,        // string (address)
  amount        // bigint
);

// Generate nullifier hash
const nullifierHash = zkService.generateNullifierHash(nullifier);

// Create deposit payload
const payload = zkService.generateFixedIlopDepositPayload(
  nullifier,
  secret,
  asset,
  amount
);

// Store/retrieve deposits
zkService.getDepositInfo(commitment);
zkService.getAllDeposits();
zkService.clearDeposits();

LeanIMT

Incremental Merkle tree implementation (Lean Incremental Merkle Tree).

import { LeanIMT } from '@kusama-shield/shielded-transfers';

const tree = new LeanIMT();

// Insert leaves
tree.insert(BigInt('1234567890...'));
tree.insert(BigInt('9876543210...'));

// Get tree state
console.log(tree.size);   // Number of leaves
console.log(tree.root);   // Current root (bigint)
console.log(tree.depth);  // Tree depth

// Generate proof
const proof = tree.getProof(0);
// { siblings: string[254], root: string, depth: number, leafIndex: number }

// Find leaf
const index = tree.findLeafIndex(BigInt('123...'));

// Reset tree
tree.reset();

Functions

Shield/Deposit

import { shieldTokens, parseAmount } from '@kusama-shield/shielded-transfers';

// Parse human-readable amount
const amount = parseAmount('1.5', 18); // 1.5 ETH -> 1500000000000000000n

// Shield tokens
const tx = await shieldTokens(
  signer,              // ethers.Signer
  contractAddress,     // string
  amount,              // bigint
  asset,               // string (address)
  commitment           // string
);

await tx.wait();

Unshield/Withdraw

import { unshieldTokens } from '@kusama-shield/shielded-transfers';

const tx = await unshieldTokens(
  signer,              // ethers.Signer
  contractAddress,     // string
  payload,             // WithdrawalPayload
  recipient            // string
);

await tx.wait();

Proof Generation

import { 
  zkWithdraw, 
  preloadZkey, 
  preloadWasm 
} from '@kusama-shield/shielded-transfers';

// Pre-load artifacts (optional, improves first-proof latency)
await preloadZkey('/withdraw_0001.zkey');
await preloadWasm('/withdraw.wasm');

// Generate withdrawal proof
const { proof, calldata, publicSignals } = await zkWithdraw({
  withdrawnValue: "1000000000000000000",
  root: "1234567890...",
  treeDepth: "254",
  context: "0",
  asset: "0x...",
  existingValue: "1000000000000000000",
  existingNullifier: "9876543210...",
  existingSecret: "1111111111...",
  newNullifier: "2222222222...",
  newSecret: "3333333333...",
  siblings: [...],  // 254 elements
  leafIndex: "0"
});

Merkle Tree from Contract

import { buildMerkleTreeFromContract } from '@kusama-shield/shielded-transfers';

const tree = await buildMerkleTreeFromContract(
  provider,          // ethers.Provider
  contractAddress,   // string
  abi                // string[] (optional)
);

console.log(`Tree has ${tree.size} leaves, root: ${tree.root}`);

Chain-Specific Functions

The library provides pre-configured functions for popular chains that work out of the box.

Supported Chains

Chain Function Native Token Decimals Shield Contract Version
Paseo AssetHub v7 shieldedPaseoV7 / withdrawPaseoV7 PAS 18 0x3068490C... v7 (linkability fix)
Paseo AssetHub v5 shieldedPaseo / withdrawPaseo PAS 18 0x3099889C... v4
Polkadot AssetHub shieldedPolkadot / withdrawPolkadot DOT 10 0xe55B8544... v4
Kusama AssetHub shieldedKusama / withdrawKusama KSM 12 0xDC805653... v3

Note

: Kusama (KSM) uses 12 decimals, Polkadot (DOT) uses 10 decimals, and Paseo (PAS) uses 18 decimals. The chain-specific functions handle this automatically via the chain config.

Quick Shield/Withdraw

Shield on Paseo

import { shieldedPaseo, PASEO_CONFIG } from '@kusama-shield/shielded-transfers';
import { ethers } from 'ethers';

// Connect wallet
const signer = await getSigner(); // From MetaMask, WalletConnect, etc.

// Shield native tokens (PAS)
const result = await shieldedPaseo(
  ethers.ZeroAddress,  // Native token
  "1.5",               // Amount in human-readable format
  signer
);

console.log('Shield Result:');
console.log('  Secret:', result.secret);           // Save this!
console.log('  Nullifier:', result.nullifier);     // Save this!
console.log('  Commitment:', result.commitment);   // For Merkle tree
console.log('  TX Hash:', result.hash);
console.log('  Explorer:', result.explorerUrl);

Shield ERC20 Tokens on Paseo

// Shield ERC20 token
const result = await shieldedPaseo(
  "0x1234567890123456789012345678901234567890",  // Token address
  "100",                                          // 100 tokens
  signer
);

Withdraw on Paseo

import { withdrawPaseo } from '@kusama-shield/shielded-transfers';

const result = await withdrawPaseo(
  ethers.ZeroAddress,  // Native token
  "1.0",               // Amount to withdraw
  "0xRecipient...",    // Recipient address
  secret,              // From shieldedPaseo result
  nullifier,           // From shieldedPaseo result
  leafIndex,           // Index in Merkle tree
  signer
);

console.log('Withdraw Result:');
console.log('  TX Hash:', result.hash);
console.log('  New Secret:', result.newSecret);
console.log('  New Nullifier:', result.newNullifier);

Shield on Kusama

import { shieldedKusama } from '@kusama-shield/shielded-transfers';

const signer = await getSigner();

// Shield KSM (native token, 12 decimals)
const result = await shieldedKusama(
  ethers.ZeroAddress,  // Native KSM
  "0.5",               // 0.5 KSM
  signer
);

// Note: Kusama uses 12 decimals, not 18!

Withdraw on Kusama

import { withdrawKusama } from '@kusama-shield/shielded-transfers';

const result = await withdrawKusama(
  ethers.ZeroAddress,  // Native KSM
  "0.25",              // 0.25 KSM
  "0xRecipient...",
  secret,
  nullifier,
  leafIndex,
  signer
);

Shield on Polkadot

import { shieldedPolkadot } from '@kusama-shield/shielded-transfers';

const signer = await getSigner();

// Shield DOT (native token, 10 decimals)
const result = await shieldedPolkadot(
  ethers.ZeroAddress,  // Native DOT
  "1.0",               // 1 DOT
  signer
);

// Note: Polkadot uses 10 decimals, not 18!
console.log('Secret:', result.secret);
console.log('Commitment:', result.commitment);
console.log('TX Hash:', result.hash);

Withdraw on Polkadot

import { withdrawPolkadot } from '@kusama-shield/shielded-transfers';

const result = await withdrawPolkadot(
  ethers.ZeroAddress,  // Native DOT
  "0.5",               // 0.5 DOT
  "0xRecipient...",
  secret,
  nullifier,
  leafIndex,
  signer
);

Pallet Asset Shield/Withdraw

For Asset Hub chains, pallet assets (foreign tokens registered in the Assets pallet) require using assetId instead of a token address. The library provides dedicated asset functions.

Chain Shield Withdraw
Paseo shieldAssetPaseo(assetId, amount, signer) withdrawAssetPaseo(assetId, amount, recipient, secret, nullifier, leafIndex, signer)
Polkadot shieldAssetPolkadot(assetId, amount, signer) withdrawAssetPolkadot(assetId, amount, recipient, ...)
Kusama shieldAssetKusama(assetId, amount, signer) withdrawAssetKusama(assetId, amount, recipient, ...)

Shield a Pallet Asset

import { shieldAssetPaseo } from '@kusama-shield/shielded-transfers';
import { ethers } from 'ethers';

const signer = await getSigner();

// Shield 100 PSILV (pallet asset ID 50000867 on Paseo)
const result = await shieldAssetPaseo(50000867, "100", signer);

console.log('Shield Result:');
console.log('  Secret:', result.secret);
console.log('  Nullifier:', result.nullifier);
console.log('  Commitment:', result.commitment);
console.log('  Asset ID:', result.assetId);
console.log('  TX Hash:', result.hash);

The function automatically:

  • Generates the FixedIlop commitment using poseidon3([amount, assetId, poseidon2([nullifier, secret])])
  • Approves the ERC20 precompile for the asset
  • Calls depositAsset(assetId, amount, commitment, nullifierHash)

Withdraw a Pallet Asset

import { withdrawAssetPaseo } from '@kusama-shield/shielded-transfers';

const result = await withdrawAssetPaseo(
  50000867,           // assetId (PSILV)
  "50",               // amount to withdraw
  "0xRecipient...",   // recipient
  secret,             // from shieldAssetPaseo result
  nullifier,          // from shieldAssetPaseo result
  leafIndex,          // index in Merkle tree
  signer
);

console.log('Withdrawn:', result.hash);

v7: Linkability Fix (Paseo AssetHub)

v7 is the latest contract version, deployed on 2026-07-13. It fixes the deposit-withdrawal linkability vulnerability that existed in all previous versions (v4/v5/v6).

What v7 fixes:

  • Removes nullifierHash from the deposit event — the nullifier is only revealed at withdrawal
  • Removes existingCommitment from the withdrawal public signals — the commitment being spent is never in the calldata
  • No deposits[] storage mapping — the contract trusts the ZK proof entirely
  • Known-roots window of 16 — withdrawals accept any recent tree root (prevents griefing)

Privacy guarantee: No shared unique field between deposit and withdrawal. The commitment is public at deposit only (Merkle leaf), the nullifierHash is public at withdrawal only. An observer cannot link any deposit to any withdrawal.

v7 public signals (8): [newCommitmentHash(0), existingNullifierHash(1), contextHash(2), withdrawnValue(3), treeDepth(4), context(5), root(6), asset(7)]

v7 withdraw signature: withdraw(pA, pB, pC, pubSignals[8], recipient) — no asset or amount params (read from pubSignals).

Shield on Paseo v7

import { shieldedPaseoV7, PASEO_V7_CONFIG } from '@kusama-shield/shielded-transfers';
import { ethers } from 'ethers';

const signer = await getSigner();

// Shield native PAS on v7 pool (no nullifierHash in deposit!)
const result = await shieldedPaseoV7(
  ethers.ZeroAddress,  // Native token
  "1.5",               // Amount
  signer
);

console.log('Secret:', result.secret);       // Save this!
console.log('Nullifier:', result.nullifier); // Save this!
console.log('Commitment:', result.commitment);
console.log('TX Hash:', result.hash);
// Note: nullifierHash is NOT sent to the contract at deposit time

Withdraw on Paseo v7

import { withdrawPaseoV7 } from '@kusama-shield/shielded-transfers';

const result = await withdrawPaseoV7(
  "1.0",               // Amount to withdraw
  "0xRecipient...",    // Recipient address
  secret,              // From shieldedPaseoV7 result
  nullifier,           // From shieldedPaseoV7 result
  leafIndex,           // Index in Merkle tree
  signer
);

console.log('TX Hash:', result.hash);
console.log('New Secret:', result.newSecret);
console.log('New Nullifier:', result.newNullifier);

Shield a Pallet Asset on Paseo v7

import { shieldAssetPaseoV7 } from '@kusama-shield/shielded-transfers';

// Shield 100 PSILV (pallet asset ID 50000867)
const result = await shieldAssetPaseoV7(50000867, "100", signer);
console.log('Commitment:', result.commitment);
console.log('TX Hash:', result.hash);

Low-Level v7 Functions

For advanced use cases, the library exports low-level v7 functions:

import {
  depositNativeV7,
  depositAssetV7,
  withdrawV7,
  proxyWithdrawV7,
} from '@kusama-shield/shielded-transfers';

// Deposit native (v7: no nullifierHash param)
const deposit = await depositNativeV7(signer, contractAddress, amount, secret, nullifier);

// Withdraw (v7: no asset/amount params — read from pubSignals)
const tx = await withdrawV7(
  signer,
  contractAddress,
  pA, pB, pC,        // Proof components
  pubSignals,         // bigint[8]
  recipient           // address
);

// Proxy withdraw (routes through SimpleTokenForwarder for extra privacy)
const proxyTx = await proxyWithdrawV7(
  signer,
  contractAddress,
  pA, pB, pC,
  pubSignals,
  recipient
);

v7 Configuration

import { PASEO_V7_CONFIG } from '@kusama-shield/shielded-transfers';

// PASEO_V7_CONFIG includes:
// - contractAddress: 0x3068490C79708D0725E3D4Aa9C35Da708f09071e
// - verifierAddress: 0xFfA17fA82606682BC2AC5F10bAA27569b54a8833
// - wasmPath: ./public/withdraw_phase2_fixed_v7.wasm
// - zkeyPath: ./public/withdraw_phase2_fixed_v7_0001.zkey
// - treeDepth: 128
// - knownRootsWindow: 16

// Use with chain config lookup
import { getChainConfig, CHAIN_CONFIGS } from '@kusama-shield/shielded-transfers';
const v7Config = getChainConfig('paseo-v7');

v7 vs v6 vs v5 Comparison

Property v5 v6 v7
Public signals 7 8 8
Deposit reveals nullifierHash Yes No No
Withdrawal reveals commitment No Yes (pubSignals[3]) No
Linkable? Yes (nullifierHash) Yes (commitment) No
Deposits[] mapping Yes (nullifier-keyed) Yes (commitment-keyed) No (trusts ZK proof)
Known-roots window No No Yes (16 recent roots)
Root griefing possible? Yes Yes No
Withdraw signature (pA, pB, pC, pub[7], asset, amount, recipient) (pA, pB, pC, pub[8], asset, amount, recipient) (pA, pB, pC, pub[8], recipient only)

Examples

The examples/ folder contains runnable scripts:

File Description
run_v7_roundtrip.mjs Full deposit → withdraw roundtrip on v7 Paseo pool
v7_security_test.mjs 10 deposits, 7 withdrawals, 6 replay attack tests
run_v6_roundtrip.mjs Full deposit → withdraw roundtrip on v6 Paseo pool
paseo_v4_roundtrip.mjs v4 roundtrip test
test_v4_proxy_withdrawals.mts v4 proxy withdrawal tests

Run with:

cd shielded-transfers
npm run build
node examples/run_v7_roundtrip.mjs

Gas Estimation

All functions use native ethers.js estimateGas under the hood. You can also estimate gas manually before submitting:

Estimate Deposit Gas

import { ethers } from 'ethers';
import { depositNativeV4, depositAssetV4, getPalletAssetPrecompile } from '@kusama-shield/shielded-transfers';

// Estimate native deposit
const depositInfo = await depositNativeV4(signer, contractAddress, amount, secret, nullifier);
// The tx is already sent - to estimate first, use the contract directly:

const contract = new ethers.Contract(contractAddress, [
  "function depositNative(bytes32 commitment, bytes32 nullifierHash) payable"
], signer);

const gasEstimate = await contract.depositNative.estimateGas(
  ethers.zeroPadValue(ethers.toBeArray(depositInfo.commitment), 32),
  ethers.zeroPadValue(ethers.toBeArray(depositInfo.nullifierHash), 32),
  { value: amount }
);
console.log(`Estimated gas: ${gasEstimate}`);

Estimate Withdraw Gas

import { withdrawNativeV4 } from '@kusama-shield/shielded-transfers';

// Call estimateGas on the v4 function directly:
const contract = new ethers.Contract(contractAddress, [
  "function withdrawNative(uint[2] calldata pA, uint[2][2] calldata pB, uint[2] calldata pC, uint[6] calldata pubSignals, uint256 amount) external"
], signer);

const gasEstimate = await contract.withdrawNative.estimateGas(pA, pB, pC, pubSignals, amount);
console.log(`Estimated withdraw gas: ${gasEstimate}`);

All return native ethers.TransactionResponse objects — use await tx.wait() to confirm, or provider.waitForTransaction(tx.hash) for custom polling.

Custom Chain Configuration

import {
  shieldedGeneric,
  withdrawGeneric,
  type ChainConfig,
} from '@kusama-shield/shielded-transfers';

const customConfig: ChainConfig = {
  name: "My Chain",
  chainId: 12345,
  rpcUrl: "https://my-chain.rpc.io",
  wsUrl: "wss://my-chain.rpc.io",       // optional, for WebSocket connections
  contractAddress: "0x...",
  wasmPath: "./public/withdraw.wasm",   // v4 single-path artifact
  zkeyPath: "./public/withdraw.zkey",
  verifierAddress: "0x...",             // optional, for on-chain verification
  leanIMTAddress: "0x...",              // optional, LeanIMT precompile
  poseidonPrecompile: "0x...",          // optional, Poseidon hash precompile
  treeDepth: 128,                        // Merkle tree depth
  explorerUrl: "https://explorer.my-chain.io",
  nativeCurrency: {
    name: "MyToken",
    symbol: "MYT",
    decimals: 18,
  },
};

// Use custom config
const shieldResult = await shieldedGeneric(
  ethers.ZeroAddress,
  "1.0",
  signer,
  customConfig
);

const withdrawResult = await withdrawGeneric(
  ethers.ZeroAddress,
  "1.0",
  "0xRecipient...",
  secret,
  nullifier,
  leafIndex,
  signer,
  customConfig
);

Utility Functions

Get Chain Config

import { getChainConfig, PASEO_CONFIG } from '@kusama-shield/shielded-transfers';

// Get config by name
const config = getChainConfig('paseo');
console.log(config.rpcUrl);
console.log(config.nativeCurrency.decimals);

// Configs are case-insensitive
getChainConfig('PASEO');     // Works
getChainConfig('Paseo');     // Works
getChainConfig('kusama');    // Works

Format/Parse Amounts

import { formatChainAmount, parseChainAmount } from '@kusama-shield/shielded-transfers';

// Parse human-readable to wei
const wei = parseChainAmount('1.5', 'paseo');  // 1500000000000000000n

// Format wei to human-readable
const formatted = formatChainAmount(wei, 'paseo');  // "1.5"

// Kusama (12 decimals)
const ksmWei = parseChainAmount('0.5', 'kusama');
const ksmFormatted = formatChainAmount(ksmWei, 'kusama');  // "0.5"

Build Merkle Tree

import { buildChainMerkleTree, getChainMerkleProof } from '@kusama-shield/shielded-transfers';

// Build tree for a chain
const tree = await buildChainMerkleTree('paseo');
console.log(`Tree has ${tree.size} leaves`);

// Get proof for a leaf
const proof = await getChainMerkleProof('paseo', leafIndex);
console.log(proof.siblings);  // string[254]
console.log(proof.root);

Chain Configuration Objects

import {
  PASEO_CONFIG,
  POLKADOT_CONFIG,
  KUSAMA_CONFIG,
  MOONBASE_CONFIG,
  CHAIN_CONFIGS,
} from '@kusama-shield/shielded-transfers';

// Access full config
console.log(PASEO_CONFIG.rpcUrl);
console.log(PASEO_CONFIG.contractAddress);
console.log(KUSAMA_CONFIG.nativeCurrency.decimals);
console.log(MOONBASE_CONFIG.explorerUrl);

// All available configs
console.log(Object.keys(CHAIN_CONFIGS));
// ['paseo', 'polkadot', 'kusama', 'moonbase']

Types

import type {
  ZKProof,           // Groth16 proof structure
  FormattedProof,    // Solidity-formatted proof
  DepositInfo,       // Stored deposit data
  WithdrawalPayload, // Withdraw transaction payload
  MerkleProof,       // Merkle tree proof
  ShieldedTransferConfig, // Provider config
  WalletAccount,     // Wallet account info
  TransactionResult, // TX result
} from '@kusama-shield/shielded-transfers';

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    React Application                        │
├─────────────────────────────────────────────────────────────┤
│  useShieldedTransfer  │  useWalletConnection  │  UI Hooks  │
├─────────────────────────────────────────────────────────────┤
│                  ShieldedTransferProvider                   │
├─────────────────────────────────────────────────────────────┤
│   ZKPService   │   LeanIMT   │  Proof Generation  │  Tx    │
├─────────────────────────────────────────────────────────────┤
│                    Web Worker (snarkjs)                     │
│              ┌─────────────────────────────────┐            │
│              │  Witness Calculation + Proving  │            │
│              └─────────────────────────────────┘            │
├─────────────────────────────────────────────────────────────┤
│              Ethers.js │ poseidon-lite │ wasmsnark          │
└─────────────────────────────────────────────────────────────┘

Server Configuration

Required Headers

For multi-threaded WASM proof generation, your server must send these headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Vite Configuration

// vite.config.ts
export default defineConfig({
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
});

Next.js Configuration

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Cross-Origin-Opener-Policy',
            value: 'same-origin',
          },
          {
            key: 'Cross-Origin-Embedder-Policy',
            value: 'require-corp',
          },
        ],
      },
    ];
  },
};

Testing

# Run all tests
npm run test

# Watch mode
npm run test:watch

# Coverage report
npm run test:coverage

# Visual dashboard
npm run test:ui

Test Coverage

Module Tests Coverage
ZKPService 25+ Commitment, nullifier, deposit tracking
LeanIMT 30+ Tree operations, proofs, caching
Shield/Unshield 35+ Transactions, ABI, errors
Proof Generation 20+ ZK proofs, workers, wasmsnark
React Hooks 25+ State, initialization
Integration 10+ End-to-end flows

Performance

Operation Time (avg) Notes
Commitment Generation <1ms Poseidon hash
Merkle Insert <1ms Incremental update
Proof Generation 5-10s Multi-threaded WASM
Proof Generation (single) ~40s Without SharedArrayBuffer

Browser Support

Browser Version Notes
Chrome 90+ Recommended
Firefox 90+
Brave 1.30+
Edge 90+
Safari 15+ ⚠️ Limited SharedArrayBuffer

Recommended: Chrome + Talisman Wallet (Polkadot) or MetaMask (EVM)

Environment Variables

# Optional: WalletConnect project ID
VITE_WALLETCONNECT_PROJECT_ID=your_project_id

# Optional: Default contract address
VITE_SHIELD_CONTRACT_ADDRESS=0x...

# Optional: Default RPC URL
VITE_RPC_URL=wss://...

ethers.js Compatibility

This library is 100% compatible with ethers.js v6. All functions accept native ethers.js types and work seamlessly with your existing ethers.js codebase.

What This Means

  • Native Signer objects - Pass ethers signers directly
  • Native Provider objects - Use any ethers provider
  • Native bigint amounts - Use parseUnits(), parseEther()
  • Native address format - Use ZeroAddress or any hex address
  • Native transaction responses - Get ContractTransactionResponse
  • Native event logs - Parse with ethers Interface

Quick Examples

import { ethers } from 'ethers';
import {
  shieldedPaseo,
  withdrawPaseo,
  parseChainAmount,
  formatChainAmount
} from '@kusama-shield/shielded-transfers';

// Connect with ethers
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

// Use native ethers functions
const amount = ethers.parseUnits("1.5", 18);
const address = ethers.ZeroAddress;

// Shield tokens - returns ethers TransactionResponse
const result = await shieldedPaseo(address, "1.5", signer);

// Wait for transaction - native ethers receipt
const receipt = await provider.waitForTransaction(result.hash);

// Format amounts back to human-readable
const formatted = formatChainAmount(receipt.value, 'paseo');

// All secrets and commitments are plain strings
console.log(result.secret);      // string
console.log(result.commitment);  // string
console.log(result.hash);        // string (TX hash)

Full ethers.js Integration Example

import { ethers } from 'ethers';
import { shieldedKusama, getChainConfig } from '@kusama-shield/shielded-transfers';

async function main() {
  // Create provider and signer
  const provider = new ethers.WebSocketProvider(
    getChainConfig('kusama').rpcUrl
  );
  const wallet = new ethers.Wallet(privateKey, provider);

  // Shield tokens
  const shieldResult = await shieldedKusama(
    ethers.ZeroAddress,  // Native KSM
    "0.5",
    wallet
  );

  // Get transaction receipt
  const receipt = await shieldResult.tx.wait();
  console.log(`Block: ${receipt.blockNumber}`);

  // Parse logs with ethers Interface
  const iface = new ethers.Interface(['event Deposit(address,uint256,uint256)']);
  const log = receipt.logs[0];
  const decoded = iface.parseLog(log);
  console.log(`Commitment: ${decoded.args[2]}`);
}

main();

Type Compatibility

All library types are designed to work with ethers.js:

import type {
  TransactionResult,
  ShieldResult,
  UnshieldResult,
} from '@kusama-shield/shielded-transfers';
import type {
  TransactionResponse,
  TransactionReceipt,
  Signer,
  Provider,
} from 'ethers';

// TransactionResult extends ethers concepts
interface TransactionResult {
  hash: string;           // TX hash (string)
  success: boolean;
  // ... plus optional fields
}

// Works with any ethers Signer
function shield(signer: Signer) {
  return shieldedPaseo(ethers.ZeroAddress, "1.0", signer);
}

Migration from ethers v5

The library uses ethers v6, but migration is easy:

// ethers v5
const amount = ethers.utils.parseEther("1.5");

// ethers v6 (what this library uses)
const amount = ethers.parseEther("1.5");

// ethers v5
const address = ethers.constants.AddressZero;

// ethers v6
const address = ethers.ZeroAddress;

Supported ethers.js Versions

Version Status Notes
v6.x Full Support Recommended
v5.x ⚠️ Partial May need minor adjustments

Common Issues

"SharedArrayBuffer is not defined"

Cause: Missing COOP/COEP headers on your server.

Solution: Add the headers shown in Server Configuration.

"Failed to fetch .zkey file"

Cause: ZK artifacts not served correctly.

Solution: Ensure .zkey and .wasm files are in your public folder and accessible via HTTP.

"Proof generation is slow"

Cause: Running single-threaded without SharedArrayBuffer.

Solution: Enable COOP/COEP headers for multi-threaded proving (5-10s vs 40s).

Contributing

# Clone repository
git clone https://codeberg.org/KusamaShield/Interface
cd Interface/shielded-transfers

# Install dependencies
npm install

# Development build
npm run dev

# Run tests
npm run test

# Build for production
npm run build

License

MIT License - Copyright 2025-2026 Kusama Shield Developers on behalf of the Kusama DAO

See LICENSE for details.

Resources

Acknowledgments


Built with ❤️ for the Kusama community