Enclave is now The Interfold.
Build
SDK

SDK

This page is the reference for the TypeScript SDK (@interfold/sdk) and the React hook package (@interfold/react). It is for developers who write an E3 client or a coordination server. It covers installation, the client class, requests, the committee public key, encryption, events, enums, utilities, and errors. The samples come from the default template (templates/default) and the SDK README. Every export on this page exists in version 0.18.0.

Packages

Both packages are in this repository.

PackageSourceImport paths
@interfold/sdkpackages/interfold-sdk/src@interfold/sdk, @interfold/sdk/crypto, @interfold/sdk/contracts, @interfold/sdk/events
@interfold/reactpackages/interfold-react/src@interfold/react
Your applicationBrowser or Node.js
React clientCoordination server
@interfold/reactOptional
useInterfoldSDK
@interfold/sdkCore
InterfoldSDKContractClientEventListenerEncryption functions
DependenciesInstalled with the SDK
@interfold/wasm@interfold/contractsviem@aztec-foundation/bb.js@noir-lang/noir_js

Install

Add the SDK. Add the React package only if you use React.

pnpm add @interfold/sdk
pnpm add @interfold/react

@interfold/react has two peer dependencies: react (^18.2.0) and wagmi (^2.14.16).

The encryption functions use WebAssembly. In a Vite project, add vite-plugin-wasm and vite-plugin-top-level-await, and exclude @interfold/wasm from dependency optimization. This configuration comes from the SDK README:

vite.config.ts
import { defineConfig } from 'vite'
import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await'
 
export default defineConfig({
  optimizeDeps: {
    exclude: ['@interfold/wasm'],
  },
  plugins: [wasm(), topLevelAwait()],
})

The template client uses the same two plugins in client/vite.config.ts.

Create a client

With viem clients

new InterfoldSDK(config) creates the client. No separate initialize call is necessary.

SDKConfig fieldTypeRequiredContent
publicClientPublicClientYesA viem public client for reads and events.
walletClientWalletClientNoA viem wallet client with an account. Write calls need it.
contractsContractAddressesYesinterfold, ciphernodeRegistry, and feeToken addresses.
chainChainNoThe viem chain.
thresholdBfvParamsPresetNameThresholdBfvParamsPresetNameNoThe BFV preset. The default is INSECURE_THRESHOLD_64.

The constructor throws an SDKError with the code MISSING_PUBLIC_CLIENT, INVALID_ADDRESS, or INVALID_THRESHOLD_BFV_PARAMS_PRESET_NAME when the configuration is not valid.

import { InterfoldSDK } from '@interfold/sdk'
import { createPublicClient, createWalletClient, custom, http } from 'viem'
import { sepolia } from 'viem/chains'
 
const publicClient = createPublicClient({ chain: sepolia, transport: http(RPC_URL) })
 
// The SDK signs with walletClient.account, so create the wallet client with an account.
const [account] = await createWalletClient({
  chain: sepolia,
  transport: custom(window.ethereum!),
}).requestAddresses()
const walletClient = createWalletClient({
  account,
  chain: sepolia,
  transport: custom(window.ethereum!),
})
 
const sdk = new InterfoldSDK({
  publicClient,
  walletClient,
  contracts: {
    interfold: INTERFOLD_ADDRESS,
    ciphernodeRegistry: REGISTRY_ADDRESS,
    feeToken: FEE_TOKEN_ADDRESS,
  },
  chain: sepolia,
  thresholdBfvParamsPresetName: 'SECURE_THRESHOLD_8192',
})

With the factory

InterfoldSDK.create(options) creates the viem clients for you. It is useful on a server.

OptionRequiredContent
rpcUrlYesAn http://, https://, ws://, or wss:// URL.
contractsYesinterfold, ciphernodeRegistry, and feeToken addresses.
chainYesThe viem chain.
privateKeyNoA private key for a local signing account. Omit it for a read-only client.
thresholdBfvParamsPresetNameNoThe BFV preset.

A ws:// or wss:// URL gives a WebSocket transport with a 30-second keep-alive and five reconnect attempts at 2-second intervals. Other URLs give an HTTP transport. This sample comes from templates/default/server/index.ts:

