Skip to main content
Version: 0.3.0

Gas Sponsorship

All SDK client factories use gas sponsorship by default -- users don't need ETH to interact with encrypted balances. This is powered by ERC-4337 account abstraction with a bundler and paymaster.

How it works

When you create a client with createEvmClient or createFullClient, you provide bundler and paymaster configuration. The SDK handles the rest:

  • Bundler -- submits transactions as UserOperations
  • Paymaster -- pays gas on behalf of your users
  • SharedAccount -- a contract account that submits every transaction. msg.sender is always the SharedAccount, never your wallet -- so on-chain there's no link between your ZKP account and any gas-paying EOA. Security comes from the ZK proof and controller signature in calldata.

Configuration

Both createEvmClient and createFullClient take the same bundler config:

import { createFullClient, createRuntime } from '@cardinal-cryptography/sdk'
import { createPublicClient, http } from 'viem'
import { baseSepolia } from 'viem/chains'

const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
})

const runtime = await createRuntime()
const account = runtime.createAccountFromMnemonic('your mnemonic here')

// signPartnerContext authenticates your app with the paymaster.
// The paymaster operator provides this function.
const signPartnerContext = async (sender, nonce, callData) => {
/* provided by paymaster operator */
}

const client = await createFullClient({
client: publicClient,
sharedAccountAddress: '0xSharedAccountContract...',
bundlerUrl: 'https://bundler.example.com',
paymaster: {
paymasterUrl: 'https://paymaster.example.com',
signPartnerContext,
},
zkpAccount: account,
})

// Use the client normally -- gas sponsorship is transparent
await client.sendEncryptedTransfer({
token: 'zkUSD',
amount: 100n,
to: 'zk1recipient...',
})

EVM client (public account operations)

Same configuration, without zkpAccount. Pass the user's EOA as account — the bundler submits the UserOp through the SharedAccount (so msg.sender is the SharedAccount, not the user), and the contract's publicToEncryptedTransferWithAuth variant needs an EIP-712 permit signed by the token holder. The SDK builds and signs that permit using this account automatically:

import { privateKeyToAccount } from 'viem/accounts'

const userAccount = privateKeyToAccount('0x...') // user's EOA

const evmClient = await createEvmClient({
client: publicClient,
account: userAccount,
sharedAccountAddress: '0xSharedAccountContract...',
bundlerUrl: 'https://bundler.example.com',
paymaster: {
paymasterUrl: 'https://paymaster.example.com',
signPartnerContext,
},
})

await evmClient.sendPublicToEncryptedTransfer({
token: 'zkUSD',
amount: 100n,
zkpAddress: 'zk1...',
})

Decrypt client (no bundler needed)

createDecryptClient is read-only -- no transactions, no bundler, no gas:

import { createDecryptClient, createReadRuntime } from '@cardinal-cryptography/sdk'

const runtime = await createReadRuntime()
const account = runtime.createReadAccountFromMnemonic('your mnemonic here')

const client = createDecryptClient({
chain: baseSepolia,
transport: http(),
zkpAccount: account,
})

const balance = await client.getDecryptedBalance({ token: 'zkUSD' })

Configuration reference

See Client Factories — Bundler config parameters.