Skip to main content
AccountService is for local or wallet signing. For node account RPCs like eth_accounts, eth_coinbase, or eth_sign*, use the Provider free functions (getAccounts, getCoinbase, sign, signTransaction) with a Provider layer.

Quick Start

import { Effect, Schedule, Layer } from 'effect'
import { AccountService, LocalAccount, withTimeout } from 'voltaire-effect'
import { Secp256k1Live, KeccakLive } from 'voltaire-effect/crypto'
import { Hex } from '@tevm/voltaire'

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

// Compose layers first
const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const AccountLayer = LocalAccount(privateKey).pipe(Layer.provide(CryptoLayer))

const program = Effect.gen(function* () {
  const account = yield* AccountService

  // Per-request timeout for signing (useful for hardware wallets)
  const signature = yield* account.signMessage(Hex.fromString('Hello!')).pipe(
    withTimeout('10 seconds')
  )
  return signature
}).pipe(Effect.provide(AccountLayer))

Browser Wallet

import { JsonRpcAccount, BrowserTransport, withTimeout } from 'voltaire-effect'
import { Layer } from 'effect'

// Compose layers first
const BrowserAccountLayer = JsonRpcAccount(userAddress).pipe(
  Layer.provide(BrowserTransport)
)

const program = Effect.gen(function* () {
  const account = yield* AccountService

  // Longer timeout for user interaction
  const signature = yield* account.signMessage(messageHex).pipe(
    withTimeout('2 minutes')
  )
  return signature
}).pipe(Effect.provide(BrowserAccountLayer))

Methods

signMessage — EIP-191 personal_sign
const signature = yield* account.signMessage(Hex.fromString('Hello!'))
signTransaction — Sign unsigned tx (all fields required)
const signature = yield* account.signTransaction({
  to: recipientAddress,
  value: 1000000000000000000n,
  nonce: 5n,
  gasLimit: 21000n,
  gasPrice: 20000000000n,
  chainId: 1n
})
signTypedData — EIP-712 structured data
const signature = yield* account.signTypedData({
  domain: { name: 'App', version: '1', chainId: 1n, verifyingContract },
  primaryType: 'Mail',
  types: { Mail: [{ name: 'to', type: 'address' }, { name: 'contents', type: 'string' }] },
  message: { to: recipient, contents: 'Hello!' }
})

Nested Types

EIP-712 supports nested struct types and arrays:
const typedData = {
  domain: { name: 'MyApp', version: '1', chainId: 1n },
  types: {
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallet', type: 'address' }
    ],
    Mail: [
      { name: 'from', type: 'Person' },
      { name: 'to', type: 'Person' },
      { name: 'contents', type: 'string' }
    ]
  },
  primaryType: 'Mail',
  message: {
    from: { name: 'Alice', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' },
    to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' },
    contents: 'Hello, Bob!'
  }
}
const signature = yield* account.signTypedData(typedData)

Array Types

Arrays of primitives and structs are supported:
const typedData = {
  domain: { name: 'Batch', version: '1', chainId: 1n },
  types: {
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallet', type: 'address' }
    ],
    BatchTransfer: [
      { name: 'recipients', type: 'Person[]' },
      { name: 'amounts', type: 'uint256[]' }
    ]
  },
  primaryType: 'BatchTransfer',
  message: {
    recipients: [
      { name: 'Alice', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' },
      { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' }
    ],
    amounts: [1000000000000000000n, 2000000000000000000n]
  }
}

HD Wallet / Mnemonic

Create accounts from BIP-39 mnemonics using MnemonicAccount:
import { MnemonicAccount } from 'voltaire-effect/native'

const mnemonic = "test test test test test test test test test test test junk"

// Compose layers first
const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)
const MnemonicAccountLayer = MnemonicAccount(mnemonic).pipe(Layer.provide(CryptoLayer))

const program = Effect.gen(function* () {
  const account = yield* AccountService
  return account.address
}).pipe(Effect.provide(MnemonicAccountLayer))
See the HD Wallet Guide for derivation paths, passphrases, and manual usage.

Error Handling

import { AccountError } from 'voltaire-effect'

program.pipe(
  Effect.catchTag('AccountError', (e) => Effect.succeed({ error: e.message }))
)

Service Interface

type AccountShape = {
  readonly address: AddressType
  readonly type: 'local' | 'json-rpc' | 'hardware'
  readonly signMessage: (message: HexType) => Effect.Effect<SignatureType, AccountError>
  readonly signTransaction: (tx: UnsignedTransaction) => Effect.Effect<SignatureType, AccountError>
  readonly signTypedData: (typedData: TypedDataType) => Effect.Effect<SignatureType, AccountError>
}

See Also