Skip to main content

Quick Start

import { Effect } from 'effect'
import { TransactionSerializerService, DefaultTransactionSerializer } from 'voltaire-effect'

const program = Effect.gen(function* () {
  const serializer = yield* TransactionSerializerService
  const bytes = yield* serializer.serialize(tx)
  const decoded = yield* serializer.deserialize(bytes)
  return { bytes, decoded }
}).pipe(Effect.provide(DefaultTransactionSerializer.Live))

Serialize

Serialize a transaction to RLP-encoded bytes:
import * as S from 'effect/Schema'
import * as Transaction from 'voltaire-effect/primitives/Transaction'

const serializer = yield* TransactionSerializerService

const tx = S.decodeSync(Transaction.EIP1559Schema)({
  type: Transaction.Type.EIP1559,
  chainId: 1n,
  nonce: 0n,
  maxPriorityFeePerGas: 1000000000n,
  maxFeePerGas: 20000000000n,
  gasLimit: 21000n,
  to: '0x1234567890123456789012345678901234567890',
  value: 1000000000000000000n,
  data: new Uint8Array(),
  accessList: [],
  yParity: 0,
  r: new Uint8Array(32),
  s: new Uint8Array(32)
})

const bytes = yield* serializer.serialize(tx)
// Returns: Uint8Array of RLP-encoded transaction

Deserialize

Deserialize RLP-encoded bytes back to a transaction:
const serializer = yield* TransactionSerializerService

const tx = yield* serializer.deserialize(rlpBytes)
// Returns the decoded transaction object

Get Signing Payload

Get the hash that should be signed for a transaction:
const serializer = yield* TransactionSerializerService

const signingHash = yield* serializer.getSigningPayload(tx)
// Returns: Uint8Array (32 bytes) - Keccak-256 hash of unsigned tx

Transaction Types

The serializer supports all Ethereum transaction types:

Legacy (Type 0)

import * as S from 'effect/Schema'
import * as Transaction from 'voltaire-effect/primitives/Transaction'

const legacyTx = S.decodeSync(Transaction.LegacySchema)({
  type: Transaction.Type.Legacy,
  nonce: 0n,
  gasPrice: 20000000000n,
  gasLimit: 21000n,
  to: '0x...',
  value: 0n,
  data: new Uint8Array(),
  v: 27n,
  r: new Uint8Array(32),
  s: new Uint8Array(32)
})

EIP-2930 Access List (Type 1)

import * as S from 'effect/Schema'
import * as Transaction from 'voltaire-effect/primitives/Transaction'

const eip2930Tx = S.decodeSync(Transaction.EIP2930Schema)({
  type: Transaction.Type.EIP2930,
  chainId: 1n,
  nonce: 0n,
  gasPrice: 20000000000n,
  gasLimit: 21000n,
  to: '0x...',
  value: 0n,
  data: new Uint8Array(),
  accessList: [
    { address: '0x...', storageKeys: [] }
  ],
  yParity: 0,
  r: new Uint8Array(32),
  s: new Uint8Array(32)
})

EIP-1559 Fee Market (Type 2)

import * as S from 'effect/Schema'
import * as Transaction from 'voltaire-effect/primitives/Transaction'

const eip1559Tx = S.decodeSync(Transaction.EIP1559Schema)({
  type: Transaction.Type.EIP1559,
  chainId: 1n,
  nonce: 0n,
  maxPriorityFeePerGas: 1000000000n,
  maxFeePerGas: 20000000000n,
  gasLimit: 21000n,
  to: '0x...',
  value: 0n,
  data: new Uint8Array(),
  accessList: [],
  yParity: 0,
  r: new Uint8Array(32),
  s: new Uint8Array(32)
})

EIP-4844 Blob (Type 3)

import * as S from 'effect/Schema'
import * as Transaction from 'voltaire-effect/primitives/Transaction'

const eip4844Tx = S.decodeSync(Transaction.EIP4844Schema)({
  type: Transaction.Type.EIP4844,
  chainId: 1n,
  nonce: 0n,
  maxPriorityFeePerGas: 1000000000n,
  maxFeePerGas: 20000000000n,
  maxFeePerBlobGas: 1000000000n,
  gasLimit: 21000n,
  to: '0x...',
  value: 0n,
  data: new Uint8Array(),
  accessList: [],
  blobVersionedHashes: [new Uint8Array(32)],
  yParity: 0,
  r: new Uint8Array(32),
  s: new Uint8Array(32)
})

EIP-7702 Set Code (Type 4)

import * as S from 'effect/Schema'
import * as Transaction from 'voltaire-effect/primitives/Transaction'

const eip7702Tx = S.decodeSync(Transaction.EIP7702Schema)({
  type: Transaction.Type.EIP7702,
  chainId: 1n,
  nonce: 0n,
  maxPriorityFeePerGas: 1000000000n,
  maxFeePerGas: 20000000000n,
  gasLimit: 21000n,
  to: '0x...',
  value: 0n,
  data: new Uint8Array(),
  accessList: [],
  authorizationList: [
    { chainId: 1n, address: '0x...', nonce: 0n, yParity: 0, r: new Uint8Array(32), s: new Uint8Array(32) }
  ],
  yParity: 0,
  r: new Uint8Array(32),
  s: new Uint8Array(32)
})

Error Handling

import { SerializeError, DeserializeError } from 'voltaire-effect'

const program = Effect.gen(function* () {
  const serializer = yield* TransactionSerializerService
  return yield* serializer.serialize(tx)
}).pipe(
  Effect.catchTag('SerializeError', (e) => {
    console.error(`Serialize failed: ${e.message}`)
    return Effect.fail(e)
  }),
  Effect.catchTag('DeserializeError', (e) => {
    console.error(`Deserialize failed: ${e.message}`)
    return Effect.fail(e)
  })
)

Service Interface

type TransactionSerializerShape = {
  readonly serialize: (tx: unknown) => Effect.Effect<Uint8Array, SerializeError>
  readonly deserialize: (bytes: Uint8Array) => Effect.Effect<unknown, DeserializeError>
  readonly getSigningPayload: (tx: unknown) => Effect.Effect<Uint8Array, SerializeError>
}

Error Types

class SerializeError extends Data.TaggedError("SerializeError")<{
  readonly transaction: unknown
  readonly message: string
  readonly cause?: unknown
}> {}

class DeserializeError extends Data.TaggedError("DeserializeError")<{
  readonly bytes: Uint8Array
  readonly message: string
  readonly cause?: unknown
}> {}