import { InterfoldSDK } from '@interfold/sdk'
import { hardhat } from 'viem/chains'
 
const sdk = InterfoldSDK.create({
  rpcUrl: RPC_URL,
  privateKey: PRIVATE_KEY as `0x${string}`,
  contracts: {
    interfold: INTERFOLD_CONTRACT as `0x${string}`,
    ciphernodeRegistry: CIPHERNODE_REGISTRY_CONTRACT as `0x${string}`,
    feeToken: FEE_TOKEN_CONTRACT as `0x${string}`,
  },
  chain: hardhat,
  thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_64',
})

BFV presets

The preset selects the BFV parameters that the client uses to encrypt. It must match the paramSet of the E3.

Preset nameparamSetRing degreeCRT moduli (L)Use
INSECURE_THRESHOLD_640643Local development and tests
SECURE_THRESHOLD_8192281923Production and secure test runs

The insecure preset has no production security. Use it only for tests. ThresholdBfvParamsPresetNames lists both names, and DEFAULT_THRESHOLD_BFV_PARAMS_PRESET_NAME is INSECURE_THRESHOLD_64.

⚠️

The SDK does not compare the preset with the paramSet of the E3. A mismatch encrypts inputs under the wrong parameters, and the committee cannot decrypt the result.

Request an E3

The request takes three calls: quote the fee, approve the fee token, and send the request. This sample is trimmed from templates/default/client/src/pages/steps/RequestComputation.tsx. It adds maxFee.

import {
  DEFAULT_COMPUTE_PROVIDER_PARAMS,
  DEFAULT_E3_CONFIG,
  calculateInputWindow,
  encodeComputeProviderParams,
} from '@interfold/sdk'
 
const publicClient = sdk.getPublicClient()
const inputWindow = await calculateInputWindow(publicClient, 100) // 100 seconds
 
const requestParams = {
  committeeSize: DEFAULT_E3_CONFIG.committeeSize, // CommitteeSize.Minimum
  inputWindow,
  e3Program: E3_PROGRAM_ADDRESS,
  paramSet: 0, // insecure-64, for local development
  computeProviderParams: encodeComputeProviderParams(DEFAULT_COMPUTE_PROVIDER_PARAMS),
}
 
const fee = await sdk.getE3Quote(requestParams)
const approveTx = await sdk.approveFeeToken(fee)
await publicClient.waitForTransactionReceipt({ hash: approveTx })
 
const hash = await sdk.requestE3({ ...requestParams, maxFee: fee })

E3RequestParams in the SDK has these fields:

FieldTypeDefault
committeeSizenumber (CommitteeSize)Required
inputWindowreadonly [bigint, bigint]Required
e3Program`0x${string}`Required
paramSetnumberRequired
computeProviderParams`0x${string}`Required
customParams`0x${string}`'0x'
maxFeebigintA fresh quote
gasLimitbigintEstimated by viem

requestE3 fills the fields that the contract needs and the SDK knows:

  • expectedFeeToken is the feeToken address from the SDK configuration.
  • expectedCryptoConfigId is the configuration ID for paramSet 0 or 2. Other values fail.
  • committeeSize must be 0, 1, or 2.

requestE3 simulates the call first, then sends it, and returns the transaction hash. To get the E3 ID, watch InterfoldEventType.E3_REQUESTED and read event.data.e3Id. For the contract rules behind each field, see The Interfold Contract.

Get the committee public key

The registry does not store the full public key. Committee members publish the serialized key in CommitteePublicKeyChunkPublished events. Each event carries the proven public key commitment (pkCommitment). The client joins the chunks, then checks the key against the commitment.

This sample comes from templates/default/server/index.ts:

import { CommitteePublicKeyAssembler } from '@interfold/sdk'
import { RegistryEventType } from '@interfold/sdk/events'
import { hexToBytes } from 'viem'
 
const committeeKeyAssembler = new CommitteePublicKeyAssembler()
 
