Metadata-Version: 2.4
Name: shielded-transfers
Version: 0.2.4
Summary: Python SDK for the Kusama Shield v7 privacy pool (ZK deposits & withdrawals) on Polkadot AssetHub
Author: Kusama Shield
License: MIT
Project-URL: Homepage, https://shield.markets
Project-URL: Documentation, https://kusamashield.codeberg.page
Keywords: shield,privacy,zeroknowledge,zk,polkadot,assethub,shielded,tornado
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: web3>=6.0.0
Requires-Dist: eth-account>=0.9.0
Requires-Dist: eth-abi>=4.0.0
Requires-Dist: eth-utils>=2.0.0
Requires-Dist: light-poseidon-python>=0.1.3
Requires-Dist: substrate-interface>=1.8.0
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# Shielded Transfers Python SDK

A Python library for interacting with the Kusama Shield v7 privacy pool on Polkadot AssetHub (Paseo).

## Overview

This SDK enables shielded (privacy-preserving) deposits and withdrawals using zero-knowledge proofs. It implements the same cryptographic primitives and Merkle tree logic as the Solidity contracts.

## Installation

```bash
cd /home/pi/zk/shielded-transfers-python
pip install -e .
```

Or install dependencies only:
```bash
pip install web3>=6.0.0 eth-account>=0.9.0 eth-abi>=4.0.0 eth-utils>=2.0.0 requests
```

## Requirements

- **light-poseidon-python** - Pure-Python/Rust Poseidon hashing (installed as a
  dependency). No Node.js needed for hashing.
- **Proof generation** — one of:
  - `snarkjs` (default) — Node CLI, `npm install -g snarkjs`
  - `rapidsnark` — faster native binary; user supplies the path
    (see "Proof generation" below).
- **Circuit files** - Located at `/home/pi/zk/shielded-transfers/public/` (override via
  the `SHIELDED_CIRCUIT_DIR` env var or the `circuit_dir` constructor arg):
  - `withdraw_phase2_fixed_v7.wasm`
  - `withdraw_phase2_fixed_v7_0001.zkey`
- **substrateinterface** *(optional, only for `ReviveShieldedClient`)* — requires
  the **modern** API (`Keypair.create_from_uri`, `SubstrateInterface.compose_call`).
  ⚠️ PyPI's `substrateinterface 1.0.0` is too old; use the newer git checkout
  (github.com/polkascan/py-substrate-interface). Imported lazily — the ETH-only
  `ShieldedClient` works without it, and `ReviveShieldedClient` raises a clear
  error if a compatible version isn't installed.

## Poseidon hashing

All hashing uses **`light-poseidon-python`** (a Rust binding), so there is **no
Node.js dependency** for commitments, Merkle tree building, or nullifiers. The
hashes match the on-chain hasher / ZK circuit (circomlibjs-compatible constants).

## Proof generation

Set the engine at construction time (or via the `PROOF_ENGINE` env var):

```python
# snarkjs (default) — needs `snarkjs` Node CLI on PATH
client = ShieldedClient(..., proof_engine="snarkjs")

# rapidsnark — faster; you provide the binary path
client = ShieldedClient(..., proof_engine="rapidsnark",
                        rapidsnark_prover="/usr/local/bin/prover")
```

`rapidsnark_prover` can also be set via the `RAPIDSNARK_PROVER` env var. If the
binary is missing or the engine is invalid, a clear error is raised.

## Two transaction styles

The SDK can submit shielded deposits/withdrawals two ways:

| Style | Class | Account type | Tx mechanism |
|-------|-------|--------------|--------------|
| **ETH** | `ShieldedClient` | ECDSA (`0x...` privkey) | `eth_sendRawTransaction` |
| **Polkadot / Substrate** | `ReviveShieldedClient` | sr25519 (seed / mnemonic) | `revive.call` extrinsic |

Both share the same EVM-based Merkle tree building (`build_tree` / `eth_getLogs`,
or fetch from the Kusama Shield Flask proxy via `/tree-leaves`). Only the
transaction submission differs.

## Quick Start

### Polkadot AssetHub (Mainnet)

