Skip to main content
voltaire-effect organizes functionality into three layers that work together:
LayerPurposeUse When
SchemaValidation, type coercionParsing user input, API responses
EffectComposable operationsChaining transformations, error handling
ServicesStateful resourcesProvider calls, wallet signing

Schema

Effect Schema wrappers. Decodes to actual Voltaire branded types.
import * as Address from 'voltaire-effect/primitives/Address'
import * as Hex from 'voltaire-effect/primitives/Hex'
import * as S from 'effect/Schema'

// Decode from hex string
const addr = S.decodeSync(Address.Hex)('0x742d35Cc6634C0532925a3b844Bc9e7595f251e3')

// Decode from bytes
const fromBytes = S.decodeSync(Address.Bytes)(new Uint8Array(20))

// Works directly with Voltaire
const hex = S.decodeSync(Hex.String)('0x1234')

Effect

Effect-wrapped operations. Use S.decode for effectful parsing with typed errors.
import * as Address from 'voltaire-effect/primitives/Address'
import * as S from 'effect/Schema'
import * as Effect from 'effect/Effect'

const program = Effect.gen(function* () {
  const addr = yield* S.decode(Address.Hex)('0x...')
  return addr
})

// Error handling
Effect.catchTag("ParseError", (e) => ...)

Services

Effect Services for stateful resources.
import { getBlock, Provider, HttpTransport, withTimeout } from 'voltaire-effect'
import { Effect, Layer, Schedule } from 'effect'

// Compose layers once
const ProviderLayer = Provider.pipe(
  Layer.provide(HttpTransport({
    url: "https://eth.llamarpc.com",
    timeout: "30 seconds",
    retrySchedule: Schedule.exponential("500 millis").pipe(
      Schedule.jittered,
      Schedule.compose(Schedule.recurs(3))
    )
  }))
)

const program = Effect.gen(function* () {
  return yield* getBlock({ blockTag: "latest" }).pipe(withTimeout("5 seconds"))
})

await Effect.runPromise(program.pipe(Effect.provide(ProviderLayer)))
Available:
  • ProviderService / Provider - JSON-RPC request layer used by provider free functions (blocks, txs, logs, call, simulate, optional account ops)
  • SignerService / Signer - Signs & sends transactions
  • AccountService - Local or JSON-RPC account abstraction for signing
  • DebugService / Debug - debug_* tracing and raw data access
  • EngineApiService / EngineApi - engine_* consensus/execution RPCs
  • TransportService - HTTP, WebSocket, Browser transports
  • CryptoLive / CryptoTest - All crypto services bundled

Signer.fromProvider Pattern

Create a Signer from existing Provider + Account:
import { sendTransaction, Signer, Provider, LocalAccount, HttpTransport } from 'voltaire-effect'
import { Effect, Layer } from 'effect'

const privateKey = Hex.fromHex('0xac0974bec...')

// Compose layers first
const TransportLayer = HttpTransport("https://eth.llamarpc.com")
const SignerLayer = Signer.fromPrivateKey(privateKey, Provider).pipe(
  Layer.provide(TransportLayer)
)

// Or explicitly with LocalAccount:
const SignerLayer2 = Signer.fromProvider(Provider, LocalAccount(privateKey)).pipe(
  Layer.provide(TransportLayer)
)

// Single provide at the edge
const program = Effect.gen(function* () {
  return yield* sendTransaction({ to, value })
})

await Effect.runPromise(program.pipe(Effect.provide(SignerLayer)))

Layer Composition

Always compose layers before providing. Don’t chain multiple Effect.provide calls.
// ❌ Anti-pattern: multiple provides
program.pipe(
  Effect.provide(Signer.Live),
  Effect.provide(Provider),
  Effect.provide(HttpTransport(rpcUrl))
)

// ✅ Correct: compose layers once, then provide
const AppLayer = Layer.mergeAll(
  Signer.Live,
  Provider
).pipe(Layer.provide(HttpTransport(rpcUrl)))

program.pipe(Effect.provide(AppLayer))

Composition Patterns

PatternUse When
Layer.mergeAll(A, B, C)Independent layers (no deps between them)
A.pipe(Layer.provide(B))A depends on B
Layer.provideMerge(A, B)Add B’s services while keeping A

Example: Full Wallet Setup

import { Effect, Layer } from 'effect'
import { Signer, Provider, HttpTransport } from 'voltaire-effect'
import { MnemonicAccount } from 'voltaire-effect/native'
import { Secp256k1Live, KeccakLive } from 'voltaire-effect/crypto'

const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const TransportLayer = HttpTransport('https://eth.llamarpc.com')
const ProviderLayer = Provider.pipe(Layer.provide(TransportLayer))

const WalletLayer = Layer.mergeAll(
  Signer.Live,
  CryptoLayer,
  ProviderLayer
).pipe(
  Layer.provideMerge(MnemonicAccount(mnemonic).pipe(Layer.provide(CryptoLayer)))
)

// Single provide at the edge
await Effect.runPromise(program.pipe(Effect.provide(WalletLayer)))

See Also