This skill should be used when the user asks about "voltaire-effect contract", "Contract factory", "readContract", "writeContract", "simulateContract", "type-safe ABI", "ERC-20 Effect", "contract read write", "contract events", "voltaire-effect multicall", or needs to understand how voltaire-effect interacts with smart contracts.
The Contract() factory creates type-safe contract instances that infer argument and return types directly from your ABI definition. Read operations require ProviderService; write operations additionally require SignerService.
import { Contract, Provider, HttpTransport } from 'voltaire-effect'
const erc20Abi = [
{
type: 'function', name: 'balanceOf',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ type: 'uint256' }],
stateMutability: 'view'
},
{
type: 'function', name: 'transfer',
inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }],
outputs: [{ type: 'bool' }],
stateMutability: 'nonpayable'
}
] as const // ← 'as const' required for type inference
const program = Effect.gen(function* () {
const usdc = yield* Contract('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', erc20Abi)
return usdc
})
Query view/pure functions via eth_call (no wallet needed):
const getBalance = Effect.gen(function* () {
const usdc = yield* Contract(usdcAddress, erc20Abi)
const balance = yield* usdc.read.balanceOf(userAddress)
return balance // bigint
})
// Provide only ProviderService
const ProviderLayer = Provider.pipe(
Layer.provide(HttpTransport('https://eth.llamarpc.com'))
)
const balance = await Effect.runPromise(getBalance.pipe(Effect.provide(ProviderLayer)))
State-changing functions require SignerService:
import { Secp256k1Live } from 'voltaire-effect/crypto/Secp256k1'
import { KeccakLive } from 'voltaire-effect/crypto/Keccak256'
const transfer = Effect.gen(function* () {
const usdc = yield* Contract(usdcAddress, erc20Abi)
const txHash = yield* usdc.write.transfer(recipientAddress, 1000000n)
return txHash
})
// Full layer stack with crypto + signer
const WriteLayer = Layer.mergeAll(
ProviderLayer,
Secp256k1Live,
KeccakLive,
SignerLayer
)
await Effect.runPromise(transfer.pipe(Effect.provide(WriteLayer)))
Validate a write operation without actually sending:
const program = Effect.gen(function* () {
const usdc = yield* Contract(usdcAddress, erc20Abi)
const { result, request } = yield* usdc.simulate.transfer(recipient, amount)
// result: true (simulated return value)
// request: prepared transaction request
return result
})
Query historical contract events:
const program = Effect.gen(function* () {
const usdc = yield* Contract(usdcAddress, erc20Abi)
const transfers = yield* usdc.getEvents({
eventName: 'Transfer',
fromBlock: 18000000n,
toBlock: 'latest'
})
return transfers
})
Batch multiple contract reads into a single network call via Multicall3:
import { multicall } from 'voltaire-effect'
const program = Effect.gen(function* () {
const results = yield* multicall({
contracts: [
{ address: usdc, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: dai, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
{ address: weth, abi: erc20Abi, functionName: 'balanceOf', args: [user] },
],
allowFailure: true // Returns Either[] for partial failures
})
return results
})
With allowFailure: true, results are Either<Result, Error>[] - some calls can fail without aborting the batch.
The ABI definition drives compile-time type checking:
const usdc = yield* Contract(address, erc20Abi)
// Correct - matches ABI
usdc.read.balanceOf('0x...') // ✓ address argument
// Wrong - TypeScript compile error
usdc.read.balanceOf(42) // ✗ number is not address
usdc.read.nonExistent() // ✗ function not in ABI
import { withTimeout, withRetrySchedule } from 'voltaire-effect'
// Timeout on a contract read
const balance = yield* usdc.read.balanceOf(addr).pipe(
withTimeout("5 seconds")
)
// Retry a write
const txHash = yield* usdc.write.transfer(to, amount).pipe(
withRetrySchedule(Schedule.exponential("500 millis").pipe(
Schedule.jittered,
Schedule.compose(Schedule.recurs(3))
))
)
program.pipe(
Effect.catchTag('ContractCallError', (e) =>
// Read/simulate failure (revert, invalid args, etc.)
Effect.fail(new AppError(`Contract read failed: ${e.message}`))
),
Effect.catchTag('ContractWriteError', (e) =>
// Write failure (insufficient gas, nonce conflict, etc.)
Effect.fail(new AppError(`Contract write failed: ${e.message}`))
)
)
const uniV2Router = yield* Contract(routerAddress, uniswapV2RouterAbi)
// 1. Get price quote
const amounts = yield* uniV2Router.read.getAmountsOut(
amountIn,
[tokenA, tokenB]
)
// 2. Calculate minimum output with slippage
const minOut = amounts[1] * 995n / 1000n // 0.5% slippage
// 3. Execute swap
const txHash = yield* uniV2Router.write.swapExactTokensForTokens(
amountIn,
minOut,
[tokenA, tokenB],
recipient,
BigInt(Math.floor(Date.now() / 1000) + 1200) // 20 min deadline
)
| Service | Purpose |
|---|---|
ContractRegistryService | Manage multiple contract instances |
ExplorerContracts | Fetch ABIs dynamically from block explorers |