await sdk.onInterfoldEvent(
  RegistryEventType.COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED,
  async (event) => {
    const assembled = committeeKeyAssembler.add(event.data)
    if (!assembled) return
 
    const isBoundKey = await sdk.validatePublicKeyCommitment(
      assembled.publicKey,
      hexToBytes(assembled.pkCommitment),
    )
    if (!isBoundKey) return
 
    committeeKeyAssembler.clear(assembled.e3Id)
    // assembled.publicKey is the committee public key for assembled.e3Id.
  },
)

CommitteePublicKeyAssembler works as follows:

  • add(event.data) returns an AssembledCommitteePublicKey (e3Id, nodes, pkCommitment, publicKey) when a candidate is complete and its Keccak-256 hash matches. Otherwise it returns undefined.
  • It accepts only the first candidate from each publisher.
  • It rejects keys larger than MAX_COMMITTEE_PUBLIC_KEY_BYTES (512 KiB). Each chunk is at most MAX_COMMITTEE_PUBLIC_KEY_CHUNK_BYTES (90 KiB).
  • It tracks 128 E3s by default. clear(e3Id) removes the state for one E3.

sdk.getE3PublicKey(e3Id) returns the 32-byte public key commitment from the registry. It is not the key. The call fails with GET_E3_PUBLIC_KEY_FAILED before the committee publishes.

Encrypt inputs

The SDK has each encryption function in two forms. The instance method uses the preset from the configuration. The standalone function takes the preset name as its last argument.

Instance methodStandalone functionReturns
generatePublicKey()generatePublicKey(preset)A new BFV public key (Uint8Array). For tests only.
computePublicKeyCommitment(pk)computePublicKeyCommitment(pk, preset)The public key commitment (Uint8Array).
validatePublicKeyCommitment(pk, c)Nonetrue when the commitment of pk equals c.
computeCiphertextCommitment(ct)computeCiphertextCommitment(ct, preset)The SAFE commitment of a ciphertext (Uint8Array).
encryptNumber(value, pk)encryptNumber(value, pk, preset)The ciphertext of one bigint (Uint8Array).
encryptVector(values, pk)encryptVector(values, pk, preset)The ciphertext of a BigUint64Array (Uint8Array).
encryptNumberAndGenInputs(value, pk)encryptNumberAndGenInputs(value, pk, preset){ encryptedData, circuitInputs }
encryptVectorAndGenInputs(values, pk)encryptVectorAndGenInputs(values, pk, preset){ encryptedData, circuitInputs }
encryptNumberAndGenProof(value, pk)encryptNumberAndGenProof(value, pk, preset){ encryptedData, proof }
encryptVectorAndGenProof(values, pk)encryptVectorAndGenProof(values, pk, preset){ encryptedData, proof }
getThresholdBfvParamsSet()getThresholdBfvParamsSet(preset)BfvParams (degree, plaintextModulus, moduli, error1Variance)
NonegenerateProof(circuitInputs)A ProofData proof from @aztec-foundation/bb.js.

A key from generatePublicKey is not a committee key. The committee cannot decrypt data that is encrypted to it. For the full encrypt and submit flow, see Encrypt and submit.

Encryption proofs

The *AndGenProof functions and generateProof use circuits that ship in the SDK. The SDK compiles them with the insecure-64 preset and the minimum committee (packages/interfold-sdk/scripts/compile-circuits.sh). The proofs target recursive verification (noir-recursive-no-zk), not a Solidity verifier.

In Node.js, generateProof checks the circuit stamp file (.active-preset.json) when it finds one. It fails with one of these codes when the stamp does not match:

  • SDK_CIRCUIT_STAMP_MISSING or SDK_CIRCUIT_STAMP_INVALID: the stamp file is missing or broken.
  • SDK_CIRCUIT_COMMITTEE_MISMATCH: the circuits are not for the minimum committee.
  • SDK_CIRCUIT_PRESET_MISMATCH: the circuits are not for insecure-64.

For another preset, another committee size, or an on-chain verifier, compile your own circuits. Use encryptNumberAndGenInputs or encryptVectorAndGenInputs to get the witness for your prover. See Noir circuits. The CRISP repository (opens in a new tab) shows a complete custom pipeline.

