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.senderis 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' })