ceaser-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # ceaser-mcp
2
+
3
+ MCP server for the [Ceaser](https://ceaser.org) privacy protocol. Shield and unshield ETH privately on Base L2 using ZK proofs, directly from Claude Code or any MCP-compatible AI agent.
4
+
5
+ ## Quick Start
6
+
7
+ ### Claude Code (local, stdio)
8
+
9
+ ```bash
10
+ claude mcp add --transport stdio ceaser -- npx -y ceaser-mcp
11
+ ```
12
+
13
+ Or add to `.mcp.json`:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "ceaser": {
19
+ "command": "npx",
20
+ "args": ["-y", "ceaser-mcp"],
21
+ "env": {
22
+ "CEASER_API_URL": "https://ceaser.org"
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### Claude API Agents (remote HTTP)
30
+
31
+ ```json
32
+ {
33
+ "mcp_servers": [{
34
+ "type": "url",
35
+ "url": "https://ceaser.org/mcp",
36
+ "name": "ceaser"
37
+ }]
38
+ }
39
+ ```
40
+
41
+ ## Tools
42
+
43
+ ### Read-Only (6 tools)
44
+
45
+ | Tool | Description |
46
+ |------|-------------|
47
+ | `ceaser_get_denominations` | Valid deposit amounts with fee breakdown |
48
+ | `ceaser_get_fees` | Fee calculation for any ETH amount |
49
+ | `ceaser_get_merkle_root` | Current Merkle tree root |
50
+ | `ceaser_get_pool_info` | Pool TVL, notes, fees |
51
+ | `ceaser_check_nullifier` | Check if a nullifier is spent |
52
+ | `ceaser_get_status` | Facilitator health and stats |
53
+
54
+ ### Shield (1 tool)
55
+
56
+ | Tool | Description |
57
+ |------|-------------|
58
+ | `ceaser_shield_eth` | Generate ZK proof + unsigned shield transaction |
59
+
60
+ ### Unshield (1 tool)
61
+
62
+ | Tool | Description |
63
+ |------|-------------|
64
+ | `ceaser_unshield` | Generate ZK proof + gasless withdrawal via facilitator |
65
+
66
+ ### Note Management (2 tools)
67
+
68
+ | Tool | Description |
69
+ |------|-------------|
70
+ | `ceaser_list_notes` | List stored privacy notes |
71
+ | `ceaser_import_note` | Import a note from backup string |
72
+
73
+ ## How It Works
74
+
75
+ 1. **Shield**: Agent generates a ZK proof locally, returns an unsigned transaction. User signs and sends from their wallet. A note (containing secrets) is stored locally.
76
+
77
+ 2. **Unshield**: Agent loads the note, rebuilds the Merkle tree from the indexer, generates a burn ZK proof, and submits it to the facilitator for gasless settlement. No wallet needed.
78
+
79
+ 3. **Notes**: Contain the secret and nullifier needed to spend shielded funds. Never transmitted over the MCP stream. Stored at `~/.ceaser-mcp/notes.json`.
80
+
81
+ ## Environment Variables
82
+
83
+ | Variable | Default | Description |
84
+ |----------|---------|-------------|
85
+ | `CEASER_API_URL` | `https://ceaser.org` | Facilitator API base URL |
86
+ | `CEASER_CONTRACT_ADDRESS` | `0x278652...BcD368` | zkWrapper contract address |
87
+ | `CEASER_CHAIN_ID` | `8453` | Chain ID (Base mainnet) |
88
+ | `CEASER_CIRCUIT_URL` | `https://ceaser.org/circuits` | Circuit artifact download URL |
89
+ | `CEASER_DATA_DIR` | `~/.ceaser-mcp` | Data directory for notes and cached circuits |
90
+
91
+ ## Architecture
92
+
93
+ - **Local mode** (stdio): Full 10 tools. Proof generation runs in-process via `@noir-lang/noir_js` + `@aztec/bb.js`.
94
+ - **Remote mode** (HTTP): Read-only 6 tools. Mounted at `/mcp` on the facilitator. No proof generation (too slow for HTTP timeouts).
95
+
96
+ ## Version Alignment
97
+
98
+ These versions must match the Ceaser contracts and frontend exactly:
99
+
100
+ | Package | Version |
101
+ |---------|---------|
102
+ | `@aztec/bb.js` | ^2.1.11 |
103
+ | `@noir-lang/noir_js` | 1.0.0-beta.18 |
104
+ | `circomlibjs` | ^0.1.7 |
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Ceaser MCP Server - CLI entry point.
5
+ *
6
+ * Starts the MCP server with stdio transport for local use with Claude Code.
7
+ *
8
+ * Usage:
9
+ * npx ceaser-mcp
10
+ * claude mcp add --transport stdio ceaser -- npx -y ceaser-mcp
11
+ */
12
+
13
+ import { startStdio } from '../src/transport/stdio.js';
14
+
15
+ startStdio();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "ceaser-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Ceaser privacy protocol - shield and unshield ETH privately on Base L2",
5
+ "type": "module",
6
+ "bin": {
7
+ "ceaser-mcp": "./bin/ceaser-mcp.js"
8
+ },
9
+ "main": "src/server.js",
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/ceaser-mcp.js"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/Zyra-V21/ceaaser-privacy.git",
21
+ "directory": "ceaser-mcp"
22
+ },
23
+ "author": "ZYRKOM",
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.12.0",
26
+ "@aztec/bb.js": "^2.1.11",
27
+ "@noir-lang/noir_js": "1.0.0-beta.18",
28
+ "circomlibjs": "^0.1.7",
29
+ "ethers": "^6.9.0",
30
+ "zod": "^3.23.0"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "keywords": [
36
+ "mcp",
37
+ "privacy",
38
+ "zk-proofs",
39
+ "ethereum",
40
+ "base-l2",
41
+ "ceaser",
42
+ "claude",
43
+ "ai-agent"
44
+ ],
45
+ "license": "MIT"
46
+ }
@@ -0,0 +1,37 @@
1
+ // BN254 scalar field
2
+ export const SNARK_SCALAR_FIELD = BigInt(
3
+ '21888242871839275222246405745257275088548364400416034343698204186575808495617'
4
+ );
5
+
6
+ // Protocol fee (basis points)
7
+ export const PROTOCOL_FEE_BPS = 25n;
8
+ export const BPS_DENOMINATOR = 10000n;
9
+
10
+ // Merkle tree depth
11
+ export const TREE_LEVELS = 24;
12
+
13
+ // Valid denominations (ETH, in wei)
14
+ export const DENOMINATIONS = [
15
+ { label: '0.001 ETH', value: '0.001', wei: '1000000000000000' },
16
+ { label: '0.01 ETH', value: '0.01', wei: '10000000000000000' },
17
+ { label: '0.1 ETH', value: '0.1', wei: '100000000000000000' },
18
+ { label: '1 ETH', value: '1', wei: '1000000000000000000' },
19
+ { label: '10 ETH', value: '10', wei: '10000000000000000000' },
20
+ { label: '100 ETH', value: '100', wei: '100000000000000000000' },
21
+ ];
22
+
23
+ // Contract ABI (minimal, for building unsigned txs)
24
+ export const ZKWRAPPER_ABI = [
25
+ 'function shieldETH(bytes calldata proof, bytes32 commitment, uint256 amount) external payable',
26
+ 'function shieldERC20(bytes calldata proof, bytes32 commitment, uint256 amount, uint256 assetId) external',
27
+ 'function unshield(bytes calldata proof, bytes32 nullifierHash, uint256 amount, uint256 assetId, address recipient, bytes32 root) external',
28
+ 'function privateTransfer(bytes calldata proof, bytes32 nullifierHash, bytes32 newCommitment, bytes32 root, uint256 assetId) external',
29
+ 'function getLastRoot() external view returns (bytes32)',
30
+ 'function isKnownRoot(bytes32 root) external view returns (bool)',
31
+ 'function totalNotesCreated() external view returns (uint64)',
32
+ 'function totalLocked(uint256 assetId) external view returns (uint256)',
33
+ 'function nullifiers(bytes32) external view returns (bool)',
34
+ 'event Shield(bytes32 indexed commitment, uint32 leafIndex, uint256 indexed assetId, uint256 timestamp)',
35
+ 'event PrivateTransfer(bytes32 indexed nullifierHash, bytes32 indexed newCommitment, uint256 indexed assetId, uint32 newLeafIndex)',
36
+ 'event Unshield(bytes32 indexed nullifierHash, address indexed recipient, uint256 indexed assetId)',
37
+ ];
package/src/lib/api.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * HTTP client for the Ceaser facilitator API.
3
+ */
4
+
5
+ import { config } from './config.js';
6
+ import { logError } from './log.js';
7
+
8
+ function apiUrl(path) {
9
+ return `${config.apiUrl}${path}`;
10
+ }
11
+
12
+ async function get(path) {
13
+ const url = apiUrl(path);
14
+ const res = await fetch(url);
15
+ if (!res.ok) {
16
+ const body = await res.text().catch(() => '');
17
+ throw new Error(`API ${res.status}: ${body || res.statusText}`);
18
+ }
19
+ return res.json();
20
+ }
21
+
22
+ async function post(path, body) {
23
+ const url = apiUrl(path);
24
+ const res = await fetch(url, {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(body),
28
+ });
29
+ if (!res.ok) {
30
+ const text = await res.text().catch(() => '');
31
+ throw new Error(`API ${res.status}: ${text || res.statusText}`);
32
+ }
33
+ return res.json();
34
+ }
35
+
36
+ // -- Read-only endpoints --
37
+
38
+ export async function getMerkleRoot() {
39
+ return get('/api/ceaser/merkle-root');
40
+ }
41
+
42
+ export async function checkNullifier(hash) {
43
+ return get(`/api/ceaser/nullifier/${encodeURIComponent(hash)}`);
44
+ }
45
+
46
+ export async function getPoolInfo(assetId = '0') {
47
+ return get(`/api/ceaser/pool/${encodeURIComponent(assetId)}`);
48
+ }
49
+
50
+ export async function getFacilitatorStatus() {
51
+ return get('/status');
52
+ }
53
+
54
+ export async function getFees(amountWei) {
55
+ return get(`/api/ceaser/fees/${amountWei}`);
56
+ }
57
+
58
+ // -- Indexer endpoints --
59
+
60
+ export async function getIndexerStatus() {
61
+ return get('/api/ceaser/indexer/status');
62
+ }
63
+
64
+ export async function getIndexerCommitments(offset = 0, limit = 1000) {
65
+ return get(`/api/ceaser/indexer/commitments?offset=${offset}&limit=${limit}`);
66
+ }
67
+
68
+ export async function getIndexerCommitment(index) {
69
+ return get(`/api/ceaser/indexer/commitment/${index}`);
70
+ }
71
+
72
+ // -- Write endpoints --
73
+
74
+ export async function settle(proof, nullifierHash, amount, assetId, recipient, root) {
75
+ return post('/settle', {
76
+ protocol: 'ceaser',
77
+ network: 'eip155:8453',
78
+ payload: {
79
+ proof,
80
+ nullifierHash,
81
+ amount,
82
+ assetId,
83
+ recipient,
84
+ root,
85
+ },
86
+ });
87
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Circuit artifact management.
3
+ * Downloads compiled Noir circuit JSONs from ceaser.org and caches them locally.
4
+ * Verifies SHA-256 integrity to prevent circuit substitution attacks.
5
+ */
6
+
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { createHash } from 'crypto';
10
+ import { config } from './config.js';
11
+ import { log } from './log.js';
12
+
13
+ const CIRCUIT_NAMES = ['shield', 'burn'];
14
+
15
+ // Pinned SHA-256 hashes for circuit integrity verification
16
+ const CIRCUIT_HASHES = {
17
+ shield: 'd366b60e6436fdfa329cf0b4af49c4176c06faf11f20f6e8c94344942b8acbeb',
18
+ burn: '4dd11647a511a9cefa25fbc2dd82a391833a4ecdc378d2096495557422f5ddce',
19
+ };
20
+
21
+ function getCircuitsDir() {
22
+ const dir = join(config.dataDir, 'circuits');
23
+ if (!existsSync(dir)) {
24
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
25
+ }
26
+ return dir;
27
+ }
28
+
29
+ function sha256(data) {
30
+ return createHash('sha256').update(data).digest('hex');
31
+ }
32
+
33
+ /**
34
+ * Download a circuit JSON if not already cached.
35
+ * Verifies SHA-256 hash against pinned value.
36
+ */
37
+ async function downloadCircuit(name) {
38
+ const circuitsDir = getCircuitsDir();
39
+ const localPath = join(circuitsDir, `${name}.json`);
40
+
41
+ if (existsSync(localPath)) {
42
+ const cached = readFileSync(localPath, 'utf8');
43
+ const hash = sha256(cached);
44
+ if (hash !== CIRCUIT_HASHES[name]) {
45
+ log(`Circuit ${name} cache corrupted (hash mismatch), re-downloading`);
46
+ } else {
47
+ log(`Circuit ${name} found in cache (hash verified)`);
48
+ return JSON.parse(cached);
49
+ }
50
+ }
51
+
52
+ const url = `${config.circuitBaseUrl}/${name}.json`;
53
+ log(`Downloading circuit ${name} from ${url}...`);
54
+
55
+ const response = await fetch(url);
56
+ if (!response.ok) {
57
+ throw new Error(`Failed to download ${name} circuit: ${response.status} ${response.statusText}`);
58
+ }
59
+
60
+ const text = await response.text();
61
+
62
+ // Verify integrity
63
+ const hash = sha256(text);
64
+ if (hash !== CIRCUIT_HASHES[name]) {
65
+ throw new Error(
66
+ `Circuit ${name} integrity check failed. Expected ${CIRCUIT_HASHES[name]}, got ${hash}. ` +
67
+ 'This could indicate a compromised source. Aborting.'
68
+ );
69
+ }
70
+
71
+ // Validate JSON structure
72
+ const circuit = JSON.parse(text);
73
+ if (!circuit.bytecode) {
74
+ throw new Error(`Invalid circuit artifact for ${name}: missing bytecode`);
75
+ }
76
+
77
+ writeFileSync(localPath, text, 'utf8');
78
+ log(`Circuit ${name} cached at ${localPath} (hash verified)`);
79
+
80
+ return circuit;
81
+ }
82
+
83
+ /**
84
+ * Ensure all required circuits are downloaded and return them.
85
+ */
86
+ export async function loadCircuits() {
87
+ const circuits = {};
88
+ for (const name of CIRCUIT_NAMES) {
89
+ circuits[name] = await downloadCircuit(name);
90
+ }
91
+ return circuits;
92
+ }
93
+
94
+ /**
95
+ * Load a single circuit by name.
96
+ */
97
+ export async function loadCircuit(name) {
98
+ if (!CIRCUIT_NAMES.includes(name)) {
99
+ throw new Error(`Unknown circuit: ${name}. Valid: ${CIRCUIT_NAMES.join(', ')}`);
100
+ }
101
+ return downloadCircuit(name);
102
+ }
@@ -0,0 +1,19 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+
4
+ export const config = {
5
+ // Facilitator API base URL
6
+ apiUrl: process.env.CEASER_API_URL || 'https://ceaser.org',
7
+
8
+ // Contract address (Base mainnet)
9
+ contractAddress: process.env.CEASER_CONTRACT_ADDRESS || '0x278652aA8383cBa29b68165926d0534e52BcD368',
10
+
11
+ // Chain ID (Base mainnet = 8453)
12
+ chainId: parseInt(process.env.CEASER_CHAIN_ID || '8453'),
13
+
14
+ // Data directory for cached circuits and notes
15
+ dataDir: process.env.CEASER_DATA_DIR || join(homedir(), '.ceaser-mcp'),
16
+
17
+ // Circuit artifact URLs (served by facilitator / ceaser.org)
18
+ circuitBaseUrl: process.env.CEASER_CIRCUIT_URL || 'https://ceaser.org/circuits',
19
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Cryptographic primitives for Ceaser protocol.
3
+ * Poseidon hashing over BN254 using circomlibjs (same as Noir stdlib).
4
+ */
5
+
6
+ import { buildPoseidon } from 'circomlibjs';
7
+ import { randomBytes } from 'crypto';
8
+ import { keccak256, toUtf8Bytes } from 'ethers';
9
+ import { SNARK_SCALAR_FIELD, TREE_LEVELS } from '../constants.js';
10
+
11
+ let poseidon;
12
+ let F;
13
+
14
+ /**
15
+ * Initialize Poseidon (lazy, cached).
16
+ */
17
+ export async function initPoseidon() {
18
+ if (!poseidon) {
19
+ poseidon = await buildPoseidon();
20
+ F = poseidon.F;
21
+ }
22
+ return { poseidon, F };
23
+ }
24
+
25
+ /**
26
+ * Generate a cryptographically secure random BN254 field element.
27
+ */
28
+ export function randomField() {
29
+ const bytes = randomBytes(31);
30
+ return BigInt('0x' + bytes.toString('hex')) % SNARK_SCALAR_FIELD;
31
+ }
32
+
33
+ /**
34
+ * Poseidon hash with N inputs.
35
+ */
36
+ export async function poseidonHash(inputs) {
37
+ const { poseidon, F } = await initPoseidon();
38
+ const hash = poseidon(inputs.map((x) => F.e(x)));
39
+ return F.toObject(hash);
40
+ }
41
+
42
+ /**
43
+ * Compute commitment = Poseidon(secret, nullifier, amount, assetId)
44
+ */
45
+ export async function computeCommitment(secret, nullifier, amount, assetId = 0n) {
46
+ return poseidonHash([secret, nullifier, amount, assetId]);
47
+ }
48
+
49
+ /**
50
+ * Compute nullifierHash = Poseidon(nullifier, leafIndex)
51
+ */
52
+ export async function computeNullifierHash(nullifier, leafIndex) {
53
+ return poseidonHash([nullifier, leafIndex]);
54
+ }
55
+
56
+ /**
57
+ * Zero value matching Solidity: keccak256("zkETH") % SNARK_SCALAR_FIELD
58
+ */
59
+ export function getZeroValue() {
60
+ const hash = keccak256(toUtf8Bytes('zkETH'));
61
+ return BigInt(hash) % SNARK_SCALAR_FIELD;
62
+ }
63
+
64
+ /**
65
+ * Format a BigInt as a 0x-prefixed 32-byte hex string (bytes32).
66
+ */
67
+ export function toBytes32(value) {
68
+ return '0x' + BigInt(value).toString(16).padStart(64, '0');
69
+ }
70
+
71
+ /**
72
+ * Format UltraHonk proof (Uint8Array) to hex string for Solidity.
73
+ */
74
+ export function formatProofHex(proof) {
75
+ return '0x' + Array.from(proof).map((b) => b.toString(16).padStart(2, '0')).join('');
76
+ }
package/src/lib/log.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Logging to stderr (never stdout, which is the MCP JSON-RPC stream).
3
+ */
4
+
5
+ export function log(...args) {
6
+ process.stderr.write(`[ceaser-mcp] ${args.join(' ')}\n`);
7
+ }
8
+
9
+ export function logError(...args) {
10
+ process.stderr.write(`[ceaser-mcp] ERROR: ${args.join(' ')}\n`);
11
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Incremental Poseidon Merkle tree (24 levels).
3
+ *
4
+ * Mirrors Solidity MerkleTreeComponent filledSubtrees pattern for O(log n) inserts.
5
+ * Also supports getProof() for generating ZK proofs (path elements + indices).
6
+ */
7
+
8
+ import { TREE_LEVELS } from '../constants.js';
9
+ import { initPoseidon, poseidonHash, getZeroValue } from './crypto.js';
10
+
11
+ export class MerkleTree {
12
+ constructor() {
13
+ this.levels = TREE_LEVELS;
14
+ this.zeros = [];
15
+ this.filledSubtrees = [];
16
+ this.leaves = [];
17
+ this.root = null;
18
+ this._initialized = false;
19
+ }
20
+
21
+ async init() {
22
+ if (this._initialized) return;
23
+
24
+ await initPoseidon();
25
+
26
+ let currentZero = getZeroValue();
27
+
28
+ for (let i = 0; i < this.levels; i++) {
29
+ this.zeros.push(currentZero);
30
+ this.filledSubtrees.push(currentZero);
31
+ currentZero = await poseidonHash([currentZero, currentZero]);
32
+ }
33
+
34
+ this.root = currentZero;
35
+ this._initialized = true;
36
+ }
37
+
38
+ /**
39
+ * Insert a leaf (commitment) into the tree.
40
+ * @param {BigInt|string} leaf
41
+ * @returns {{ leafIndex: number, root: BigInt }}
42
+ */
43
+ async insert(leaf) {
44
+ if (!this._initialized) await this.init();
45
+
46
+ const leafBigInt = BigInt(leaf);
47
+ const leafIndex = this.leaves.length;
48
+ this.leaves.push(leafBigInt);
49
+
50
+ let currentIndex = leafIndex;
51
+ let currentHash = leafBigInt;
52
+
53
+ for (let i = 0; i < this.levels; i++) {
54
+ if (currentIndex % 2 === 0) {
55
+ this.filledSubtrees[i] = currentHash;
56
+ currentHash = await poseidonHash([currentHash, this.zeros[i]]);
57
+ } else {
58
+ currentHash = await poseidonHash([this.filledSubtrees[i], currentHash]);
59
+ }
60
+ currentIndex = Math.floor(currentIndex / 2);
61
+ }
62
+
63
+ this.root = currentHash;
64
+ return { leafIndex, root: this.root };
65
+ }
66
+
67
+ /**
68
+ * Generate a Merkle proof for a given leaf index.
69
+ * Returns pathElements (siblings) and pathIndices (0=left, 1=right).
70
+ */
71
+ async getProof(leafIndex) {
72
+ if (!this._initialized) await this.init();
73
+
74
+ if (leafIndex >= this.leaves.length) {
75
+ throw new Error(`Leaf index ${leafIndex} not found`);
76
+ }
77
+
78
+ const pathElements = [];
79
+ const pathIndices = [];
80
+ let currentIndex = leafIndex;
81
+
82
+ for (let i = 0; i < this.levels; i++) {
83
+ const isRight = currentIndex % 2 === 1;
84
+ pathIndices.push(isRight ? 1 : 0);
85
+
86
+ const siblingIndex = isRight ? currentIndex - 1 : currentIndex + 1;
87
+ const siblingStart = siblingIndex * (1 << i);
88
+
89
+ if (siblingStart >= this.leaves.length) {
90
+ pathElements.push(this.zeros[i]);
91
+ } else {
92
+ const sibling = await this._getNodeHash(i, isRight ? currentIndex - 1 : currentIndex + 1);
93
+ pathElements.push(sibling);
94
+ }
95
+
96
+ currentIndex = Math.floor(currentIndex / 2);
97
+ }
98
+
99
+ return { pathElements, pathIndices };
100
+ }
101
+
102
+ /**
103
+ * Recursively compute the hash of a node at a given level and index.
104
+ */
105
+ async _getNodeHash(level, index) {
106
+ if (level === 0) {
107
+ return index < this.leaves.length ? this.leaves[index] : this.zeros[0];
108
+ }
109
+
110
+ const leftChild = index * 2;
111
+ const rightChild = index * 2 + 1;
112
+
113
+ const leftHash = await this._getNodeHash(level - 1, leftChild);
114
+ const rightHash = await this._getNodeHash(level - 1, rightChild);
115
+
116
+ return poseidonHash([leftHash, rightHash]);
117
+ }
118
+
119
+ getRoot() {
120
+ return this.root;
121
+ }
122
+
123
+ getLeafCount() {
124
+ return this.leaves.length;
125
+ }
126
+ }