Using @kusamashield/shielded-transfers V7 SDK
This guide covers the V7 SDK for Paseo, Polkadot, and Kusama AssetHub.
Library link: https://www.npmjs.com/package/@kusamashield/shielded-transfers
A step-by-step guide to depositing and withdrawing shielded tokens using the V7 pool and 8-signal ZK circuit.
Prerequisites
- Node.js 18+
- An AssetHub account with balance
- Circuit artifacts (included in the npm package)
localStoragepolyfill if running in Node.js (the SDK uses localStorage for Merkle tree caching)
1. Install
npm install @kusamashield/shielded-transfers@0.1.4 ethers
2. Copy Circuit Artifacts
The V7 WASM and proving key files need to be served. For a Node.js script, copy them to your project:
cp node_modules/@kusamashield/shielded-transfers/dist/withdraw_phase2_fixed_v7.wasm ./
cp node_modules/@kusamashield/shielded-transfers/dist/withdraw_phase2_fixed_v7_0001.zkey ./
For browser apps, these go in
public/so the library can fetch them viafetch().
3. Connect
import { ethers } from "ethers";
import { PASEO_CONFIG, POLKADOT_CONFIG, KUSAMA_CONFIG } from "@kusamashield/shielded-transfers";
const provider = new ethers.JsonRpcProvider(POLKADOT_CONFIG.rpcUrl);
const wallet = new ethers.Wallet("0xYOUR_PRIVATE_KEY", provider);
4. V7 Deposit
V7 uses depositNative(bytes32 commitment) — only the commitment is exposed, not the nullifierHash.
import { ZKPService, depositNativeV7 } from "@kusamashield/shielded-transfers";
const zk = new ZKPService();
const amount = ethers.parseEther("1.0");
const deposit = await depositNativeV7(wallet, poolAddress, amount);
// deposit contains: { commitment, secret, nullifier, nullifierHash, tx }
await deposit.tx.wait();
What happens:
- Generates random
secretandnullifier - Computes V7 commitment:
poseidon2([poseidon2([amount, 0]), poseidon2([nullifier, secret])]) - Calls
depositNative(commitment)on the V7 pool — nullifierHash is NOT sent - The contract emits
Deposit(address,bytes32)with only the asset and commitment
5. Build Merkle Tree
import { buildMerkleTreeFromContract, clearMerkleCache } from "@kusamashield/shielded-transfers";
clearMerkleCache(poolAddress);
const tree = await buildMerkleTreeFromContract(provider, poolAddress);
const leafIdx = tree.findLeafIndex(BigInt(deposit.commitment));
const proof = tree.getProof(leafIdx);
6. V7 Withdraw
V7 uses 8 public signals and requires a context hash for replay protection:
const BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
function computeContextHash(address) {
return BigInt(ethers.keccak256(ethers.solidityPacked(["address"], [address]))) % BN254_R;
}
const context = computeContextHash(wallet.address);
const { formattedCall, publicSignals } = await zk.generateV4WithdrawProof({
withdrawnValue: amount.toString(),
root: proof.root,
treeDepth: "128",
context: context.toString(),
asset: "0",
existingValue: amount.toString(),
existingNullifier: deposit.nullifier,
existingSecret: deposit.secret,
newNullifier: zk.generateRandomNullifier().toString(),
newSecret: zk.generateRandomSecret().toString(),
siblings: proof.siblings,
leafIndex: leafIdx.toString(),
}, wasmPath, zkeyPath);
// formattedCall = [pA, pB, pC] pre-formatted for Solidity
// pA: [bigint, bigint]
// pB: [[bigint, bigint], [bigint, bigint]] (transposed for Solidity)
// pC: [bigint, bigint]
const tx = await withdrawV7(wallet, poolAddress,
formattedCall[0], formattedCall[1], formattedCall[2],
publicSignals.map(BigInt), wallet.address);
await tx.wait();
7. V7 Proxy Withdraw
Route withdrawal through a proxy contract for extra sender unlinkability:
import { proxyWithdrawV7 } from "@kusamashield/shielded-transfers";
const tx = await proxyWithdrawV7(wallet, poolAddress,
formattedCall[0], formattedCall[1], formattedCall[2],
publicSignals.map(BigInt), recipientAddress);
await tx.wait();
The proxy withdraw deploys a fresh SimpleTokenForwarder contract per withdrawal, giving the recipient a unique sender address each time.
Complete Roundtrip Example (V7)
import { ethers } from "ethers";
import {
ZKPService, depositNativeV7, withdrawV7,
buildMerkleTreeFromContract, clearMerkleCache,
} from "@kusamashield/shielded-transfers";
const BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
// localStorage polyfill for Node.js
global.localStorage = {
_data: Object.create(null),
getItem(k) { return this._data[k] ?? null; },
setItem(k, v) { this._data[k] = v; },
removeItem(k) { delete this._data[k]; },
clear() { this._data = Object.create(null); },
get length() { return Object.keys(this._data).length; },
key(i) { return Object.keys(this._data)[i] ?? null; },
};
function computeContextHash(address) {
return BigInt(ethers.keccak256(ethers.solidityPacked(["address"], [address]))) % BN254_R;
}
async function roundtrip() {
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const pool = process.env.POOL_ADDRESS;
const wasm = process.env.WASM_PATH || "./public/withdraw_phase2_fixed_v7.wasm";
const zkey = process.env.ZKEY_PATH || "./public/withdraw_phase2_fixed_v7_0001.zkey";
const zk = new ZKPService();
// Deposit
const amount = ethers.parseEther("0.01");
const deposit = await depositNativeV7(wallet, pool, amount);
await deposit.tx.wait();
// Tree
clearMerkleCache(pool);
const tree = await buildMerkleTreeFromContract(provider, pool);
const idx = tree.findLeafIndex(BigInt(deposit.commitment));
const proof = tree.getProof(idx);
// Proof
const ctx = computeContextHash(wallet.address);
const { formattedCall, publicSignals } = await zk.generateV4WithdrawProof({
withdrawnValue: amount.toString(), root: proof.root,
treeDepth: "128", context: ctx.toString(), asset: "0",
existingValue: amount.toString(),
existingNullifier: deposit.nullifier, existingSecret: deposit.secret,
newNullifier: zk.generateRandomNullifier().toString(),
newSecret: zk.generateRandomSecret().toString(),
siblings: proof.siblings, leafIndex: idx.toString(),
}, wasm, zkey);
// Withdraw
const tx = await withdrawV7(wallet, pool,
formattedCall[0], formattedCall[1], formattedCall[2],
publicSignals.map(BigInt), wallet.address);
return tx.wait();
}
Pallet Assets (V7)
For non-native tokens (USDC, USDT, etc.):
import { depositAssetV7, getPalletAssetPrecompile } from "@kusamashield/shielded-transfers";
// Approve first, then deposit
const precompile = getPalletAssetPrecompile(assetId);
const token = new ethers.Contract(precompile, ERC20_ABI, wallet);
await token.approve(poolAddress, amount).then(tx => tx.wait());
const deposit = await depositAssetV7(wallet, poolAddress, assetId, amount, precompile);
await deposit.tx.wait();
Available Functions (V7)
| Function | Purpose |
|---|---|
depositNativeV7(signer, pool, amount, secret?, nullifier?) | Deposit native token |
depositAssetV7(signer, pool, assetId, amount, tokenAddr, secret?, nullifier?) | Deposit pallet asset |
withdrawV7(signer, pool, pA, pB, pC, pubSignals, recipient) | Withdraw (standard) |
proxyWithdrawV7(signer, pool, pA, pB, pC, pubSignals, recipient) | Withdraw (proxy) |
ZKPService.generateV4WithdrawProof(input, wasm, zkey) | Generate Groth16 proof (8 signals) |
ZKPService.generateRandomSecret() | Random Poseidon-compatible secret |
ZKPService.generateRandomNullifier() | Random Poseidon-compatible nullifier |
buildMerkleTreeFromContract(provider, pool) | Rebuild LeanIMT from on-chain events |
clearMerkleCache(pool) | Clear localStorage tree cache |
Chain Configurations (V7)
// Paseo AssetHub (V7)
PASEO_CONFIG: ChainConfig {
contract: 0xbcE09D4De052b2816df1285663ac89528DF45380,
verifier: 0xcA4cBc5d31eccd08d393C43aF492F729FF30b685,
poseidon: 0x1d165f6fE5A30422E0E2140e91C8A9B800380637,
treeDepth: 128,
deploymentBlock: 11273491,
rpcUrl: "https://paseo-assethub-rpc.laissez-faire.trade"
}
// Polkadot AssetHub (V7)
POLKADOT_CONFIG: ChainConfig {
contract: 0x0D694Da746e73D1e255c1894F90e38170db45809,
verifier: 0x6A13781E43AEA21918120CD0E7a2ed8614c01e14,
poseidon: 0xB8F0C6679D6Cc56450470522Bd96573C3D615052,
treeDepth: 128,
deploymentBlock: 18460000,
rpcUrl: "https://polkadot-assethub-rpc.laissez-faire.trade"
}
// Kusama AssetHub
KUSAMA_CONFIG: ChainConfig {
contract: 0x625159459EB6C50C4F4b126A955B18d5c4DCA573,
verifier: 0x66988131CFfd10d2804ffaC93Ac302D0886D7829,
treeDepth: 254,
deploymentBlock: 0,
rpcUrl: "https://kusama-rpc.laissez-faire.trade"
}
V7 Public Signals (8 signals)
pubSignals = [
newCommitmentHash, // [0]
existingNullifierHash, // [1]
contextHash, // [2]
withdrawnValue, // [3]
treeDepth, // [4] = 128
context, // [5]
root, // [6]
asset, // [7] precompile address for pallet assets, 0 for native
]
Gas Estimates (pallet-revive)
| Operation | Gas | Notes |
|---|---|---|
| Deposit (native) | ~45,000 | ~270x cheaper than Ethereum |
| Withdraw (standard) | ~7,000 | Groth16 verify + transfer |
| Withdraw (proxy) | ~120,000 | Includes proxy contract deployment |
Troubleshooting
siblings array must have exactly 128 elements— Tree depth must be 128. TheLeanIMT.getProof()pads automatically.Unknown root— Rebuild the tree from the deployment block. Wait for block confirmations after deposit.deposit already spent— Each nullifier can only be used once. Generate a fresh secret for each deposit.- Proof generation takes ~15s — Normal for snarkjs. Use rapidsnark (C++) for ~4.3s if available.
BN254_R modulus must be applied— The context hash requires% BN254_Ror the proof will fail.assetparameter — For V7 pallet assets, pass the precompile address (fromgetPalletAssetPrecompile(assetId)), not the numeric assetId.
Reference
- Library: codeberg.org/KusamaShield/shielded-transfers
- npm:
@kusamashield/shielded-transfers@0.1.4 - V7 Pool Spec: V7 Shielded Pool
- Network Reference: Network Reference
Python SDK
A Python library for interacting with the Kusama Shield V7 pool. Implements the same cryptographic primitives and Merkle tree logic as the Solidity contracts and TypeScript SDK.
Install
pip install shielded-transfers
Requires: Python 3.8+, web3>=6.0.0, eth-account>=0.9.0, light-poseidon-python>=0.1.3
Quick Start
from shielded_transfers import ShieldedClient
import json
client = ShieldedClient(
rpc_url="https://polkadot-assethub-rpc.laissez-faire.trade",
pool_address="0x0D694Da746e73D1e255c1894F90e38170db45809",
private_key="0x_your_private_key",
deployment_block=18460000,
)
# Check balances
wallet_bal, _ = client.get_balance()
pool_bal, _ = client.get_pool_balance()
print(f"Wallet: {wallet_bal}, Pool: {pool_bal}")
# Deposit 1 DOT
note = client.deposit(1 * 10**18)
# Save note securely
with open("deposit_note.json", "w") as f:
json.dump(note, f)
# ... later ...
# Withdraw using saved note
with open("deposit_note.json") as f:
note = json.load(f)
tx_hash = client.withdraw(note)
print(f"Withdraw TX: {tx_hash}")
Commitment Generation
from shielded_transfers import generate_commitment
note = generate_commitment(
secret_hex="0x" + "a" * 62, # 31 bytes
amount_wei=1000000000000, # 0.000001 DOT
asset_id=0,
)
print(note["commitment"]) # public commitment
print(note["nullifier"]) # nullifier for proof
print(note["nullifier_hash"]) # double-spend prevention
Merkle Tree (LeanIMT)
from shielded_transfers import LeanIMT
tree = LeanIMT(depth=128)
tree.insert(leaf) # Insert leaf
proof = tree.get_proof(leaf_index) # Get Merkle proof
index = tree.find_leaf_index(leaf) # Find leaf position
root = tree.root # Current root
size = tree.size # Number of leaves
Network Configurations
from shielded_transfers import NETWORKS
POLKADOT = {
"rpc": "https://polkadot-assethub-rpc.laissez-faire.trade",
"pool": "0x0D694Da746e73D1e255c1894F90e38170db45809",
"verifier": "0x6A13781E43AEA21918120CD0E7a2ed8614c01e14",
"poseidon": "0xB8F0C6679D6Cc56450470522Bd96573C3D615052",
"deployment_block": 18697500,
"native_token": "DOT",
"chain_id": 420420419,
}
PASEO = {
"rpc": "https://paseo-assethub-rpc.laissez-faire.trade",
"pool": "0xbcE09D4De052b2816df1285663ac89528DF45380",
"verifier": "0xcA4cBc5d31eccd08d393C43aF492F729FF30b685",
"poseidon": "0x1d165f6fE5A30422E0E2140e91C8A9B800380637",
"deployment_block": 11273491,
"native_token": "DOT",
"chain_id": 420420421,
}
KUSAMA = {
"rpc": "https://kusama-assethub-rpc.laissez-faire.trade",
"pool": "0x625159459EB6C50C4F4b126A955B18d5c4DCA573",
"deployment_block": 0,
"native_token": "KSM",
"chain_id": 420420418,
}
API Reference
ShieldedClient
client = ShieldedClient(
rpc_url, # RPC endpoint
pool_address, # Pool contract address
private_key, # Account private key
deployment_block, # Block pool was deployed
native_token="DOT", # Native token symbol
)
| Method | Returns | Description |
|---|---|---|
get_balance() | Tuple[int, str] | Wallet balance (wei, formatted) |
get_pool_balance() | Tuple[int, str] | Pool balance |
get_tree_size() | int | Merkle tree leaf count |
get_root() | int | Current tree root |
is_known_root(root) | bool | Root in 16-slot window |
deposit(amount_wei, asset_id=0) | Dict | Shielded deposit (returns note) |
build_tree(recent_blocks=0) | LeanIMT | Build tree from on-chain events |
withdraw(note, recipient=None) | str | Shielded withdrawal (returns tx hash) |
Error Handling
from shielded_transfers import (
DepositError, WithdrawError, ProofError,
PoseidonError, TreeError, CommitmentError,
)
try:
note = client.deposit(amount)
except DepositError as e:
print(f"Deposit failed: {e}")
except ProofError as e:
print(f"ZK proof failed: {e}")
Performance
| Operation | Time |
|---|---|
| Proof generation | ~17s (snarkjs) / ~4.3s (rapidsnark) |
| Tree build (recent) | ~5s |
| Tree build (full) | ~30s |
| Total withdraw | ~25s |
Times measured on Raspberry Pi 5 (ARM64).
Source
- Package:
shielded-transferson PyPI - Poseidon backend:
light-poseidon-python(Rust, 2x faster than Node.js) - Repo: codeberg.org/KusamaShield/shielded-transfers-python