Skip to content
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
"import": "./dist/esm/providers/index.js",
"require": "./dist/cjs/providers/index.js"
},
"./zk": {
"types": "./dist/esm/zk/index.d.ts",
"import": "./dist/esm/zk/index.js",
"require": "./dist/cjs/zk/index.js"
"./wallet": {
"types": "./dist/esm/wallet/index.d.ts",
"import": "./dist/esm/wallet/index.js",
Expand Down Expand Up @@ -67,6 +71,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"abitype": "^1.3.0",
"@noble/curves": "^1.9.0",
"abitype": "^1.3.0",
"chalk": "^6.0.0",
Expand All @@ -82,6 +87,15 @@
"@vitest/coverage-v8": "^1.6.1",
"typedoc": "^0.28.20",
"typescript": "^5.4.0",
"vitest": "^1.3.1"
},
"peerDependencies": {
"snarkjs": ">=0.7.0"
},
"peerDependenciesMeta": {
"snarkjs": {
"optional": true
}
"vitest": "^1.3.1",
"ws": "^8.16.0"
}
Expand Down
22 changes: 22 additions & 0 deletions src/core/Contract.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { Address, PublicClient, WalletClient, Hash } from 'viem'
import type { Abi, ExtractAbiFunctionNames, ExtractAbiFunction, AbiParametersToPrimitiveTypes, AbiStateMutability } from 'abitype'
import { ValidationError } from '../errors/index.js'
import type { Abi, Address, PublicClient } from 'viem'
import { WhiteChainError } from '../types.js'
import type { Address, PublicClient, WalletClient, Hash } from 'viem'
Expand Down Expand Up @@ -106,6 +109,9 @@ export class Contract<
public readonly publicClient?: PublicClient,
public readonly walletClient?: WalletClient,
) {}

/**
* Strongly typed wrapper for publicClient.readContract
/**
* Representation of a deployed smart contract bound to an address, ABI, and optional clients.
*
Expand Down Expand Up @@ -222,6 +228,16 @@ export class Contract<

const _args = args.length > 0 ? (args[0] as unknown[]) : []

return (this.publicClient as any).readContract({
address: this.address,
abi: this.abi,
functionName,
args: _args,
})
}

/**
* Strongly typed wrapper for walletClient.writeContract
try {
return await (this.publicClient as any).readContract({
address: this.address,
Expand Down Expand Up @@ -250,6 +266,12 @@ export class Contract<

const _args = args.length > 0 ? (args[0] as unknown[]) : []

return (this.walletClient as any).writeContract({
address: this.address,
abi: this.abi,
functionName,
args: _args,
})
try {
return await (this.walletClient as any).writeContract({
address: this.address,
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export * from './constants.js'
export * from './config/networks.js'
export * from './network/provider.js'
export * from './network/BatchProvider.js'
export { Contract } from './core/Contract.js'
export * from './core/TransactionHelper.js'
export { NetworkContext, type NetworkObserver, type NetworkState } from './core/NetworkContext.js'
export { AbiCache, abiCache } from './core/AbiCache.js'
Expand All @@ -63,6 +64,8 @@ export type {

export { TODO } from './types.js'
export * from './errors/index.js'
export * from './storage/index.js'
export * from './zk/index.js'
export * from './errors/WhitechainErrors.js'
export { parseContractError } from './utils/errorHandler.js'
export * from './storage/index.js'
Expand All @@ -87,6 +90,9 @@ export {
} from './providers/IpcProvider.js'

export {
MockProvider,
returns,
} from './testing/MockProvider.js'
RpcProvider,
createRpcProvider,
type RpcProviderOptions,
Expand Down
1 change: 1 addition & 0 deletions src/network/provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { http, type Transport } from 'viem'
import type { WhiteChainConfig, WhiteChainAddresses } from '../types.js'
import type { NetworkProfile } from '../config/networks.js'
import { ValidationError } from '../errors/index.js'
import { TimeoutError, ValidationError } from '../errors/index.js'

type RateLimitListener = () => void
Expand Down
152 changes: 152 additions & 0 deletions src/zk/Prover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* ZkProver — the main public facade for generating Groth16 zk-SNARK proofs.
*
* Abstracts all complexity:
* - Fetching + caching .wasm and .zkey files from a CDN
* - Verifying artifact integrity (SHA-256) before use
* - Offloading proof generation to a Web Worker (no UI freezing)
* - Falling back to main-thread execution in Node.js / environments without Worker
* - Formatting proof output into Solidity-ready calldata
*
* @example
* ```ts
* import { ZkProver } from 'whitechain-sdk'
*
* const prover = new ZkProver({
* wasmUrl: 'https://cdn.example.com/vote.wasm',
* zkeyUrl: 'https://cdn.example.com/vote_final.zkey',
* wasmHash: 'abc123...', // optional SHA-256 integrity hash
* zkeyHash: 'def456...', // optional SHA-256 integrity hash
* })
*
* // Runs in a Web Worker — UI stays responsive
* const calldata = await prover.prove({
* nullifier: '123456',
* voteOption: '1',
* merkleProof: [...],
* })
*
* // Pass directly to the on-chain verifier
* await governanceContract.castVote(calldata.pA, calldata.pB, calldata.pC, calldata.pubSignals)
* ```
*/

