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.
| Package | Source | Import paths |
|---|---|---|
@interfold/sdk | packages/interfold-sdk/src | @interfold/sdk, @interfold/sdk/crypto, @interfold/sdk/contracts, @interfold/sdk/events |
@interfold/react | packages/interfold-react/src | @interfold/react |
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:
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 field | Type | Required | Content |
|---|---|---|---|
publicClient | PublicClient | Yes | A viem public client for reads and events. |
walletClient | WalletClient | No | A viem wallet client with an account. Write calls need it. |
contracts | ContractAddresses | Yes | interfold, ciphernodeRegistry, and feeToken addresses. |
chain | Chain | No | The viem chain. |
thresholdBfvParamsPresetName | ThresholdBfvParamsPresetName | No | The 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.
| Option | Required | Content |
|---|---|---|
rpcUrl | Yes | An http://, https://, ws://, or wss:// URL. |
contracts | Yes | interfold, ciphernodeRegistry, and feeToken addresses. |
chain | Yes | The viem chain. |
privateKey | No | A private key for a local signing account. Omit it for a read-only client. |
thresholdBfvParamsPresetName | No | The 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 name | paramSet | Ring degree | CRT moduli (L) | Use |
|---|---|---|---|---|
INSECURE_THRESHOLD_64 | 0 | 64 | 3 | Local development and tests |
SECURE_THRESHOLD_8192 | 2 | 8192 | 3 | Production 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:
| Field | Type | Default |
|---|---|---|
committeeSize | number (CommitteeSize) | Required |
inputWindow | readonly [bigint, bigint] | Required |
e3Program | `0x${string}` | Required |
paramSet | number | Required |
computeProviderParams | `0x${string}` | Required |
customParams | `0x${string}` | '0x' |
maxFee | bigint | A fresh quote |
gasLimit | bigint | Estimated by viem |
requestE3 fills the fields that the contract needs and the SDK knows:
expectedFeeTokenis thefeeTokenaddress from the SDK configuration.expectedCryptoConfigIdis the configuration ID forparamSet0 or 2. Other values fail.committeeSizemust 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 anAssembledCommitteePublicKey(e3Id,nodes,pkCommitment,publicKey) when a candidate is complete and its Keccak-256 hash matches. Otherwise it returnsundefined.- 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 mostMAX_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 method | Standalone function | Returns |
|---|---|---|
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) | None | true 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) |
| None | generateProof(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_MISSINGorSDK_CIRCUIT_STAMP_INVALID: the stamp file is missing or broken.SDK_CIRCUIT_COMMITTEE_MISMATCH: the circuits are not for theminimumcommittee.SDK_CIRCUIT_PRESET_MISMATCH: the circuits are not forinsecure-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 path | Exports |
|---|---|
@interfold/sdk/crypto | The standalone encryption functions, generateProof, ThresholdBfvParamsPresetNames, and the types BfvParams, ThresholdBfvParamsPresetName, VerifiableEncryptionResult, EncryptedValueAndPublicInputs |
@interfold/sdk/contracts | ContractClient, E3Stage, FailureReason, and the types ContractClientConfig, ContractAddresses, E3, E3RequestParams |
@interfold/sdk/events | EventListener, 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.
| Method | Effect |
|---|---|
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
| Key | Event | Emitted by the current contracts |
|---|---|---|
E3_REQUESTED | E3Requested | Yes |
CIPHERTEXT_OUTPUT_REFERENCE_PUBLISHED | CiphertextOutputReferencePublished | Yes |
CIPHERTEXT_OUTPUT_PUBLISHED | CiphertextOutputPublished | No. Use the reference event. |
PLAINTEXT_OUTPUT_PUBLISHED | PlaintextOutputPublished | Yes |
E3_PROGRAM_REGISTERED | E3ProgramRegistered | Yes |
ENCRYPTION_SCHEME_ENABLED | EncryptionSchemeEnabled | Yes |
CIPHERNODE_REGISTRY_SET | CiphernodeRegistrySet | Yes |
MAX_DURATION_SET | MaxDurationSet | Yes |
PARAM_SET_REGISTERED | ParamSetRegistered | Yes |
OWNERSHIP_TRANSFERRED | OwnershipTransferred | Yes |
INITIALIZED | Initialized | Yes |
RegistryEventType
| Key | Event | Emitted by the current contracts |
|---|---|---|
COMMITTEE_RANDOMNESS_REQUESTED | CommitteeRandomnessRequested | Yes |
RANDOMNESS_CIRCUIT_BREAKER_TRIPPED | RandomnessCircuitBreakerTripped | Yes |
COMMITTEE_FINALIZED | SortitionCommitteeFinalized | Yes |
COMMITTEE_PUBLIC_KEY_CHUNK_PUBLISHED | CommitteePublicKeyChunkPublished | Yes |
INTERFOLD_SET | InterfoldSet | Yes |
OWNERSHIP_TRANSFERRED | OwnershipTransferred | Yes |
INITIALIZED | Initialized | Yes |
COMMITTEE_REQUESTED | CommitteeRequested | No. It is kept for old logs. |
COMMITTEE_PUBLISHED | CommitteePublished | No. 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
| Enum | Values |
|---|---|
E3Stage | None (0), Requested (1), CommitteeFinalized (2), KeyPublished (3), CiphertextReady (4), Complete (5), Failed (6) |
FailureReason | None (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) |
CommitteeSize | Minimum (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.
| Method | Contract 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.
| Export | Content |
|---|---|
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, generateEventId | Small 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.
| Codes | Source |
|---|---|
MISSING_PUBLIC_CLIENT, INVALID_ADDRESS, INVALID_THRESHOLD_BFV_PARAMS_PRESET_NAME | The constructor |
NO_WALLET, NO_ACCOUNT | A write method without a wallet or an account |
APPROVE_FEE_TOKEN_FAILED, REQUEST_E3_FAILED, CANCEL_E3_FAILED, PUBLISH_CIPHERTEXT_OUTPUT_FAILED | The write methods |
GET_E3_FAILED, GET_E3_PUBLIC_KEY_FAILED, GET_E3_QUOTE_FAILED, GET_E3_STAGE_FAILED, GET_FAILURE_REASON_FAILED | The read methods |
GAS_ESTIMATION_FAILED, TRANSACTION_WAIT_FAILED | Gas estimation and receipts |
WATCH_EVENT_FAILED, WATCH_LOGS_FAILED, HISTORICAL_EVENTS_FAILED, POLLING_START_FAILED, INVALID_EVENT_CONFIG | The event listener |
INVALID_COMMITTEE_SIZE, UNSUPPORTED_CRYPTO_CONFIG, MISSING_ERROR1_VARIANCE | Parameter checks |
SDK_CIRCUIT_STAMP_MISSING, SDK_CIRCUIT_STAMP_INVALID, SDK_CIRCUIT_COMMITTEE_MISMATCH, SDK_CIRCUIT_PRESET_MISMATCH | Proof 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 field | Content |
|---|---|
contracts | interfold, ciphernodeRegistry, and feeToken addresses. |
autoConnect | When true, the hook creates the SDK as soon as a public client exists. |
thresholdBfvParamsPresetName | The 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.