> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nullark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up the Nullark SDK

Create a Nullark client with verified proving files and chain reads from one runtime.

Create the client once, then use it for deposit, balance, restore, and withdrawal. Keep the proving files and every chain adapter pointed at the runtime returned by the SDK.

## Create the client

```ts
import {
  Nullark,
  generateBundledChildDepositGroth16Proof,
  generateWithdrawalGroth16Proof,
  getCurrentRuntime,
  type BundledChildDepositReadResult,
  type HexString,
  type NullarkCurrentRuntime,
  type NullarkOptions,
  type ProverRunner
} from "@nullark/sdk";
import {
  resolveProverArtifacts,
  verifyLocalProverArtifacts
} from "@nullark/sdk/node";
import { groth16 } from "snarkjs";
import { createPublicClient, http, parseAbiItem } from "viem";

const MAX_LOGS_PER_MULTI_BLOCK_QUERY = 20_000;

const COMMITMENT_INSERTED = parseAbiItem(
  "event CommitmentInserted(bytes32 indexed commitment, uint256 amount, uint256 indexed leafIndex, bytes encryptedPayload)"
);

export type NullarkChainAdapters = Pick<
  NullarkOptions,
  "resolveMembership" | "isNullifierSpent" | "runtimeFeeReadClient"
>;

export type NullarkDepositLogClient = {
  getChainId(): Promise<number>;
  getBlockNumber(): Promise<bigint>;
  getLogs(input: {
    address: HexString;
    event: typeof COMMITMENT_INSERTED;
    fromBlock: bigint;
    toBlock: bigint;
  }): Promise<unknown>;
};

export async function loadProvingContext(artifactDir: string) {
  const runtime = getCurrentRuntime();
  const artifacts = resolveProverArtifacts(runtime, {
    mode: "local-artifact-dir",
    artifactDir
  });
  const artifactBinding = await verifyLocalProverArtifacts(runtime, artifacts);

  if (!artifactBinding.trusted) {
    throw new Error(`Proving files rejected: ${artifactBinding.reason}`);
  }

  const proverRunner: ProverRunner<Record<string, unknown>> = {
    fullProve: (witness, wasmPath, zkeyPath) =>
      groth16.fullProve(witness, wasmPath, zkeyPath)
  };

  return { runtime, artifacts, artifactBinding, proverRunner };
}

export async function createNullarkClient(input: {
  artifactDir: string;
  adapters: NullarkChainAdapters;
}): Promise<Nullark> {
  const context = await loadProvingContext(input.artifactDir);
  const publicClient = createPublicClient({ transport: http(context.runtime.rpcUrl) });

  return new Nullark({
    runtime: context.runtime,
    ...input.adapters,
    readDeposits: ({ runtime }) =>
      readBundledChildDepositEvents(publicClient as unknown as NullarkDepositLogClient, runtime),
    localDepositProver: ({ binding }) =>
      generateBundledChildDepositGroth16Proof({
        binding,
        artifacts: context.artifacts,
        artifactBinding: context.artifactBinding,
        proverRunner: context.proverRunner
      }),
    localWithdrawalProver: ({ witnessBundle }) =>
      generateWithdrawalGroth16Proof({
        witness: witnessBundle.witness,
        intent: witnessBundle.intent,
        artifacts: context.artifacts,
        artifactBinding: context.artifactBinding,
        proverRunner: context.proverRunner
      })
  });
}

export async function readBundledChildDepositEvents(
  client: NullarkDepositLogClient,
  runtime: NullarkCurrentRuntime
): Promise<BundledChildDepositReadResult> {
  const chainId = await client.getChainId();
  if (chainId !== runtime.chainId) {
    throw new Error("Deposit-event reader connected to the wrong chain.");
  }
  const latestBlock = await client.getBlockNumber();
  const deploymentBlock = BigInt(runtime.poolDeploymentBlock);
  if (latestBlock < deploymentBlock) {
    throw new Error("Deposit-event reader returned a block before the pool deployment.");
  }

  const logs = await readDepositLogRange(client, runtime, deploymentBlock, latestBlock);
  const events: BundledChildDepositReadResult["events"][number][] = [];
  const seenLogs = new Set<string>();

  for (const value of logs) {
    const decoded = decodeDepositLog(value, runtime, deploymentBlock, latestBlock);
    if (seenLogs.has(decoded.id)) {
      throw new Error("Deposit-event range scan returned a duplicate log.");
    }
    seenLogs.add(decoded.id);
    events.push(decoded.event);
  }

  return {
    source: {
      chainId,
      pool: runtime.pool,
      runtimeId: runtime.runtimeId,
      runtimeIdHash: runtime.runtimeIdHash,
      templateSetHash: runtime.templateSetHash
    },
    events
  };
}

async function readDepositLogRange(
  client: NullarkDepositLogClient,
  runtime: NullarkCurrentRuntime,
  fromBlock: bigint,
  toBlock: bigint
): Promise<readonly unknown[]> {
  let logs: unknown;
  try {
    logs = await client.getLogs({
      address: runtime.pool,
      event: COMMITMENT_INSERTED,
      fromBlock,
      toBlock
    });
  } catch (error) {
    if (fromBlock === toBlock || !isOversizedLogQuery(error)) throw error;
    return splitLogRange(client, runtime, fromBlock, toBlock);
  }
  if (!Array.isArray(logs)) throw new Error("Deposit-event reader returned a malformed log array.");
  if (fromBlock < toBlock && logs.length >= MAX_LOGS_PER_MULTI_BLOCK_QUERY) {
    return splitLogRange(client, runtime, fromBlock, toBlock);
  }
  return logs;
}

async function splitLogRange(
  client: NullarkDepositLogClient,
  runtime: NullarkCurrentRuntime,
  fromBlock: bigint,
  toBlock: bigint
): Promise<readonly unknown[]> {
  const midpoint = (fromBlock + toBlock) / 2n;
  const left = await readDepositLogRange(client, runtime, fromBlock, midpoint);
  const right = await readDepositLogRange(client, runtime, midpoint + 1n, toBlock);
  return [...left, ...right];
}

function isOversizedLogQuery(error: unknown): boolean {
  let current = error;
  for (let depth = 0; depth < 6 && current; depth += 1) {
    if (!isRecord(current)) break;
    if (current.code === -32020) return true;
    if (
      typeof current.message === "string" &&
      /response too large|query.*too large|more than.*logs|maximum.*logs|context deadline exceeded/i.test(current.message)
    ) {
      return true;
    }
    current = current.cause;
  }
  return false;
}

function decodeDepositLog(
  value: unknown,
  runtime: NullarkCurrentRuntime,
  deploymentBlock: bigint,
  latestBlock: bigint
) {
  if (!isRecord(value)) throw new Error("Deposit-event reader returned a malformed log.");
  if (typeof value.address !== "string" || value.address.toLowerCase() !== runtime.pool.toLowerCase()) {
    throw new Error("Deposit-event reader returned a log from the wrong pool.");
  }
  if (value.removed !== false) throw new Error("Deposit-event reader returned a removed log.");
  if (
    typeof value.blockNumber !== "bigint" ||
    typeof value.logIndex !== "number" ||
    !Number.isSafeInteger(value.logIndex) ||
    value.logIndex < 0
  ) {
    throw new Error("Deposit-event reader returned a malformed log position.");
  }
  const blockNumber = value.blockNumber;
  const logIndex = value.logIndex;
  if (blockNumber < deploymentBlock || blockNumber > latestBlock) {
    throw new Error("Deposit-event reader returned a log outside the requested block snapshot.");
  }
  if (typeof value.blockHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value.blockHash)) {
    throw new Error("Deposit-event reader returned a malformed block hash.");
  }
  if (!isRecord(value.args)) throw new Error("Deposit-event reader returned malformed event arguments.");
  const commitment = value.args.commitment;
  const amount = value.args.amount;
  const encryptedPayload = value.args.encryptedPayload;
  if (typeof commitment !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(commitment)) {
    throw new Error("CommitmentInserted log has a malformed commitment.");
  }
  if (typeof amount !== "bigint") throw new Error("CommitmentInserted log has a malformed amount.");
  if (typeof encryptedPayload !== "string" || !/^0x[0-9a-fA-F]*$/.test(encryptedPayload)) {
    throw new Error("CommitmentInserted log has a malformed encrypted payload.");
  }
  return {
    id: `${value.blockHash.toLowerCase()}:${logIndex.toString()}`,
    event: {
      commitment: commitment as HexString,
      amountWei: amount.toString(),
      encryptedPayload: encryptedPayload as HexString
    }
  };
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === "object" && !Array.isArray(value);
}
```

