Transaction Lifecycle
The SDK produces transactions in two shapes, depending on whether the contract needs an EIP-712 signature embedded in calldata.
EVM transactions without a signature
Register, plain deposit (WalletClient path only), auto-encrypt. The contract
checks msg.sender or nothing — no permit needed. Simple prepare-and-send:
prepare() → PreparedTransaction { tokenAddress, functionName, args }
↓
send() → tx hash
// One call
await client.sendPublicToEncryptedTransfer({ token, amount, zkpAddress })
// Or two steps
const prepared = client.preparePublicToEncryptedTransfer({ token, amount, zkpAddress })
const txHash = await client.sendPreparedTransaction(prepared)
EVM transactions with an EIP-712 permit
When the contract needs a permit signed by the token owner — today that's
publicToEncryptedTransferWithAuth, used on the bundler path because the
UserOp submitter (SharedAccount) isn't the tokenholder — the flow adds
a signing step:
prepare() → UnsignedTransaction { tokenAddress, functionName, args, typedData, nonce, signatureArgName }
↓
signAuthorization() → PreparedTransaction (authorization signature inserted under `signatureArgName`)
↓
send() → tx hash
// One call — the convenience wrapper auto-routes wallet vs bundler and,
// on the bundler path, runs prepare → sign → send under the hood.
await client.sendPublicToEncryptedTransfer({ token, amount, zkpAddress })
// Or step-by-step (useful for external signers / custody flows)
const unsigned = await client.preparePublicToEncryptedTransferWithAuth({
token, amount, zkpAddress, owner: userAddress, deadline,
})
const signed = await client.signAuthorization(unsigned)
const txHash = await client.sendPreparedTransaction(signed)
signAuthorization is bound to the account passed to the factory. For raw signatures (custody, Ledger, Fireblocks), use toSignedTransaction(unsigned, signature):
const unsigned = await client.preparePublicToEncryptedTransferWithAuth({ ... })
const signature = await externalSigner.signTypedData(unsigned.typedData)
const signed = client.toSignedTransaction(unsigned, signature)
await client.sendPreparedTransaction(signed)
ZKP transactions (proof + controller signature)
Encrypted transfers. Same prepare → sign → submit pattern as the permit
flow above, but prepare additionally generates a ZK proof and the
signing step (signTransaction, distinct from the permit's signAuthorization)
uses the controller key instead of the token-owner account:
prepare() → UnsignedTransaction { tokenAddress, functionName, args, typedData, nonce, signatureArgName }
↓
signTransaction() → PreparedTransaction (controller EIP-712 signature added)
↓
send() → tx hash
// One call (handles all steps internally)
await client.sendEncryptedTransfer({ token, amount, to })
// Or step-by-step
const unsigned = await client.prepareEncryptedTransfer({ token, amount, to })
const signed = await client.signTransaction(unsigned)
const txHash = await client.sendPreparedTransaction(signed)
What prepare does internally
- reading-balance -- reads the encrypted balance ciphertext from chain (RPC call)
- decrypting -- decrypts it locally using your encryption key (compute-intensive)
- generating-proof -- generates a ZK proof that the transfer is valid without revealing amounts (slowest step, runs locally)
- building-transaction -- constructs the EIP-712 typed data and transaction args
What signTransaction does
Signs the EIP-712 typed data with the controller spending key (CSK). This produces a secp256k1 signature that the contract verifies via ecrecover.
What send does
Encodes the function call with the controller signature appended, then submits:
- WalletClient --
writeContract(standard EVM transaction) - BundlerClient --
sendUserOperation(ERC-4337 UserOp)
Progress callbacks
The multi-step operations report progress:
await client.sendEncryptedTransfer({
token: 'zkUSD',
amount: 100n,
to: 'zk1...',
onProgress: ({ step, total, stage }) => {
console.log(`[${step}/${total}] ${stage}`)
},
})
Stages in order:
reading-balancedecryptinggenerating-proofbuilding-transactionsigningsubmitting
Transaction types
| Type | Produced by | Contains | Next step |
|---|---|---|---|
PreparedTransaction | EVM prepare* (plain variants) or signTransaction / toSignedTransaction | tokenAddress, functionName, args | sendPreparedTransaction |
UnsignedTransaction | ZKP prepare*, or preparePublicToEncryptedTransferWithAuth | Above + typedData, nonce, signatureArgName | signTransaction or toSignedTransaction |