npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kusamashield/shielded-transfers

v0.1.2

Published

Zero-knowledge powered shielded transfer library for React applications

Downloads

186

Readme

@kusama-shield/shielded-transfers

Zero-knowledge shielded transfer library for React applications

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 | | 🌲 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 | |-------|----------|--------------|----------|-----------------| | Paseo AssetHub | shieldedPaseo / withdrawPaseo | PAS | 18 | 0x3099889C... | | Polkadot AssetHub | shieldedPolkadot / withdrawPolkadot | DOT | 10 | 0xe55B8544... | | Kusama AssetHub | shieldedKusama / withdrawKusama | KSM | 12 | 0xDC805653... | | Moonbase Alpha | shieldedMoonbase / withdrawMoonbase | DEV | 18 | Custom |

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);

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