The starter reads `CommitmentInserted` from the runtime pool. The three adapters supplied by your app read the remaining chain state:

- `resolveMembership` returns the selected note's accepted Merkle root and path.
- `isNullifierSpent` checks whether that note has already exited.
- `runtimeFeeReadClient` reads the pool's fee state before and after proving.

Deposit and spent-state reads return their source identity with the value. The SDK stops on a chain, pool, or runtime mismatch.

Use `@nullark/sdk/node` for local proving files. Browser integrations keep Node-only imports in trusted tooling and load the main `@nullark/sdk` entry in the client bundle.

## Compatibility

These are the environments exercised by the package-pinned starter.

| Surface | Tested setup | Result |
| --- | --- | --- |
| Node | `22.19.0` | Strict tests and the real proof smoke passed |
| TypeScript | `5.9.3`, strict mode | Passed with package declarations checked |
| Browser build | Vite `8.0.10` | Production bundle passed; package loading was the browser scope |
| Wallet | Unsigned boundary | Connect and test the wallet inside the host app before release |
| Recovery serialization | UTF-8 JSON roundtrip | Save, parse, and restore passed |
| Local proving | Pinned WASM and zkey files | One deposit plus one withdrawal completed in about `1.8 s` on the test machine |

Proving time changes with hardware and browser conditions. Show progress while the proof runs.

> **Note:**
> The starter's deterministic Merkle and proof callbacks are test fixtures. Runtime code uses verified proving files and live adapters.