```python
from shielded_transfers import ShieldedClient, POLKADOT_ASSET_HUB
import json

client = ShieldedClient(
    rpc_url=POLKADOT_ASSET_HUB["rpc"],
    pool_address=POLKADOT_ASSET_HUB["pool"],
    private_key="0x_your_private_key",
    deployment_block=POLKADOT_ASSET_HUB["deployment_block"],
    native_token="DOT",
)

# Check balances
wallet_bal, _ = client.get_balance()
pool_bal, _ = client.get_pool_balance()
print(f"Wallet: {wallet_bal} wei, Pool: {pool_bal} wei")

# Deposit 1 DOT
note = client.deposit(1 * 10**18)

with open("deposit_note.json", "w") as f:
    json.dump(note, f)

# ... later ...

with open("deposit_note.json") as f:
    note = json.load(f)

tx_hash = client.withdraw(note)
print(f"Withdraw TX: {tx_hash}")
```

### Paseo AssetHub (Testnet)

```python
from shielded_transfers import ShieldedClient, PASEO_ASSET_HUB
import json

client = ShieldedClient(
    rpc_url=PASEO_ASSET_HUB["rpc"],
    pool_address=PASEO_ASSET_HUB["pool"],
    private_key="0x_your_private_key",
    deployment_block=PASEO_ASSET_HUB["deployment_block"],
    native_token="DOT",
)

# Deposit 10 PAS (testnet)
note = client.deposit(10 * 10**18)

with open("deposit_note.json", "w") as f:
    json.dump(note, f)

# Withdraw
with open("deposit_note.json") as f:
    note = json.load(f)

tx_hash = client.withdraw(note, recipient="0x_recipient_address")
print(f"Withdraw TX: {tx_hash}")
```

### Polkadot / Substrate (revive.call) — `ReviveShieldedClient`

For sr25519 (Substrate) accounts — e.g. a polkadot.js browser wallet or Nova
account that cannot sign Ethereum transactions directly.

```python
from shielded_transfers import ReviveShieldedClient, POLKADOT_ASSET_HUB

client = ReviveShieldedClient(
    rpc_url=POLKADOT_ASSET_HUB["rpc"],           # EVM JSON-RPC (reads/tree)
    ws_url="wss://asset-hub-polkadot-rpc.n.dwellir.com",   # Substrate WS (revive)
    pool_address=POLKADOT_ASSET_HUB["pool"],
    substrate_uri="0x_your_sr25519_seed",        # or a mnemonic / "//Alice"
    deployment_block=POLKADOT_ASSET_HUB["deployment_block"],
    native_token="DOT",
    native_decimals=10,                          # DOT = 10, Paseo PAS = 12
)

# One-time mapping of the sr25519 account to its H160 (only needed once)
client.ensure_mapped()

# Deposit 0.01 DOT via revive.call
note = client.deposit_revive(amount_dot=0.01)
print("Deposit TX:", note["tx_hash"])
print("Secret:   ", note["secret"])              # keep private for withdrawal

# Withdraw back via revive.call, fetching the current tree from the Flask proxy
tx_hash = client.withdraw_revive(
    note,
    recipient=client.h160,
    proxy_base_url="https://proxyswap.laissez-faire.trade",
)
print("Withdraw TX:", tx_hash)
```

**Important notes for `revive.call`:**
- The `value` is in **native plancks**, not wei (`1 DOT = 1e10`, Paseo `1 PAS = 1e12`).
- Requires a large weight limit (handled internally) and a non-zero storage
  deposit (~0.1 native units) — the SDK sets these automatically.
- `revive.call` EVM logs are **not** indexed by `eth_getLogs` on AssetHub, so
  the tree is best fetched from the Kusama Shield Flask proxy
  (`proxy_base_url=...` → `GET /tree-leaves/<network>`).

## Configuration

### Constructor Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `rpc_url` | str | Yes | RPC endpoint URL |
| `pool_address` | str | Yes | Shielded pool contract address |
| `private_key` | str | Yes | Account private key |
| `deployment_block` | int | Yes | Block number when pool was deployed |
| `circuit_dir` | Path | No | Directory containing circuit files |
| `native_token` | str | No | Native token name (e.g. "DOT") |
| `proof_engine` | str | No | `"snarkjs"` (default) or `"rapidsnark"` |
| `rapidsnark_prover` | str | No | Path to rapidsnark prover binary (for `proof_engine="rapidsnark"`) |

### Active Deployments

```python
# Polkadot AssetHub (Mainnet)
POLKADOT_ASSET_HUB = {
    "rpc": "https://polkadot-assethub-rpc.laissez-faire.trade",
    "pool": "0x0D694Da746e73D1e255c1894F90e38170db45809",
    "verifier": "0x6A13781E43AEA21918120CD0E7a2ed8614c01e14",
    "poseidon": "0xB8F0C6679D6Cc56450470522Bd96573C3D615052",
    "deployment_block": 18697500,
    "chain_id": 420420419,
}

# Paseo AssetHub (Testnet)
PASEO_ASSET_HUB = {
    "rpc": "https://paseo-assethub-rpc.laissez-faire.trade",
    "pool": "0xbcE09D4De052b2816df1285663ac89528DF45380",
    "verifier": "0xcA4cBc5d31eccd08d393C43aF492F729FF30b685",
    "poseidon": "0x1d165f6fE5A30422E0E2140e91C8A9B800380637",
    "deployment_block": 11273491,
    "chain_id": 420420421,
}
```

