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)
  • localStorage polyfill 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 via fetch().

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:

  1. Generates random secret and nullifier
  2. Computes V7 commitment: poseidon2([poseidon2([amount, 0]), poseidon2([nullifier, secret])])
  3. Calls depositNative(commitment) on the V7 pool — nullifierHash is NOT sent
  4. 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)

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

OperationGasNotes
Deposit (native)~45,000~270x cheaper than Ethereum
Withdraw (standard)~7,000Groth16 verify + transfer
Withdraw (proxy)~120,000Includes proxy contract deployment

Troubleshooting

  • siblings array must have exactly 128 elements — Tree depth must be 128. The LeanIMT.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_R or the proof will fail.
  • asset parameter — For V7 pallet assets, pass the precompile address (from getPalletAssetPrecompile(assetId)), not the numeric assetId.

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
)
MethodReturnsDescription
get_balance()Tuple[int, str]Wallet balance (wei, formatted)
get_pool_balance()Tuple[int, str]Pool balance
get_tree_size()intMerkle tree leaf count
get_root()intCurrent tree root
is_known_root(root)boolRoot in 16-slot window
deposit(amount_wei, asset_id=0)DictShielded deposit (returns note)
build_tree(recent_blocks=0)LeanIMTBuild tree from on-chain events
withdraw(note, recipient=None)strShielded 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

OperationTime
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