Transaction Encoders & Crypto
The composable toolkit at the heart of the UPP SDK — pure encoders that produce viem-compatible tx args, crypto primitives, and key derivation. The non-React integration path.
The UPP SDK ships as a composable toolkit, not a monolithic client. This page covers the non-React integration path: building pool transactions by composing pure functions. If you're integrating from a React app, the React hooks layer wraps everything below and is usually the better starting point.
Shape of the SDK
Every pool transaction has the same shape on the JS side:
- Build inputs — assemble the operation's witness (notes, proofs, encrypted payloads) using SDK crypto + proof helpers, or pull a
BuildDataobject from a hook. - Encode — pass the inputs to an
encode*Txfunction, which returns a plain{ abi, functionName, args }object compatible with viem'swriteContract. - Submit — your app calls
writeContractAsync(or any wagmi/viem equivalent) with that object spread.
import { encodeShieldTx } from '@permissionless-technologies/upp-sdk'
const txArgs = encodeShieldTx({
token: '0x...',
amount: 1_000_000_000_000_000_000n,
ownerHash: '0x...',
blinding: '0x...',
encryptedNote: '0x...',
})
await writeContractAsync({
address: poolAddress,
...txArgs,
})The encoders never touch chain state and never sign anything. They're pure functions over typed input — completely deterministic, easy to test, and easy to wrap in your own abstractions.
Why no monolithic client
An earlier createUPPClient interface existed in the source but was never implemented. The React hooks took over the high-level lifecycle role (useUPPAccount, usePoolTransfer, etc.) and the encoders + indexer + crypto became the SDK's actual stable surface. The encoders are what every working integration depends on; everything else is a way of composing them.
Pool transaction encoders
Grouped by operation and proof rail. Each encoder is a direct counterpart to a pool contract entry point.
| Operation | SNARK (PLONK/BLS12-381) | STARK V3 (binary tree, legacy) | STARK V4 (octary, current) |
|---|---|---|---|
| Shield | encodeShieldTx | encodeShieldSTARKTx | encodeShieldSTARKV4Tx |
| Transfer | encodeTransferTx | encodeTransferSTARKTx | encodeTransferSTARKV4CommitTx + encodeFlushPendingV4InsertsTx |
| Withdraw | encodeWithdrawTx | encodeWithdrawSTARKTx | encodeWithdrawSTARKV4Tx |
| Merge | encodeMergeTx | — | — |
The V4 transfer split is documented in V4 Commit/Flush Transfers. The atomic encodeTransferSTARKV4Tx also exists for completeness but won't submit through any public RPC — see Known Limitations §4.
Swap module encoders follow the same shape:
| Operation | Encoder |
|---|---|
| Place order | encodeSwapOrderTx |
| Fill order | encodeSwapFillTx |
| Claim filled order | encodeSwapClaimTx |
| Cancel order | encodeSwapCancelTx |
Signature shape
All encoders share the same return type:
{ abi: typeof UNIVERSAL_PRIVATE_POOL_ABI, functionName: string, args: [...] }Most accept a typed input struct plus an optional feeAuth argument; passing feeAuth enables the relayer-based fee model documented on the Fee System page. If omitted, the encoder uses RELAYER_FEE_AUTH (a constant that resolves to the live UPD relayer config).
import {
encodeTransferSTARKV4CommitTx,
encodeFlushPendingV4InsertsTx,
} from '@permissionless-technologies/upp-sdk'
// Commit half: ~230K gas
const commitTx = encodeTransferSTARKV4CommitTx({
proof, verifierId, nullifier, stateRoot, aspRoot, aspId, token,
outputCommitment1, outputCommitment2,
encryptedNote1, encryptedNote2,
isRagequit: 0,
})
// Flush half: ~12M gas, called once per output commitment
const flushTx = encodeFlushPendingV4InsertsTx(1n)*FromBuildData helpers
The React hooks return rich BuildData objects that bundle the proof, the witness inputs, and the encrypted payloads. The *FromBuildData helpers unpack these into the encoder format so your app never has to:
import { encodeTransferTxFromBuildData } from '@permissionless-technologies/upp-sdk'
const buildData = await usePoolTransfer({...}).build({ amount, recipient })
await writeContractAsync({
address: poolAddress,
...encodeTransferTxFromBuildData(buildData),
})| Hook output | Helper |
|---|---|
usePoolTransfer().build() | encodeTransferTxFromBuildData |
usePoolTransfer().buildSplit() | encodeTransferTxFromSplitData |
useShield().build() | encodeShieldTxFromBuildData |
useWithdraw().build() | encodeWithdrawTxFromBuildData |
useSwap().buildPlace() | encodeSwapOrderTxFromBuildData |
useSwap().buildFill() | encodeSwapFillTxFromBuildData |
useSwap().buildClaim() | encodeSwapClaimTxFromBuildData |
useSwap().buildCancel() | encodeSwapCancelTxFromBuildData |
Non-React integrations can ignore these — pass directly to the bare encode*Tx functions instead.
Cryptographic primitives
Note encryption / decryption (SNARK rail uses ECIES + AES-GCM; STARK rail uses a 32-byte plaintext payload per the known limitations):
import { createNote, encryptNote, decryptNote } from '@permissionless-technologies/upp-sdk'Stealth address encoding for both rails:
import {
// SNARK (BabyJubJub) stealth addresses
generateStealthAddress,
encodeStealthAddress,
decodeStealthAddress,
isValidStealthAddress,
// STARK (Poseidon31) stealth addresses
generateStarkStealthAddress,
encodeStarkStealthAddress,
decodeStarkStealthAddress,
isValidStarkStealthAddress,
// Utility — distinguish SNARK vs STARK from the encoded form
detectAddressType,
} from '@permissionless-technologies/upp-sdk'Constants worth knowing:
ADDRESS_VERSION,STARK_ADDRESS_VERSION— version bytes for the two encoded address formatsSTEALTH_ADDRESS_PREFIX,STARK_STEALTH_ADDRESS_PREFIX— string prefixes (0zk1…,0zks1…)STARK_VERIFIER_ID_V3,STARK_VERIFIER_ID_V4— pool-side verifier registry IDs
Key derivation
The SDK derives both SNARK and STARK key material from a single wallet signature, with strict domain separation per rail:
import {
deriveDualKeysFromSignature,
deriveDualKeysFromSeed,
deriveStarkKeysP31FromSignature,
} from '@permissionless-technologies/upp-sdk'
const signature = await walletClient.signTypedData(EIP712_DOMAIN_AND_MESSAGE)
const { snark, stark } = await deriveDualKeysFromSignature(signature)The signature payload is a deterministic EIP-712 message that the user signs once per account setup. Same signature → same keys, every time — no server-side key storage anywhere. See Viewing Keys & Key Hierarchy for the 4-tier graduated-disclosure model the derived keys feed into.
Deployment configuration
Pre-configured deployments ship with the SDK; resolve them by chain ID:
import { getDeploymentOrThrow, getSupportedChainIds, getTokenAddress } from '@permissionless-technologies/upp-sdk'
const deployment = getDeploymentOrThrow(11155111) // Sepolia
// → { contractAddress, swapModuleAddress, fromBlock, tokens, asps, ... }
const upd = getTokenAddress(11155111, 'UPD')Custom networks (local Anvil, testnets, future mainnet) can register via registerDeployment. See the Quickstart for the current Sepolia addresses.
Composing the encoders directly
A complete non-React shield flow (for reference; the React hooks compose all of this for you):
import {
createNote, encryptNote,
generateStealthAddress,
encodeShieldTx,
deriveDualKeysFromSignature,
getDeploymentOrThrow,
} from '@permissionless-technologies/upp-sdk'
// 1. Derive keys from a signature
const { snark } = await deriveDualKeysFromSignature(signature)
// 2. Generate a fresh stealth address for the shield
const { stealthAddress, ephemeralPubKey } = generateStealthAddress(snark.metaAddress)
// 3. Build the note + commitment + encrypted payload
const note = createNote({ amount, token, origin: depositor })
const encryptedNote = await encryptNote(note, ephemeralPubKey)
// 4. Encode the tx and submit
const { contractAddress } = getDeploymentOrThrow(11155111)
const txArgs = encodeShieldTx({
token: note.token,
amount: note.amount,
ownerHash: note.ownerHash,
blinding: note.blinding,
encryptedNote,
})
await writeContractAsync({
address: contractAddress,
...txArgs,
})The same shape works for transfer and withdraw — replace createNote + encryptNote with generateProof and the corresponding encoder.
See also
- Indexer — Finding the notes you own after they're shielded
- React hooks — The framework-level composition of everything on this page
- Fee System — The
FeeAuthargument all encoders accept - V4 Octary State Tree and Commit/flush split — Why STARK V4 has the encoders it does
SDK Overview
UPP SDK module structure, subpath exports, and integration paths — a composable toolkit, not a monolithic client.
Fee System
Pluggable per-operation fees collected by an external FeeManager contract — EIP-3009 self-pay, relayer transferFrom, configurable per FeeKind. Pre-audit, all live Sepolia amounts are zero.