import { fetchArtifact } from './artifacts.js'
import { formatCalldata } from './format.js'
import { createWorkerBlobUrl } from './worker.js'
import type { ZkProverOptions, ProofCalldata, SnarkJSModule, Groth16Proof } from './types.js'

export class ZkProver {
private readonly options: ZkProverOptions

constructor(options: ZkProverOptions) {
if (!options.wasmUrl) throw new Error('ZkProver: wasmUrl is required')
if (!options.zkeyUrl) throw new Error('ZkProver: zkeyUrl is required')
this.options = options
}

/**
* Generates a Groth16 zk-SNARK proof for the given circuit input.
*
* The proving pipeline:
* 1. Downloads .wasm and .zkey files in parallel (cached after first call).
* 2. Optionally verifies SHA-256 hashes.
* 3. If `Worker` is available (browser): runs `snarkjs.groth16.fullProve` in a
* Web Worker using Transferable buffers for zero-copy performance.
* 4. If `Worker` is not available (Node.js/SSR): falls back to direct main-thread execution.
* 5. Formats the raw SnarkJS output into Solidity `uint256` calldata.
*
* @param input The private and public inputs required by the circuit.
* @returns `ProofCalldata` ready to pass to the on-chain Groth16 verifier.
*/
async prove(input: Record<string, unknown>): Promise<ProofCalldata> {
// Step 1 & 2: Fetch and verify artifacts in parallel
const [wasm, zkey] = await Promise.all([
fetchArtifact(this.options.wasmUrl, this.options.wasmHash),
fetchArtifact(this.options.zkeyUrl, this.options.zkeyHash),
])

// Step 3: Run in Web Worker if available, else fall back to main thread
const { proof, publicSignals } = typeof Worker !== 'undefined'
? await this._proveInWorker(wasm, zkey, input)
: await this._proveMainThread(wasm, zkey, input)

// Step 4: Format calldata
return formatCalldata(proof, publicSignals)
}

/**
* Runs proof generation in a Web Worker using a Blob URL.
* Buffers are transferred (zero-copy) to the worker thread.
*/
private _proveInWorker(
wasm: Uint8Array,
zkey: Uint8Array,
input: Record<string, unknown>
): Promise<{ proof: Groth16Proof; publicSignals: string[] }> {
return new Promise((resolve, reject) => {
let blobUrl: string | null = null

try {
blobUrl = createWorkerBlobUrl()
} catch {
// If Blob URL creation fails (e.g. missing Blob API), fall back to main thread
this._proveMainThread(wasm, zkey, input).then(resolve).catch(reject)
return
}

const worker = new Worker(blobUrl, { type: 'module' })

worker.onmessage = (event) => {
URL.revokeObjectURL(blobUrl!)
worker.terminate()

const { proof, publicSignals, error } = event.data
if (error) {
reject(new Error(`ZkProver worker error: ${error}`))
} else {
resolve({ proof, publicSignals })
}
}

worker.onerror = (err) => {
URL.revokeObjectURL(blobUrl!)
worker.terminate()
reject(new Error(`ZkProver worker crashed: ${err.message}`))
}

// Transfer buffers (zero-copy) — originals become detached
const wasmCopy = wasm.slice()
const zkeyCopy = zkey.slice()

worker.postMessage(
{ wasmBuffer: wasmCopy.buffer, zkeyBuffer: zkeyCopy.buffer, input },
[wasmCopy.buffer, zkeyCopy.buffer]
)
})
}

/**
* Main-thread fallback for Node.js and SSR environments without Worker.
* Dynamically imports snarkjs so non-ZK users never pay the load cost.
*/
private async _proveMainThread(
wasm: Uint8Array,
zkey: Uint8Array,
input: Record<string, unknown>
): Promise<{ proof: Groth16Proof; publicSignals: string[] }> {
let snarkjs: SnarkJSModule

try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
snarkjs = (await import('snarkjs' as any)) as SnarkJSModule
} catch {
throw new Error(
'snarkjs is not installed. Run: npm install snarkjs\n' +
'snarkjs is an optional peer dependency required only for ZK proof generation.'
)
}

return snarkjs.groth16.fullProve(input, wasm, zkey)
}
}
Loading