@kusamashield/shielded-transfers
v0.1.2
Published
Zero-knowledge powered shielded transfer library for React applications
Downloads
186
Maintainers
Readme
@kusama-shield/shielded-transfers
Zero-knowledge shielded transfer library for React applications
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-transfersPeer 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'); // WorksFormat/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-corpVite 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:uiTest 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
Signerobjects - Pass ethers signers directly - ✅ Native
Providerobjects - Use any ethers provider - ✅ Native
bigintamounts - UseparseUnits(),parseEther() - ✅ Native address format - Use
ZeroAddressor 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 buildLicense
MIT License - Copyright 2025-2026 Kusama Shield Developers on behalf of the Kusama DAO
See LICENSE for details.
Resources
Acknowledgments
- snarkjs - Groth16 proving system
- poseidon-lite - Poseidon hash implementation
- wasmsnark - Multi-threaded WASM proving
- halo2 - Zero-knowledge proof system
Built with ❤️ for the Kusama community