Submodule imports

Each subpath exports part of the SDK. The main entry point exports everything, except the type CiphertextOutputReferencePublishedData, which only @interfold/sdk/events exports.

Import pathExports
@interfold/sdk/cryptoThe standalone encryption functions, generateProof, ThresholdBfvParamsPresetNames, and the types BfvParams, ThresholdBfvParamsPresetName, VerifiableEncryptionResult, EncryptedValueAndPublicInputs
@interfold/sdk/contractsContractClient, E3Stage, FailureReason, and the types ContractClientConfig, ContractAddresses, E3, E3RequestParams
@interfold/sdk/eventsEventListener, InterfoldEventType, RegistryEventType, RandomnessProviderEventType, and the event data types
import { encryptNumber, generatePublicKey } from '@interfold/sdk/crypto'
import { ContractClient } from '@interfold/sdk/contracts'
import { EventListener, InterfoldEventType } from '@interfold/sdk/events'

Events

The SDK watches events with viem watchContractEvent. It sends registry-only event names to the ciphernodeRegistry address and all other names to the interfold address. The shared names OwnershipTransferred and Initialized therefore go to the interfold address, also for a RegistryEventType key.

MethodEffect
onInterfoldEvent(type, callback)Watches an Interfold or registry event.
off(type, callback)Removes a callback. The watcher stops when no callback remains.
once(type, callback)Runs the callback for the first event only.
getHistoricalEvents(type, fromBlock?, toBlock?)Reads past logs. With fromBlock, it reads in ranges of 10,000 blocks.
onRandomnessProviderEvent(provider, type, callback, fromBlock?)Watches a randomness provider.
offRandomnessProviderEvent(provider, type, callback)Removes a randomness provider callback.
getHistoricalRandomnessProviderEvents(provider, type, from, to?)Reads past provider logs. It needs fromBlock.
startEventPolling(), stopEventPolling()Starts or stops a loop that reads the block number every 5 seconds. The loop does not deliver events.
cleanup()Stops polling, stops every watcher, and removes every callback.

Each callback gets an InterfoldEvent with type, data (the decoded event arguments), log, timestamp, blockNumber, and transactionHash. timestamp is the local time when the SDK got the log. Live watchers skip a log that they already delivered.

InterfoldEventType

KeyEventEmitted by the current contracts
E3_REQUESTEDE3RequestedYes
CIPHERTEXT_OUTPUT_REFERENCE_PUBLISHEDCiphertextOutputReferencePublishedYes
CIPHERTEXT_OUTPUT_PUBLISHEDCiphertextOutputPublishedNo. Use the reference event.
PLAINTEXT_OUTPUT_PUBLISHEDPlaintextOutputPublishedYes
E3_PROGRAM_REGISTEREDE3ProgramRegisteredYes
ENCRYPTION_SCHEME_ENABLEDEncryptionSchemeEnabledYes
CIPHERNODE_REGISTRY_SETCiphernodeRegistrySetYes
MAX_DURATION_SETMaxDurationSetYes
PARAM_SET_REGISTEREDParamSetRegisteredYes
OWNERSHIP_TRANSFERREDOwnershipTransferredYes
INITIALIZEDInitializedYes

RegistryEventType

KeyEventEmitted by the current contracts
COMMITTEE_RANDOMNESS_REQUESTEDCommitteeRandomnessRequestedYes
RANDOMNESS_CIRCUIT_BREAKER_TRIPPEDRandomnessCircuitBreakerTrippedYes
COMMITTEE_FINALIZEDSortitionCommitteeFinalizedYes
COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHEDCommitteePublicKeyChunkPublishedYes
INTERFOLD_SETInterfoldSetYes
OWNERSHIP_TRANSFERREDOwnershipTransferredYes
INITIALIZEDInitializedYes
COMMITTEE_REQUESTEDCommitteeRequestedNo. It is kept for old logs.
COMMITTEE_PUBLISHEDCommitteePublishedNo. Use the chunk event.

RandomnessProviderEventType

