Accounts & Runtimes
Account interfaces
| Interface | Capabilities | Properties |
|---|---|---|
ZkpReadAccount | decrypt(ciphertext) | zkpAddress |
ZkpProvingAccount | Above + prove(request) | |
ZkpSigningAccount | signControllerAuth(typedData) | controllerAddress |
ZkpAccount | All of the above | zkpAddress, controllerAddress |
ZkpAccount extends both ZkpProvingAccount and ZkpSigningAccount.
Account factories
createReadAccount
import { createReadAccount } from '@cardinal-cryptography/core'
const account = createReadAccount({
esk: EncryptionSecretKey,
solver: Solver,
})
// Returns: ZkpReadAccount
createAccount
Creates a full account with a local controller spending key (CSK):
import { createAccount } from '@cardinal-cryptography/core'
const account = createAccount({
esk: EncryptionSecretKey,
solver: Solver,
prover: Prover,
csk: ControllerSpendingKey,
})
// Returns: ZkpAccount
createAccountWithSigner
Creates a full account with an external signer (MetaMask, hardware wallet):
import { createAccountWithSigner } from '@cardinal-cryptography/core'
const account = createAccountWithSigner({
esk: EncryptionSecretKey,
solver: Solver,
prover: Prover,
signer: ExternalSigner,
})
// Returns: ZkpAccount
ExternalSigner interface
Any object with these two methods:
interface ExternalSigner {
getAddress(): EvmAddress
signTypedData(args: {
domain: TypedDataToSign['domain']
types: TypedDataToSign['types']
primaryType: string
message: Record<string, unknown>
}): Promise<HexSignature>
}
viem's WalletClient does not satisfy this interface directly — wrap it in a small { getAddress, signTypedData } adapter. The linking helpers below need no adapter; they call walletClient.signTypedData themselves — see Wallet Linking.
Wallet linking helpers
Exported from @cardinal-cryptography/core (and @cardinal-cryptography/sdk):
| Function | Description |
|---|---|
buildZkStablesLinkingSignRequest(wallet, app) | EIP-712 payload for signTypedData |
signZkStablesLinkingMessage(walletClient, { app, account? }) | Sign linking message via viem; rejects non-EOA signatures |
createAccountFromLinkedWallet(runtime, walletClient, { app, account? }) | Sign, derive ESK, build account with the wallet as controller |
signerFromWalletClient(walletClient, address) | Adapt a WalletClient into the controller signer |
app is required (use ZKSTABLES_DEFAULT_APP for the default) and permanent — it is the EIP-712 domain name and determines the derived account, so choose once per app and never change it. See Wallet Linking for what it controls and why.
Constants: ZKSTABLES_DEFAULT_APP, ZKSTABLES_LINKING_DISCLAIMER, ZKSTABLES_LINKING_VERSION, ZKSTABLES_LINKING_TYPES.
Runtime interfaces
ReadRuntime
Solver only. For decryption without proving.
interface ReadRuntime {
readonly solver: Solver
createReadAccount(esk): ZkpReadAccount
createReadAccountFromMnemonic(mnemonic, accountIndex?): ZkpReadAccount
createReadAccountFromSeed(seed, accountIndex?): ZkpReadAccount
destroy(): void
}
ZkpRuntime
Solver + prover. Full capability.
interface ZkpRuntime extends ReadRuntime {
readonly prover: Prover
createAccount(esk, signer): ZkpAccount
createAccountFromMnemonic(mnemonic, accountIndex?): ZkpAccount
createAccountFromSeed(seed, accountIndex?): ZkpAccount
prewarmProver(): Promise<void>
destroy(): void
}
Runtime factories
Node (ergonomic — preferred)
@cardinal-cryptography/sdk factories build the solver and prover internally:
import { createReadRuntime, createRuntime } from '@cardinal-cryptography/sdk'
const readOnly = createReadRuntime() // solver only
const full = createRuntime() // solver + prover (threads auto-detected)
Both accept an optional config:
const readOnly = createReadRuntime({ solver: { tiers: [...] } })
const full = createRuntime({ solver: { tiers: [...] }, prover: { threads: 4, initTimeoutMs: 15_000 } })
prover.threads sets the bb.js worker count; prover.initTimeoutMs (default 15_000) bounds backend initialization so a failed worker / WASM load fails loudly instead of hanging forever.
Core (low-level, platform-agnostic)
@cardinal-cryptography/core exposes assembleRuntime / assembleReadRuntime for callers
who supply their own pre-built backends (useful in tests or custom environments):
import { assembleReadRuntime, assembleRuntime } from '@cardinal-cryptography/core'
const readOnly = assembleReadRuntime(solver)
const full = assembleRuntime(solver, prover)
Lifecycle
- Create a runtime (backends are built internally)
- Create accounts from the runtime (multiple accounts can share one runtime)
- Use accounts with clients
- Destroy the runtime when done (frees WASM resources)
const runtime = createRuntime()
const account = runtime.createAccountFromMnemonic('...')
// ... use account with clients ...
runtime.destroy() // Frees solver + prover WASM