## API Reference

### ShieldedClient

```python
client = ShieldedClient(rpc_url, pool_address, private_key, deployment_block)
```

#### Properties

- `client.address` - Account address
- `client.chain_id` - Chain ID
- `client.pool_address` - Pool contract address

#### Methods

##### get_balance()

```python
wei, formatted = client.get_balance()
```
Returns wallet balance in wei and formatted string.

##### get_pool_balance()

```python
wei, formatted = client.get_pool_balance()
```
Returns pool balance in wei and formatted string.

##### get_tree_size()

```python
size = client.get_tree_size()
```
Returns the current Merkle tree size from the contract.

##### get_root()

```python
root = client.get_root()
```
Returns the current Merkle tree root.

##### is_known_root(root)

```python
known = client.is_known_root(root)
```
Checks if a root is in the 16-slot known-roots window.

##### deposit(amount_wei, asset_id=0)

```python
note = client.deposit(amount_wei, asset_id=0)
```
Creates a shielded deposit.

**Parameters:**
- `amount_wei` (int): Amount in wei
- `asset_id` (int): Asset ID (0 for native PAS)

**Returns:**
```python
{
    "secret": "0x...",           # Secret key (keep private!)
    "nullifier": 123...,         # Nullifier for proving
    "nullifier_hash": 456...,    # Hash for double-spend prevention
    "commitment": 789...,        # Public commitment
    "amount_wei": 10000000000000000000,
    "asset_id": 0,
    "tx_hash": "0x...",
    "block_number": 11000000,
    "deposit_block": 11085793,
}
```

##### build_tree(start_block=None)

```python
tree = client.build_tree(start_block=11085793)
```
Builds the Merkle tree from on-chain events.

**Returns:** `LeanIMT` instance

##### withdraw(note, recipient=None)

```python
tx_hash = client.withdraw(note, recipient=None)
```

**Parameters:**
- `note` (dict): Deposit note from `deposit()`
- `recipient` (str): Recipient address (default: self)

**Returns:** Transaction hash

### ReviveShieldedClient

```python
client = ReviveShieldedClient(rpc_url, ws_url, pool_address, substrate_uri, deployment_block)
```

Subclasses `ShieldedClient` and adds Substrate (`revive.call`) tx submission.
Additional constructor parameters:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `ws_url` | str | Yes | Substrate WebSocket RPC URL |
| `substrate_uri` | str | Yes | sr25519 seed (`0x...`), mnemonic, or SURI (`//Alice`) |
| `native_decimals` | int | No | Native token decimals (DOT=10, Paseo=12). Defaults to a heuristic |
| `ss58_format` | int | No | SS58 format (default 42 for AssetHub) |

#### Properties

- `client.ss58_address` - Substrate (SS58) address
- `client.h160` - Derived EVM H160 address
- `client.keypair` - substrateinterface Keypair

#### Methods

##### is_mapped()

```python
mapped = client.is_mapped()
```
Returns whether the sr25519 account is mapped to its H160 for revive.

##### ensure_mapped()

```python
client.ensure_mapped()  # or tx_hash = client.ensure_mapped()
```
Maps the account (one-time, via `revive.mapAccount`) if not already mapped.

##### deposit_revive(amount_dot, wait_for_inclusion=True)

```python
note = client.deposit_revive(0.01)
```
Deposits native tokens via `revive.call`. `amount_dot` is in native units.

##### withdraw_revive(note, recipient, wait_for_inclusion=True, proxy_base_url=None)

```python
tx_hash = client.withdraw_revive(note, recipient=client.h160,
                                 proxy_base_url="https://proxyswap.laissez-faire.trade")
```
Withdraws via `revive.call`. `recipient` is an EVM H160. If `proxy_base_url` is
set, the Merkle tree is fetched from the proxy's `/tree-leaves/<network>`;
otherwise it is built locally via `eth_getLogs` (`build_tree`).

##### fetch_tree_from_proxy(base_url="https://proxyswap.laissez-faire.trade", network=None)

```python
tree = client.fetch_tree_from_proxy(network="polkadot")
```
Fetches the current tree leaves from the Kusama Shield Flask proxy and returns
a `LeanIMT`.