RANDOMNESS_FULFILLED maps to RandomnessFulfilled. The registry freezes a provider for each request. Read provider, requestId, and e3Id from CommitteeRandomnessRequested, then watch that provider:

import { RandomnessProviderEventType } from '@interfold/sdk'
 
await sdk.onRandomnessProviderEvent(
  provider,
  RandomnessProviderEventType.RANDOMNESS_FULFILLED,
  ({ data }) => {
    console.log(data.requestId, data.e3Id, data.fulfilledAt)
  },
)

A fulfillment shows only that the provider stored a response. The registry decides whether the response was on time and usable. Callers that share one provider watcher must use the same fromBlock, or omit it. A different explicit fromBlock fails with INVALID_EVENT_CONFIG.

Lifecycle enums

EnumValues
E3StageNone (0), Requested (1), CommitteeFinalized (2), KeyPublished (3), CiphertextReady (4), Complete (5), Failed (6)
FailureReasonNone (0), CommitteeFormationTimeout (1), InsufficientCommitteeMembers (2), DKGTimeout (3), DKGInvalidShares (4), NoInputsReceived (5), ComputeTimeout (6), ComputeProviderExpired (7), ComputeProviderFailed (8), RequesterCancelled (9), DecryptionTimeout (10), DecryptionInvalidShares (11), VerificationFailed (12)
CommitteeSizeMinimum (0), Micro (1), Small (2)

validateCommitteeSize(value) returns the value or throws INVALID_COMMITTEE_SIZE. Read the stage with sdk.getE3Stage(e3Id) and the reason with sdk.getFailureReason(e3Id).

Contract calls

These InterfoldSDK methods call the contracts. The write methods need a wallet client with an account.

MethodContract call
getE3Quote(params)Interfold.getE3Quote
approveFeeToken(amount)approve(interfold, amount) on the fee token
requestE3(params)Interfold.request
cancelE3(e3Id)Interfold.cancelE3
publishCiphertextOutput(e3Id, outputReference, gasLimit?)Interfold.publishCiphertextOutput with an encoded CiphertextOutputReference
getE3(e3Id), getE3Stage(e3Id), getFailureReason(e3Id)The matching Interfold read functions
getE3PublicKey(e3Id)CiphernodeRegistry.committeePublicKey
estimateGas(functionName, args, address, abi, value?)viem estimateContractGas
waitForTransaction(hash)viem waitForTransactionReceipt with one confirmation
getPublicClient()Returns the viem public client

The SDK does not call the E3 program. Call publishInput on your program with viem. See Encrypt and submit.

Utilities

The main entry point exports these utilities.

ExportContent
calculateInputWindow(publicClient, duration?, startBuffer?)[now + startBuffer, now + startBuffer + duration] from the latest block time. Defaults: 1800 s and 15 s.
getCurrentTimestamp(publicClient)The timestamp of the latest block.
encodeComputeProviderParams(params, mock?)The JSON of params as hex bytes. With mock set to true, 32 zero bytes.
encodeCustomParams(params)The JSON of an object as hex bytes.
encodeBfvParams(params)ABI-encoded BFV parameters. It throws MISSING_ERROR1_VARIANCE without error1Variance.
decodePlaintextOutput(hex)The first 8 bytes as a little-endian unsigned integer, as a number. null when the input is too short.
DEFAULT_COMPUTE_PROVIDER_PARAMS{ name: 'openvm', parallel: false, batch_size: 2 }
DEFAULT_E3_CONFIG{ committeeSize: 0, duration: 1800, payment_amount: '0' }
isValidAddress(value), isValidHash(value)Format checks for a 20-byte address and a 32-byte hash.
sleep(ms), formatBigInt, parseBigInt, formatEventName, parseEventData, generateEventIdSmall helpers.

Contract addresses

ContractAddresses has three fields: interfold, ciphernodeRegistry, and feeToken. Create one SDK instance for each chain.

The template reads the addresses from interfold.config.yaml. interfold print-env --chain localhost prints them as INTERFOLD_ADDRESS, REGISTRY_ADDRESS, E3_PROGRAM_ADDRESS, FEE_TOKEN_ADDRESS, and more. With --vite, the names start with VITE_. For live networks, see Contracts & Addresses.

