Skip to main content
Version: develop

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

  1. reading-balance -- reads the encrypted balance ciphertext from chain (RPC call)
  2. decrypting -- decrypts it locally using your encryption key (compute-intensive)
  3. generating-proof -- generates a ZK proof that the transfer is valid without revealing amounts (slowest step, runs locally)
  4. 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:

  1. reading-balance
  2. decrypting
  3. generating-proof
  4. building-transaction
  5. signing
  6. submitting

Transaction types

TypeProduced byContainsNext step
PreparedTransactionEVM prepare* (plain variants) or signTransaction / toSignedTransactiontokenAddress, functionName, argssendPreparedTransaction
UnsignedTransactionZKP prepare*, or preparePublicToEncryptedTransferWithAuthAbove + typedData, nonce, signatureArgNamesignTransaction or toSignedTransaction