Skip to main content
Looking for Contributors! This Skill needs an implementation.Contributing a Skill involves:
  1. Writing a reference implementation with full functionality
  2. Adding comprehensive tests
  3. Writing documentation with usage examples
See the react-query Skill for an example of a framework integration Skill.Interested? Open an issue or PR at github.com/evmts/voltaire.
Skill — Copyable reference implementation. Use as-is or customize. See Skills Philosophy.
Effect.ts provides a powerful foundation for building type-safe, composable applications. Combined with Voltaire’s branded types and typed errors, you get full type safety across your entire Ethereum application—including error channels.

Why Effect.ts + Voltaire?

Type-Safe Error Channels

Voltaire’s errors all have a typesafe name property, making them perfect for Effect’s typed error channels:
import { Effect, Schema } from "effect";
import * as Address from "@voltaire/primitives/Address";
import { InvalidFormatError, InvalidLengthError } from "@voltaire/primitives/errors";

// Effect knows exactly what errors can occur
const parseAddress = (input: string): Effect.Effect<
  Address.Address,
  InvalidFormatError | InvalidLengthError
> =>
  Effect.try({
    try: () => Address.from(input),
    catch: (e) => e as InvalidFormatError | InvalidLengthError,
  });

Schema Validation for Branded Types

Use Effect Schema to validate and decode branded types:
import { Schema } from "@effect/schema";
import * as Address from "@voltaire/primitives/Address";
import * as Hex from "@voltaire/primitives/Hex";

// Schema for Address validation
const AddressSchema = Schema.transform(
  Schema.String,
  Schema.instanceOf(Uint8Array),
  {
    decode: (s) => Address.from(s),
    encode: (a) => Address.toHex(a),
  }
);

// Schema for Hex validation
const HexSchema = Schema.transform(
  Schema.String,
  Schema.instanceOf(Uint8Array),
  {
    decode: (s) => Hex.from(s),
    encode: (h) => Hex.toHex(h),
  }
);

Planned Implementation

Effect Wrappers for Voltaire Functions

// effectify.ts - Convert throwing functions to Effects
import { Effect } from "effect";

export const effectify = <A, E extends Error, Args extends unknown[]>(
  fn: (...args: Args) => A,
  errorType: new (...args: any[]) => E
) =>
  (...args: Args): Effect.Effect<A, E> =>
    Effect.try({
      try: () => fn(...args),
      catch: (e) => e as E,
    });

Address Module with Effect

import { Effect } from "effect";
import * as Address from "@voltaire/primitives/Address";
import {
  InvalidFormatError,
  InvalidLengthError,
  InvalidChecksumError,
} from "@voltaire/primitives/Address/errors";

export const from = effectify(
  Address.from,
  InvalidFormatError
);

export const fromHex = effectify(
  Address.fromHex,
  InvalidFormatError
);

export const assert = effectify(
  Address.assert,
  InvalidChecksumError
);

Transaction Pipeline

import { Effect, pipe } from "effect";
import * as Transaction from "@voltaire/primitives/Transaction";
import * as Secp256k1 from "@voltaire/crypto/Secp256k1";

type TransactionError =
  | InvalidFormatError
  | InvalidSignatureError
  | InvalidPrivateKeyError;

const buildAndSignTransaction = (
  params: TransactionParams,
  privateKey: Uint8Array
): Effect.Effect<SignedTransaction, TransactionError> =>
  pipe(
    // Build transaction
    Effect.try({
      try: () => Transaction.from(params),
      catch: (e) => e as InvalidFormatError,
    }),
    // Hash for signing
    Effect.map(Transaction.hash),
    // Sign
    Effect.flatMap((hash) =>
      Effect.try({
        try: () => Secp256k1.sign(hash, privateKey),
        catch: (e) => e as InvalidSignatureError | InvalidPrivateKeyError,
      })
    ),
    // Create signed transaction
    Effect.map((signature) => ({ ...params, signature }))
  );

Provider with Effect

import { Effect, Schedule, Duration } from "effect";

const fetchBalance = (
  provider: JsonRpcProvider,
  address: Address.Address
): Effect.Effect<bigint, ProviderError> =>
  pipe(
    Effect.tryPromise({
      try: () => provider.request({ method: "eth_getBalance", params: [Address.toHex(address), "latest"] }),
      catch: (e) => new ProviderError("Failed to fetch balance", { cause: e }),
    }),
    Effect.map((hex) => BigInt(hex)),
    // Automatic retry with exponential backoff
    Effect.retry(
      Schedule.exponential(Duration.millis(100)).pipe(
        Schedule.compose(Schedule.recurs(3))
      )
    )
  );

Error Recovery

import { Effect, Match } from "effect";

const handleAddressError = Effect.catchAll(
  parseAddress("invalid"),
  (error) =>
    Match.value(error).pipe(
      Match.when({ name: "InvalidFormatError" }, () =>
        Effect.succeed(Address.zero())
      ),
      Match.when({ name: "InvalidLengthError" }, () =>
        Effect.fail(new UserError("Address must be 20 bytes"))
      ),
      Match.exhaustive
    )
);

Key Benefits

FeatureBenefit
Typed ErrorsKnow exactly what can fail at compile time
ComposableChain operations with pipe and flatMap
RetriesBuilt-in retry policies for network operations
InterruptionCancel long-running operations cleanly
ConcurrencyParallel operations with resource limits
TelemetryBuilt-in tracing and metrics

Error Types Reference

Voltaire’s errors are organized hierarchically, making them easy to match:
// Base errors
PrimitiveError
├── ValidationError
│   ├── InvalidFormatError
│   ├── InvalidLengthError
│   ├── InvalidRangeError
│   └── InvalidChecksumError
├── SerializationError
│   ├── EncodingError
│   └── DecodingError
├── CryptoError
│   ├── InvalidSignatureError
│   ├── InvalidPublicKeyError
│   └── InvalidPrivateKeyError
└── TransactionError
    ├── InvalidTransactionTypeError
    └── InvalidSignerError
All errors have:
  • name: Typesafe discriminator (e.g., "InvalidFormatError")
  • code: Programmatic error code (e.g., "INVALID_FORMAT")
  • message: Human-readable description
  • context: Optional debugging metadata