Errors

The SDK throws SDKError, a subclass of Error with name set to SDKError and an optional code.

CodesSource
MISSING_PUBLIC_CLIENT, INVALID_ADDRESS, INVALID_THRESHOLD_BFV_PARAMS_PRESET_NAMEThe constructor
NO_WALLET, NO_ACCOUNTA write method without a wallet or an account
APPROVE_FEE_TOKEN_FAILED, REQUEST_E3_FAILED, CANCEL_E3_FAILED, PUBLISH_CIPHERTEXT_OUTPUT_FAILEDThe write methods
GET_E3_FAILED, GET_E3_PUBLIC_KEY_FAILED, GET_E3_QUOTE_FAILED, GET_E3_STAGE_FAILED, GET_FAILURE_REASON_FAILEDThe read methods
GAS_ESTIMATION_FAILED, TRANSACTION_WAIT_FAILEDGas estimation and receipts
WATCH_EVENT_FAILED, WATCH_LOGS_FAILED, HISTORICAL_EVENTS_FAILED, POLLING_START_FAILED, INVALID_EVENT_CONFIGThe event listener
INVALID_COMMITTEE_SIZE, UNSUPPORTED_CRYPTO_CONFIG, MISSING_ERROR1_VARIANCEParameter checks
SDK_CIRCUIT_STAMP_MISSING, SDK_CIRCUIT_STAMP_INVALID, SDK_CIRCUIT_COMMITTEE_MISMATCH, SDK_CIRCUIT_PRESET_MISMATCHProof generation in Node.js

A write method wraps the original error in its own code. The contract revert reason is in message. getE3Quote, getE3Stage, and getFailureReason return the viem read directly. A contract revert from these calls therefore reaches you as a viem error, not as an SDKError.

import { SDKError } from '@interfold/sdk'
 
try {
  await sdk.requestE3({ ...requestParams, maxFee: fee })
} catch (error) {
  if (error instanceof SDKError && error.code === 'REQUEST_E3_FAILED') {
    console.error(error.message) // Contains the revert, for example FeeExceedsMaximum
  }
}

React hook

useInterfoldSDK(config) creates an InterfoldSDK from the wagmi public client and wallet client. The component must be inside a WagmiProvider.

UseInterfoldSDKConfig fieldContent
contractsinterfold, ciphernodeRegistry, and feeToken addresses.
autoConnectWhen true, the hook creates the SDK as soon as a public client exists.
thresholdBfvParamsPresetNameThe BFV preset.

The hook returns sdk, isInitialized, error (a string), requestE3, getThresholdBfvParamsSet, onInterfoldEvent, off, InterfoldEventType, and RegistryEventType. It creates the SDK again when the wallet client changes, and it calls cleanup() on unmount. The helper functions throw SDK not initialized before the SDK exists.

The template memoizes the configuration, because the hook creates a new SDK when the contracts object changes. This sample follows client/src/context/WizardContext.tsx and client/src/utils/sdk-config.ts:

import { useMemo } from 'react'
import { useInterfoldSDK } from '@interfold/react'
 
export function useTemplateSdk() {
  const sdkConfig = useMemo(
    () => ({
      autoConnect: true,
      contracts: {
        interfold: import.meta.env.VITE_INTERFOLD_ADDRESS,
        ciphernodeRegistry: import.meta.env.VITE_REGISTRY_ADDRESS,
        feeToken: import.meta.env.VITE_FEE_TOKEN_ADDRESS,
      },
      thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_64' as const,
    }),
    [],
  )
  return useInterfoldSDK(sdkConfig)
}

@interfold/react also exports InterfoldEventType, RegistryEventType, RandomnessProviderEventType, and the common event data types from the SDK.

Application SDKs

A complex E3 program can ship its own SDK next to the Interfold SDK. CRISP uses @crisp-e3/sdk (packages/crisp-sdk (opens in a new tab) in the CRISP repository). It exports CrispSDK, vote functions such as encryptVote, prepareBallot, and decodeTally, and Merkle helpers such as generateMerkleProof.

Next steps