### Commitment Generation

```python
from shielded_transfers import generate_commitment

note = generate_commitment(secret_hex, amount_wei, asset_id)
```

### LeanIMT

```python
from shielded_transfers import LeanIMT

tree = LeanIMT()
tree.insert(leaf)
tree.get_proof(leaf_index)
tree.find_leaf_index(leaf)
tree.root
tree.size
```

## CLI Usage

### Deposit

```bash
shielded-deposit \
  --amount 10 \
  --rpc-url https://paseo-assethub-rpc.laissez-faire.trade \
  --private-key 0x... \
  --output deposit_note.json
```

### Withdraw

```bash
shielded-withdraw \
  --note deposit_note.json \
  --rpc-url https://paseo-assethub-rpc.laissez-faire.trade \
  --private-key 0x... \
  --recipient 0x...
```

## Environment Variables

```bash
export PASEO_RPC_URL="https://paseo-assethub-rpc.laissez-faire.trade"
export PRIVATE_KEY="0x_your_private_key"
```

## Architecture

```
shielded_transfers/
├── __init__.py         # Package exports
├── client.py           # ShieldedClient (ETH / eth_sendRawTransaction)
├── revive.py           # ReviveShieldedClient (Substrate / revive.call)
├── commitment.py       # Commitment generation (Poseidon)
├── tree.py             # LeanIMT Merkle tree implementation
├── poseidon_polkadot.py # Poseidon via light-poseidon-python (Rust)
├── constants.py        # Selectors, BN254 parameters
├── exceptions.py       # Custom exceptions
├── networks.py         # Network configs (Polkadot, Paseo)
└── cli.py              # Command-line interface
```

### Key Components

1. **Commitment Generation**: Uses `light-poseidon-python` (Rust binding, no
   Node.js) to compute:
   - `nullifier = poseidon2(secret, 1)`
   - `nullifier_hash = poseidon1(nullifier)` 
   - `precommitment = poseidon2(nullifier, secret)`
   - `value_asset_hash = poseidon2(amount, asset_id)`
   - `commitment = poseidon2(value_asset_hash, precommitment)`

2. **Merkle Tree**: LeanIMT with 128 levels, matching the Solidity contract

3. **ZK Proof**: Generated with `snarkjs` (default) or `rapidsnark` (optional,
   faster) using the v7 circuit — see "Proof generation" above.

## Known Issues

1. **Withdraw event scanning**: The tree building from events may occasionally miss deposits due to RPC event indexing. The script includes recovery logic to handle this.

2. **Gas estimation**: Some RPCs may fail gas estimation. The SDK uses a default of 500k gas as fallback.

## Error Handling

```python
from shielded_transfers import (
    ShieldedTransfersError,
    DepositError,
    WithdrawError,
    ProofError,
)

try:
    note = client.deposit(amount)
except DepositError as e:
    print(f"Deposit failed: {e}")
except ProofError as e:
    print(f"ZK proof failed: {e}")
```

## Testing

```bash
# Run the original roundtrip script
cd /home/pi/zk/rust_tx_gen
python3 paseo_v7_roundtrip.py --deposit-only
python3 paseo_v7_roundtrip.py --withdraw deposit_note_*.json

# Or use the library
cd /home/pi/zk/shielded-transfers-python
python3 -c "
from shielded_transfers import ShieldedClient, PASEO_ASSET_HUB
client = ShieldedClient(
    rpc_url=PASEO_ASSET_HUB['rpc'],
    pool_address=PASEO_ASSET_HUB['pool'],
    private_key='0x...',
    deployment_block=PASEO_ASSET_HUB['deployment_block'],
)
note = client.deposit(10**18)
print(f'Deposit: {note[\"tx_hash\"]}')
"
```

## Files

| File | Description |
|------|-------------|
| `client.py` | Main SDK client with ETH deposit/withdraw (`eth_sendRawTransaction`) |
| `revive.py` | Substrate `revive.call` deposit/withdraw (`ReviveShieldedClient`) |
| `commitment.py` | Commitment and Poseidon hashing |
| `tree.py` | LeanIMT Merkle tree |
| `constants.py` | Contract addresses, selectors |
| `exceptions.py` | Custom exception classes |
| `cli.py` | Command-line tools |

## Related

- [Original roundtrip script](../rust_tx_gen/paseo_v7_roundtrip.py)
- [Deployment notes](../rust_tx_gen/DEPLOYMENT_NOTES.md)
- [Circuit files](../shielded-transfers/public/)
