Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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

kindUse forContent
evm-txSingle EVM transactioncalls[] of length 1
evm-batchEIP-5792 atomic batchcalls[] of length >= 2, usually with capabilities.atomicRequired
signatureEIP-712 / personal-sign / SIWEdomain, 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.short is a blind-signing pattern.

Two integrity layers:

  1. Producer signature (producer.signature covers envelope bytes). Tampering in transit detected. Schemes: secp256k1, ed25519, p256 (implemented); ml-dsa-*, slh-dsa-* (reserved post-quantum NIST FIPS 204/205).
  2. Wallet-side decoder re-verify. Consumer runs local decoder on calls[*].data, compares synthesized movements to producer's metadata.tokenMovements. Mismatch -> hard warning even with valid signature.

Attack-defense map

v0.1 closes gaps identified in 2024-2026 incident review:

AttackShape defense
Blind signing (description vs data mismatch)producer.signature + wallet decoder re-verify
Multicall bait-and-switchNested calls[] with per-call operation; calls.length === 1 only for evm-tx
MAX_UINT256 approve drainertokenMovement.kind: 'approve' + isUnlimited: true triggers validator warning
Permit / Permit2 replaykind: '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 phishingorigin: { url, verifyStatus, attestation? } REQUIRED
Swap-as-drainertokenMovements[].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-protocol

Minimal 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 PreparedEnvelope

Helpers and constants

ExportKindDescription
createEvmTx(content, envelope?)factoryWraps an EvmTxContent (single call) into a full EvmTxEnvelope. Stamps $schema, version, kind: 'evm-tx', issuedAt: rfc3339Now() defaults
createEvmBatch(content, envelope?)factorySame shape as createEvmTx (also accepts EvmTxContent) but stamps kind: 'evm-batch'. Use when calls.length >= 2
createSignature(content, envelope?)factoryWraps a SignatureContent (EIP-712 / personal_sign / SIWE) into a SignatureEnvelope
validateEnvelope(envelope)validatorStrict zod parse. Returns { ok: true, value, warnings? } or { ok: false, error, issues }
serialize(envelope)codecCanonical JSON serialization (deterministic key order, BigInt-safe)
deserialize(json)codecInverse of serialize plus validation
isImplementedKind(kind)guardTrue for 'evm-tx' | 'evm-batch' | 'signature'
isReservedKind(kind)guardTrue for the 9 reserved kinds ('evm-userop', 'evm-frame', etc.)
SPEC_VERSIONconstant'0.1' - the literal envelope version
SPEC_SCHEMA_URLconstant'https://txkit.dev/schemas/v0.1/envelope.json'
IMPLEMENTED_KINDSconstantTuple of three implemented kinds
RESERVED_KINDSconstantTuple of nine reserved kinds
CALLS_STATUSconstantEIP-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):

FileDemonstrates
stakewise-deposit.tskind: 'evm-tx' - StakeWise V3 vault deposit with tokenMovements, counterparties, validated origin
uniswap-permit2-swap.tsTwo envelopes: a signature (Permit2 EIP-712) followed by an evm-tx (Uniswap swap)
safe-delegatecall-warning.tsevm-batch with operation: 'delegatecall' - shows the validator's WARN emission (Bybit $1.4B lesson)