Effect.ts integration for Voltaire with typed errors, services (Provider, Signer), and contract patterns. Use for composable blockchain operations with full type safety.
Effect.ts integration for Voltaire with typed errors, dependency injection, and composable operations.
npm install voltaire-effect effect @tevm/voltaire
Every operation returns Effect<A, E, R>:
A - Success value typeE - Typed error unionR - Required services/dependenciesimport { Effect } from 'effect';
import { Provider, getBalance } from 'voltaire-effect';
// Effect<Uint256, ProviderError | InvalidAddressError, Provider>
const balanceEffect = getBalance(address);
// Run with provider
const result = await Effect.runPromise(
balanceEffect.pipe(
Effect.provide(Provider.http("https://eth.llamarpc.com"))
)
);
Pre-configure contracts for reuse:
import { makeContractRegistry, Contract } from 'voltaire-effect';
import * as Address from '@tevm/voltaire/Address';
// Define contract registry
const contracts = makeContractRegistry({
usdc: Contract.fromAbi({
abi: erc20Abi,
address: Address.from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
}),
weth: Contract.fromAbi({
abi: wethAbi,
address: Address.from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
})
});
// Use in effects
const getUsdcBalance = (owner: Address) =>
contracts.usdc.read.balanceOf(owner);
const approveWeth = (spender: Address, amount: Uint256) =>
contracts.weth.write.approve(spender, amount);
For one-off contract interactions:
import { Contract } from 'voltaire-effect';
const token = Contract.fromAbi({
abi: erc20Abi,
address: tokenAddress
});
// Read call
const balance = token.read.balanceOf(owner);
// Write call (requires Signer)
const transfer = token.write.transfer(recipient, amount);
For deploying or interacting with multiple instances:
const ERC20Factory = Contract.factory({
abi: erc20Abi
});
// Create instance with address
const usdc = ERC20Factory.attach(usdcAddress);
const dai = ERC20Factory.attach(daiAddress);
JSON-RPC provider for read operations:
import { Provider } from 'voltaire-effect';
// HTTP provider
const httpProvider = Provider.http("https://eth.llamarpc.com");
// WebSocket provider
const wsProvider = Provider.websocket("wss://eth.llamarpc.com");
// With options
const provider = Provider.http("https://...", {
timeout: 30000,
retries: 3
});
Transaction signing service:
import { Signer } from 'voltaire-effect';
import * as PrivateKey from '@tevm/voltaire/PrivateKey';
// From private key
const signer = Signer.fromPrivateKey(
PrivateKey.from("0x...")
);
// From mnemonic
const signer = Signer.fromMnemonic(
"word1 word2 ... word12",
"m/44'/60'/0'/0/0"
);
Dependency injection for contracts:
import { ContractRegistryService, makeContractRegistry } from 'voltaire-effect';
import { Effect, Layer } from 'effect';
const registry = makeContractRegistry({ usdc, weth, uniswap });
// Create layer
const ContractLayer = Layer.succeed(
ContractRegistryService,
registry
);
// Use in effects
const swap = Effect.gen(function* () {
const contracts = yield* ContractRegistryService;
yield* contracts.usdc.write.approve(uniswapAddress, amount);
yield* contracts.uniswap.write.swap(params);
});
All errors are typed and can be pattern matched:
import { Effect } from 'effect';
import {
ProviderError,
ContractError,
InsufficientFundsError,
RevertError
} from 'voltaire-effect';
const safeTransfer = transfer(recipient, amount).pipe(
Effect.catchTag("InsufficientFundsError", (e) =>
Effect.fail(new UserFriendlyError(`Not enough balance: ${e.required}`))
),
Effect.catchTag("RevertError", (e) =>
Effect.fail(new UserFriendlyError(`Contract reverted: ${e.reason}`))
),
Effect.catchTag("ProviderError", (e) =>
Effect.retry({ times: 3, schedule: Schedule.exponential(1000) })
)
);
import { Effect, Schedule } from 'effect';
const robustCall = getBalance(address).pipe(
Effect.retry({
times: 5,
schedule: Schedule.exponential("100 millis")
})
);
const timedCall = getBalance(address).pipe(
Effect.timeout("10 seconds")
);
const [balance1, balance2, balance3] = await Effect.runPromise(
Effect.all([
getBalance(addr1),
getBalance(addr2),
getBalance(addr3)
], { concurrency: 3 })
);
const approveAndTransfer = Effect.gen(function* () {
yield* token.write.approve(spender, amount);
yield* token.write.transferFrom(owner, recipient, amount);
});
import { Effect, Layer } from 'effect';
import {
Provider,
Signer,
makeContractRegistry,
Contract
} from 'voltaire-effect';
import * as Address from '@tevm/voltaire/Address';
import * as Uint256 from '@tevm/voltaire/Uint256';
// Setup contracts
const contracts = makeContractRegistry({
usdc: Contract.fromAbi({
abi: erc20Abi,
address: Address.from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
})
});
// Define operation
const transferUsdc = (to: Address, amount: Uint256) =>
Effect.gen(function* () {
const balance = yield* contracts.usdc.read.balanceOf(myAddress);
if (Uint256.lt(balance, amount)) {
return yield* Effect.fail(new InsufficientFundsError({ required: amount, available: balance }));
}
const txHash = yield* contracts.usdc.write.transfer(to, amount);
return txHash;
});
// Setup layers
const MainLayer = Layer.mergeAll(
Provider.http("https://eth.llamarpc.com"),
Signer.fromPrivateKey(privateKey)
);
// Run
const result = await Effect.runPromise(
transferUsdc(recipient, amount).pipe(
Effect.provide(MainLayer)
)
);
See references/SERVICES.md for service patterns. See references/CONTRACT.md for contract patterns.