viem-cosmos
v0.0.3
Published
viem client extensions for Cosmos EVM precompiles (bank, staking, distribution, gov). Typed actions built on the cosmos-evm-contracts ABIs.
Readme
viem-cosmos
Typed viem client extensions ("decorators") for the Cosmos EVM precompiles.
viem-cosmos is a representation layer on top of the
cosmos-evm-contracts
ABIs. It bridges the gap between EVM-native types (hex addresses, raw
uint/bytes fields) and Cosmos-native representations (bech32 addresses,
typed BondStatus/ProposalStatus/VoteOption enums, human-readable
LegacyDec/DecCoin numbers). The underlying call plumbing is viem's
readContract/writeContract; argument and return types are inferred straight
from the ABI via a generic factory, so every action is fully type-safe without
hand-written bindings.
v0.x covers the validator-economy precompiles: bank, staking,
distribution, gov (core four, combined), and slashing (standalone).
Other precompiles (ICS-20, vesting, …) are reachable raw via
precompileActions.
Install
pnpm add viem-cosmos viem
# viem is a peer dependency
# cosmos-evm-contracts (precompile ABIs) is a bundled dependencyQuick start
import { http } from "viem"
import { defineCosmosChain, stakingActions, pageRequest, bondStatusToWireString } from "viem-cosmos"
import { createPublicClient } from "viem"
const cosmosEvmLocal = defineCosmosChain({
id: 262144,
name: "evmd Local",
nativeCurrency: { name: "TEST", symbol: "TEST", decimals: 18 },
rpcUrls: { default: { http: ["http://localhost:8545"] } },
bech32Prefix: "cosmos",
})
const client = createPublicClient({
chain: cosmosEvmLocal,
transport: http(),
}).extend(stakingActions())
const { validators } = await client.validators({
args: [bondStatusToWireString("BONDED"), pageRequest({ limit: 10n })],
})
// validators[0].operatorAddress === "cosmosvaloper1…"
// validators[0].operatorAddressHex === "0x…" (retained, non-lossy)
// validators[0].status === "BONDED" (friendly union; statusRaw keeps the wire uint8)defineCosmosChain wraps viem's defineChain and adds a bech32Prefix (and
optional bech32Prefixes overrides for non-standard validator/consensus
prefixes). The formatted decorators read the prefix from client.chain so you
never thread it through individual calls.
Precompiles covered
| Precompile | Decorator | In cosmosActions() | Notes |
| --- | --- | --- | --- |
| bank | bankActions() | yes | Raw passthrough — balances are ERC20-paired integer Coin amounts; no bech32 or LegacyDec transform needed |
| staking | stakingActions() | yes | Formatted: hex operator → bech32 valoper, uint8 → BondStatus, LegacyDec → parsed |
| distribution | distributionActions() | yes | Formatted: DecCoin amounts → parsed FormattedDecCoin; write guards require bech32 addresses |
| gov | govActions() | yes | Formatted: uint32 → ProposalStatus, uint8 → VoteOption, tally LegacyDec → parsed |
| slashing | slashingActions() | no (standalone) | Formatted: signing-info hex validator address → bech32 valoper, getParams Dec fields parsed. Standalone because getParams collides with gov's |
Decorator model
Each precompile ships two decorator factories:
xActions()— formatted, chain-prefix-aware canonical surface. Transforms reads (hex → bech32, uint enums → typed string unions, LegacyDec/DecCoin → parsed non-lossily). Validates bech32 on write args that carry role-bearing addresses (funds path: hex rejected, no silent mis-targeting).xActionsRaw()— thin ABI→action passthrough, the wagmi-cli analogue. Values returned exactly as viem decoded them. Use as an escape hatch when you need the raw wire form.
The combined cosmosActions() merges bank + staking + distribution + gov
onto one client (collision-free). getEvents is stripped from the combined
surface to avoid name collisions — use a single-module decorator for event reads.
Slashing is standalone and is NOT part of cosmosActions() because its
getParams collides by name with gov's getParams. Extend slashingActions()
on a separate client or alongside the individual decorators you need (see
Composition below).
This follows the viem op-stack pattern: extend only the decorators a chain supports. Calling an action for an absent precompile errors at runtime.
Usage examples
Combined client (core four)
import { createCosmosClient } from "viem-cosmos"
// createCosmosClient wraps createClient + cosmosActions()
const client = createCosmosClient({
chain: cosmosEvmLocal, // a defineCosmosChain(...) result
transport: http(),
account, // optional; required for write actions
})
// bank
const balances = await client.balances({ args: [account] })
// staking write — validator address must be bech32 valoper
const hash = await client.delegate({
args: [account, "cosmosvaloper1…", amount],
})
// gov
const proposal = await client.getProposal({ args: [1n] })
// proposal.status === "VOTING_PERIOD" (friendly union; statusRaw keeps the wire uint32)Per-module decorators (tree-shakeable)
import { createPublicClient, createWalletClient, http } from "viem"
import { stakingActions, distributionActions } from "viem-cosmos"
const reader = createPublicClient({ chain: cosmosEvmLocal, transport: http() })
.extend(stakingActions())
const { validators, pageResponse } = await reader.validators({
args: ["BOND_STATUS_BONDED", pageRequest({ limit: 50n })],
})Slashing (standalone)
import { createPublicClient, http } from "viem"
import { slashingActions } from "viem-cosmos"
const client = createPublicClient({ chain: cosmosEvmLocal, transport: http() })
.extend(slashingActions())
const info = await client.getSigningInfo({ args: [consAddressHex] })
// info.validatorAddress === "cosmosvaloper1…" (bridged from hex)
const params = await client.getParams()
// params.slashFractionDoubleSign, params.slashFractionDowntime — parsed `Dec` fields
// unjail takes the hex operator address directly (no bech32 guard — the ABI arg is
// a plain 20-byte hex, not a role-bearing bech32 valoper)
const hash = await walletClient.extend(slashingActions()).unjail({
args: [validators[0].operatorAddressHex],
})Composing individual decorators
import { createWalletClient, http } from "viem"
import { stakingActions, slashingActions } from "viem-cosmos"
// Compose staking + slashing on the same client (viem-idiomatic chaining)
const client = createWalletClient({ chain: cosmosEvmLocal, transport: http(), account })
.extend(stakingActions())
.extend(slashingActions())Raw escape hatch
import { createPublicClient, http } from "viem"
import { stakingActionsRaw } from "viem-cosmos"
// Values returned as decoded: hex operator address, uint8 status, LegacyDec strings
const client = createPublicClient({ chain: cosmosEvmLocal, transport: http() })
.extend(stakingActionsRaw())
const result = await client.validators({ args: ["BOND_STATUS_BONDED", pageRequest()] })
// result[0][0].operatorAddress === "0x…" (hex, not bridged)Action surface
| Module | Reads | Writes |
| --- | --- | --- |
| bank | balances, supplyOf, totalSupply | — |
| staking | delegation, redelegation, redelegations, unbondingDelegation, validator, validators | delegate, undelegate, redelegate, cancelUnbondingDelegation, createValidator, editValidator |
| distribution | communityPool, delegationRewards, delegationTotalRewards, delegatorValidators, delegatorWithdrawAddress, validatorCommission, validatorDistributionInfo, validatorOutstandingRewards, validatorSlashes | claimRewards, depositValidatorRewardsPool, fundCommunityPool, setWithdrawAddress, withdrawDelegatorRewards, withdrawValidatorCommission |
| gov | getConstitution, getDeposit, getDeposits, getParams, getProposal, getProposals, getTallyResult, getVote, getVotes | cancelProposal, deposit, submitProposal, vote, voteWeighted |
| slashing | getSigningInfo, getSigningInfos, getParams | unjail |
Every action accepts the underlying viem call fields:
- reads —
argsplusblockNumber/blockTag/account/stateOverride - writes —
argsplusaccount/chain/gas/nonce/ fee fields
Representation helpers
Chain definition
import { defineCosmosChain, resolvePrefixes } from "viem-cosmos"
const chain = defineCosmosChain({
id: 262144,
name: "My Cosmos EVM Chain",
nativeCurrency: { name: "ATOM", symbol: "ATOM", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.example.com"] } },
bech32Prefix: "cosmos",
// Optional: override derived prefixes for non-standard chains
bech32Prefixes: { valoper: "customvaloper", valcons: "customvalcons" },
})
// { account: "cosmos", valoper: "cosmosvaloper", valcons: "cosmosvalcons" }
const prefixes = resolvePrefixes(chain)Address conversion
import { evmAddressToBech32, bech32ToEvmAddress } from "viem-cosmos"
const bech32 = evmAddressToBech32("0xabc…", "cosmosvaloper") // "cosmosvaloper1…"
const hex = bech32ToEvmAddress("cosmos1…", "cosmos") // "0x…" (asserts the prefix)Decimal formatting
import { formatDecCoin, formatDec, formatLegacyDec, parseLegacyDec, formatCoin } from "viem-cosmos"
// DecCoin (precompile-decoded struct with denom + amount)
const coin = formatDecCoin({ denom: "uatom", amount: 1_234_567n })
// { denom: "uatom", amount: 1_234_567n, formatted: "1.234567" }
// LegacyDec (the ABI returns an 18-decimal fixed-point value)
const parsed = parseLegacyDec(1_000_000_000_000_000_000n) // { raw: 1000000000000000000n, precision: 18 }
const str = formatLegacyDec(1_000_000_000_000_000_000n) // "1"
const dec = formatDec({ value: 1_000_000_000_000_000_000n, precision: 18 }) // "1"
// Plain Coin (integer base-unit amount, e.g. from bank.balances)
const display = formatCoin({ denom: "uatom", amount: 1_000_000n }, 6) // "1"Enums
import {
bondStatusFromWire, bondStatusToWireString, bondStatusToWireNumber,
proposalStatusFromWire, proposalStatusToWireNumber, proposalStatusToWireString,
voteOptionFromWire, voteOptionToWireNumber, voteOptionToWireString,
} from "viem-cosmos"
// Wire → friendly union (accepts the uint OR the protobuf string form)
bondStatusFromWire(1) // "UNBONDED"
proposalStatusFromWire(2) // "VOTING_PERIOD"
voteOptionFromWire(1) // "YES"
// Friendly → wire number (for uint ABI args like vote())
bondStatusToWireNumber("BONDED") // 3
// Friendly → wire string (for string-keyed ABI args like validators())
bondStatusToWireString("BONDED") // "BOND_STATUS_BONDED"Pagination
import { pageRequest, paginate, paginated, hasNextPage } from "viem-cosmos"
// Single page
const page = pageRequest({ limit: 50n })
const { validators, pageResponse } = await client.validators({
args: ["BOND_STATUS_BONDED", page],
})
if (hasNextPage(pageResponse)) { /* fetch next */ }
// Lazy async generator — one page per round-trip, exit early
for await (const page of paginated(
(req, signal) =>
client.validators({ args: ["BOND_STATUS_BONDED", req] })
.then(r => [r.validators, r.pageResponse]),
{ limit: 50n, signal },
)) {
render(page)
if (foundWhatINeed) break
}
// Eager collect-all
const all = await paginate(
(req) =>
client.validators({ args: ["BOND_STATUS_BONDED", req] })
.then(r => [r.validators, r.pageResponse]),
{ limit: 100n },
)Gov proposal encoding
import { encodeGovProposalJson } from "viem-cosmos"
const jsonProposal = encodeGovProposalJson({
messages: [{
"@type": "/cosmos.bank.v1beta1.MsgSend",
from_address: govModuleAddress,
to_address: recipient,
amount: [{ denom: "uatom", amount: "1" }],
}],
title: "Community pool spend",
summary: "Send 1 uatom from the gov module account.",
})
await client.submitProposal({ args: [proposer, jsonProposal, deposit] })Pure transforms
The raw transform functions are exported for use with wagmi's select, multicall,
or any other composition pattern that bypasses the decorator layer:
import { formatValidators, formatProposal, formatVote, formatSigningInfo } from "viem-cosmos"
// Use with wagmi useReadContract select:
const { data } = useReadContract({
abi: stakingIAbi,
functionName: "validators",
select: (raw) => formatValidators(raw, { valoper: "cosmosvaloper" }),
})Available: formatValidator, formatValidators, formatDelegation,
formatProposal, formatProposals, formatTallyResult, formatVote,
formatVotes, formatSigningInfo, formatSigningInfos, formatSlashingParams,
formatDecCoins, formatDecStruct.
Typed errors
All errors extend viem's BaseError and integrate with error.walk(...):
import {
CosmosBaseError,
InvalidBech32AddressError,
Bech32PrefixMismatchError,
InvalidAddressLengthError,
ChainMissingBech32PrefixError,
InvalidDecimalScaleError,
UnknownEnumValueError,
InvalidPageRequestError,
} from "viem-cosmos"
try {
await client.delegate({ args: [account, "not-a-valoper", amount] })
} catch (e) {
if (e instanceof Bech32PrefixMismatchError) { /* wrong chain or role */ }
if (e instanceof CosmosBaseError) { /* any viem-cosmos error */ }
}Custom precompile factory
Build a decorator for any precompile ABI not yet shipped as a named decorator:
import { precompileActions } from "viem-cosmos"
import { ics20IAbi } from "cosmos-evm-contracts/precompiles/ics20/ICS20I"
const ics20Actions = precompileActions(
ics20IAbi,
"0x0000000000000000000000000000000000000802",
)
const client = publicClient.extend(ics20Actions())Addresses
Default precompile addresses are exported and used automatically by every decorator:
import { precompileAddresses } from "viem-cosmos"
// {
// bank: "0x0000000000000000000000000000000000000804",
// staking: "0x0000000000000000000000000000000000000800",
// distribution: "0x0000000000000000000000000000000000000801",
// gov: "0x0000000000000000000000000000000000000805",
// slashing: "0x0000000000000000000000000000000000000806",
// }A test asserts these never drift from the *_PRECOMPILE_ADDRESS constants in
cosmos-evm-contracts. Override per decorator or per call if a chain wires a
precompile to a different address:
// per decorator
const client = publicClient.extend(bankActions({ precompileAddress: "0x…" }))
// per combined client
const client = createCosmosClient({
chain, transport: http(),
precompiles: { bank: { precompileAddress: "0x…" } },
})Testing
Unit tests run without a chain; e2e tests exercise the full stack against the
evmd reference chain via cosmock, including a gov proposal lifecycle
(submit → deposit → vote → tally) and slashing signing-info reads.
pnpm --filter viem-cosmos typecheck # tsc --noEmit
pnpm --filter viem-cosmos test # unit tests (no chain required)
pnpm --filter viem-cosmos test:e2e # on-chain e2e (local Cosmos EVM via cosmock)
pnpm --filter viem-cosmos build # tsdown → dist (ESM + CJS + d.ts)Status
v0.x — API is not yet stable. The validator-economy precompiles (bank, staking,
distribution, gov, slashing) are covered. Remaining precompiles (ICS-20,
vesting, …) are reachable today via precompileActions and will receive named
decorators in future releases.
License
MIT © Wonhee Lee
