viem-goviem-go

Custom Chains

Define custom EVM-compatible chains with DefineChain and use ExtractChain and AssertCurrentChain

You can support any EVM-compatible chain by defining a chain.Chain and passing it to DefineChain. viem-go does not include formatters or fee config; the Chain struct holds ID, name, native currency, RPC URLs, block explorers, optional block time, contracts, and a few extra fields.

Import

import (
"github.com/ethereum/go-ethereum/common"
"github.com/ChefBingbong/viem-go/chain"
)

DefineChain

DefineChain accepts a chain.Chain and returns an independent value copy. Use it to build chain configs for private networks, L2s, or any EVM chain. Store the result and pass a pointer to clients (&myChain).

import (
"github.com/ethereum/go-ethereum/common"
"github.com/ChefBingbong/viem-go/chain"
)
func int64Ptr(n int64) *int64 { return &n }
func uint64Ptr(n uint64) *uint64 { return &n }
myChain := chain.DefineChain(chain.Chain{
ID: 42_069,
Name: "My Custom Chain",
NativeCurrency: chain.ChainNativeCurrency{
Name: "Ether",
Symbol: "ETH",
Decimals: 18,
},
BlockTime: int64Ptr(12),
RpcUrls: map[string]chain.ChainRpcUrls{
"default": {
HTTP: []string{"https://my-rpc.example.com"},
WebSocket: []string{"wss://my-rpc.example.com"},
},
},
BlockExplorers: map[string]chain.ChainBlockExplorer{
"default": {
Name: "Explorer",
URL: "https://explorer.example.com",
ApiURL: "https://api.explorer.example.com",
},
},
Contracts: &chain.ChainContracts{
Multicall3: &chain.ChainContract{
Address: common.HexToAddress("0xca11bde05977b3631167028862be2a173976ca11"),
BlockCreated: uint64Ptr(1),
},
},
})
// Use with client
publicClient, err := client.CreatePublicClient(client.PublicClientConfig{
Chain: &myChain,
Transport: transport.HTTP(myChain.DefaultRpcUrl()),
})

Chain type (Go)

The Chain struct mirrors the usual chain metadata. Use the key "default" for the primary RPC and block explorer.

FieldTypeDescription
IDint64Chain ID.
NamestringHuman-readable name.
NativeCurrencyChainNativeCurrencyName, symbol, decimals.
RpcUrlsmap[string]ChainRpcUrlsRPC endpoints; use "default" for primary.
BlockExplorersmap[string]ChainBlockExplorerExplorers; use "default" for primary.
BlockTime*int64Block time in ms; used for default polling.
Contracts*ChainContractsOptional Multicall3, ENS, etc.
EnsTlds[]stringOptional ENS TLDs.
SourceID*int64Optional L1 chain ID (e.g. for L2s).
TestnetboolMarks testnet.
ExperimentalPreconfirmationTime*int64Optional preconfirmation time.

ChainNativeCurrency

type ChainNativeCurrency struct {
    Name     string
    Symbol   string
    Decimals uint8
}

ChainRpcUrls

type ChainRpcUrls struct {
    HTTP      []string
    WebSocket []string // optional
}

Use a map key (e.g. "default") when setting RpcUrls on Chain.

ChainBlockExplorer

type ChainBlockExplorer struct {
    Name   string
    URL    string
    ApiURL string // optional
}

ChainContracts and ChainContract

type ChainContracts struct {
    Multicall3           *ChainContract
    EnsRegistry          *ChainContract
    EnsUniversalResolver *ChainContract
}

type ChainContract struct {
    Address      common.Address
    BlockCreated *uint64
}

Addresses use common.Address (e.g. common.HexToAddress("0x...")).

Helper methods

On a *chain.Chain (or value when the method set is on *Chain):

  • DefaultRpcUrl() string — first HTTP URL in RpcUrls["default"], or "".
  • DefaultBlockExplorer() ChainBlockExplorerBlockExplorers["default"], or zero value.
c := &myChain
rpcURL := c.DefaultRpcUrl()
explorer := c.DefaultBlockExplorer()
fmt.Println(explorer.URL)

ExtractChain

ExtractChain(chains []*Chain, chainID int64) returns the first chain in the slice whose ID matches chainID, or an error. Useful when you have a list of chains and a current chain ID (e.g. from the wallet).

import "github.com/ChefBingbong/viem-go/chain"
chains := []*chain.Chain{
&definitions.Mainnet,
&definitions.Polygon,
&Sepolia,
}
c, err := chain.ExtractChain(chains, 1)
if err != nil {
// ErrChainNotFound or ErrInvalidChainID
return err
}
// c is *chain.Chain for mainnet (ID 1)
  • Returns: (*Chain, error).
  • Errors: ErrChainNotFound (empty slice or no matching ID), ErrInvalidChainID (chainID < 0).

AssertCurrentChain

AssertCurrentChain(chain *Chain, currentChainID int64) checks that currentChainID equals chain.ID. Use it before sending a transaction to ensure the wallet is on the expected chain.

import "github.com/ChefBingbong/viem-go/chain"
err := chain.AssertCurrentChain(&definitions.Mainnet, currentChainID)
if err != nil {
if errors.Is(err, chain.ErrChainNotFound) {
// chain was nil
}
var mismatch *chain.ChainMismatchError
if errors.As(err, &mismatch) {
// mismatch.CurrentChainID != mismatch.Chain.ID
}
return err
}
  • Returns: nil if chain != nil and currentChainID == chain.ID.
  • Errors: ErrChainNotFound if chain == nil; *ChainMismatchError if IDs do not match. ChainMismatchError contains Chain and CurrentChainID and implements error with a descriptive message.

Error handling

ErrorWhen
ErrChainNotFoundNo chain provided (e.g. AssertCurrentChain(nil, ...)) or ExtractChain found no match.
ErrInvalidChainIDExtractChain(..., chainID) with chainID < 0.
ErrInvalidChainsLenUsed when the chain array is empty where a non-empty list is required.
ChainMismatchErrorAssertCurrentChain: current chain ID does not match the target chain.

Using a custom chain with a client

myChain := chain.DefineChain(chain.Chain{ /* ... */ })
publicClient, err := client.CreatePublicClient(client.PublicClientConfig{
Chain: &myChain,
Transport: transport.HTTP(myChain.DefaultRpcUrl()),
})
if err != nil {
log.Fatal(err)
}
defer publicClient.Close()

Go vs viem

  • viem-go has no separate formatters or fees config; the Chain struct holds only the fields above.
  • DefineChain returns a value; clients take *chain.Chain. Use &myChain.
  • Contract addresses are common.Address, not strings.
  • RPC and explorer entries are maps keyed by name (e.g. "default"), not nested structs with a default field.