PreparedEnvelope spec v0.1
Canonical spec: packages/tx-protocol/spec/v0.1/prepared-transaction.md.
TL;DR
PreparedEnvelope is a discriminated union on kind:
type PreparedEnvelope = EvmTxEnvelope | EvmBatchEnvelope | SignatureEnvelope
interface BaseEnvelope<K extends string, C> {
$schema: string
version: '0.1'
kind: K
id?: string // EIP-5792 idempotency
issuedAt: string // RFC3339
expiresAt?: string
nonce?: `0x${string}`
producer?: Producer // DID / CAIP-10 / ERC-8004 / URL, PQ-ready signing
origin?: Origin // dApp URL + verify status
content: C // kind-specific
risk?: RiskAssessment // unbound; wallet/scanner injects
capabilities?: Capabilities // EIP-5792 aligned open record
meta?: Record<string, unknown>
}Kinds
Implemented in v0.1
kind | Use for | Content |
|---|---|---|
evm-tx | Single EVM transaction | calls[] of length 1 |
evm-batch | EIP-5792 atomic batch | calls[] of length >= 2, usually with capabilities.atomicRequired |
signature | EIP-712 / personal-sign / SIWE | domain, types, primaryType, message, or messageText |
Reserved (declared, not validated today)
evm-userop (ERC-4337), evm-frame (EIP-8141), evm-7702 (set-code), mandate (AP2 / Visa TAP / Mastercard VI), intent (ERC-7683 / UniswapX / Anoma), psbt (Bitcoin), svm-tx (Solana), move-tx (Aptos/Sui), cosmos-tx (Cosmos SDK).
Security model
Off-chain fields (
description,metadata,origin,risk,decoderRef,clearSigning,meta) are presentational and carry no cryptographic integrity on their own.The authoritative representation of on-chain effect is
{chain, calls[*].to, calls[*].data, calls[*].value}for EVM txs, or{scheme, domain, message}for signatures.Policy engines, allowlists, and spend limits MUST validate on raw fields. Trusting
description.shortis a blind-signing pattern.
Two integrity layers:
- Producer signature (
producer.signaturecovers envelope bytes). Tampering in transit detected. Schemes:secp256k1,ed25519,p256(implemented);ml-dsa-*,slh-dsa-*(reserved post-quantum NIST FIPS 204/205). - Wallet-side decoder re-verify. Consumer runs local decoder on
calls[*].data, compares synthesized movements to producer'smetadata.tokenMovements. Mismatch -> hard warning even with valid signature.
Attack-defense map
v0.1 closes gaps identified in 2024-2026 incident review:
| Attack | Shape defense |
|---|---|
| Blind signing (description vs data mismatch) | producer.signature + wallet decoder re-verify |
| Multicall bait-and-switch | Nested calls[] with per-call operation; calls.length === 1 only for evm-tx |
| MAX_UINT256 approve drainer | tokenMovement.kind: 'approve' + isUnlimited: true triggers validator warning |
| Permit / Permit2 replay | kind: 'signature' with full EIP-712 domain/types/message + validity.notAfter |
| Delegatecall spoof (Bybit $1.4B) | Per-call operation: 'delegatecall' is first-class typed; validator emits WARN |
| WalletConnect phishing | origin: { url, verifyStatus, attestation? } REQUIRED |
| Swap-as-drainer | tokenMovements[].from + to REQUIRED; wallet asserts to === user for inbound |
| Address poisoning ($336M) | counterparties[].labelSource + similarityWarning |
| Dormant pre-signed tx (Drift $285M) | validity.notAfter REQUIRED + nonceKind awareness |
| Bridge DVN compromise (Kelp $293M) | Reserved content.bridgeConfig in v0.3 |
Links
Install
pnpm add @txkit/tx-protocolMinimal usage
import { createEvmTx, validateEnvelope } from '@txkit/tx-protocol'
import type { EvmTxContent } from '@txkit/tx-protocol'
const content: EvmTxContent = {
chain: 'eip155:1',
calls: [ { to: '0x...', data: '0x...', value: '0x0' } ],
validity: { notAfter: Math.floor(Date.now() / 1000) + 3600 },
description: { short: 'Claim rewards', action: 'claim' },
metadata: {
protocol: 'my-protocol',
tokenMovements: [],
counterparties: [],
},
}
const envelope = createEvmTx(content, {
origin: { url: 'https://app.example.io', verifyStatus: 'VERIFIED' },
})
const result = validateEnvelope(envelope)
if (!result.ok) throw new Error(result.error)
// result.value is a type-safe PreparedEnvelopeHelpers and constants
| Export | Kind | Description |
|---|---|---|
createEvmTx(content, envelope?) | factory | Wraps an EvmTxContent (single call) into a full EvmTxEnvelope. Stamps $schema, version, kind: 'evm-tx', issuedAt: rfc3339Now() defaults |
createEvmBatch(content, envelope?) | factory | Same shape as createEvmTx (also accepts EvmTxContent) but stamps kind: 'evm-batch'. Use when calls.length >= 2 |
createSignature(content, envelope?) | factory | Wraps a SignatureContent (EIP-712 / personal_sign / SIWE) into a SignatureEnvelope |
validateEnvelope(envelope) | validator | Strict zod parse. Returns { ok: true, value, warnings? } or { ok: false, error, issues } |
serialize(envelope) | codec | Canonical JSON serialization (deterministic key order, BigInt-safe) |
deserialize(json) | codec | Inverse of serialize plus validation |
isImplementedKind(kind) | guard | True for 'evm-tx' | 'evm-batch' | 'signature' |
isReservedKind(kind) | guard | True for the 9 reserved kinds ('evm-userop', 'evm-frame', etc.) |
SPEC_VERSION | constant | '0.1' - the literal envelope version |
SPEC_SCHEMA_URL | constant | 'https://txkit.dev/schemas/v0.1/envelope.json' |
IMPLEMENTED_KINDS | constant | Tuple of three implemented kinds |
RESERVED_KINDS | constant | Tuple of nine reserved kinds |
CALLS_STATUS | constant | EIP-5792 status codes (100, 200, 400, 500, 600) |
The *Schema exports (evmTxEnvelopeSchema, evmBatchEnvelopeSchema, signatureEnvelopeSchema,
originSchema, metadataSchema, etc.) let consumers compose custom validators.
EnvelopeCommon (the second arg to all factories) accepts { id?, issuedAt?, expiresAt?, nonce?, producer?, origin?, capabilities?, meta? }. To stamp risk (optional risk-assessment slot), set it on the returned envelope after construction - the factory does not pipe it.
Examples in the repo
Three end-to-end examples live in packages/tx-protocol/examples/ (run with pnpm exec tsx packages/tx-protocol/examples/<file>.ts):
| File | Demonstrates |
|---|---|
stakewise-deposit.ts | kind: 'evm-tx' - StakeWise V3 vault deposit with tokenMovements, counterparties, validated origin |
uniswap-permit2-swap.ts | Two envelopes: a signature (Permit2 EIP-712) followed by an evm-tx (Uniswap swap) |
safe-delegatecall-warning.ts | evm-batch with operation: 'delegatecall' - shows the validator's WARN emission (Bybit $1.4B lesson) |