Skip to content
Back to skills

Viem

ASecurity

Reads blockchain data, sends transactions and calls smart contracts from TypeScript with Viem, the type-safe Ethereum library that wagmi is built on. Use when a user asks to read a balance or ERC-20 token data, call or simulate a contract with an ABI, send ETH or tokens, watch or query contract events, resolve ENS names, batch reads with multicall, or replace ethers.js with viem.

  • 142 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
blockchaintypescriptgobashreactnodetestinggit

Works with

  • terminal
  • cli

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill viem --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Viem?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Viem
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-viem/badge)](https://www.skillsdirectory.com/skills/terminalskills-viem)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: viem
description: >-
  Reads blockchain data, sends transactions and calls smart contracts from TypeScript with Viem, the type-safe Ethereum library that wagmi is built on. Use when a user asks to read a balance or ERC-20 token data, call or simulate a contract with an ABI, send ETH or tokens, watch or query contract events, resolve ENS names, batch reads with multicall, or replace ethers.js with viem.
license: Apache-2.0
compatibility: "Node.js 18 or newer, Bun, Deno or a browser; TypeScript 5.0.4 or newer for type inference. Needs a JSON-RPC endpoint (Alchemy, Infura, a public node or a local Anvil) for the chain used."
metadata:
  author: terminal-skills
  version: "1.1.0"
  category: development
  tags:
    - ethereum
    - typescript
    - web3
    - smart-contracts
    - wagmi
  repository: https://github.com/wevm/viem
---

# Viem — Type-Safe Ethereum Interactions for TypeScript

## Overview

Viem (current release line 2.x) is a TypeScript interface to Ethereum and EVM chains. It is split into small, tree-shakeable modules and infers argument and return types from an ABI declared `as const` (or parsed with `parseAbi`). Everything numeric is a native `bigint`. Wagmi's React hooks are built on it, so the same calls work in a front end and in a Node script.

The model is *client + transport + chain*:

- **Public client** — reads: balances, blocks, contract calls, logs, gas estimates.
- **Wallet client** — writes: sends transactions and signs, with a local account (private key, mnemonic) or a browser wallet.
- **Transport** — `http(url)`, `webSocket(url)` or `custom(window.ethereum)`. `http()` with no URL falls back to the chain's public RPC, which is rate-limited; give it your own endpoint.
- **Chain** — an object from `viem/chains` (`mainnet`, `sepolia`, `base`, `arbitrum`, ...) or your own from `defineChain`.

## Instructions

### Install

```bash
npm install viem
```

### Clients

```typescript
import { createPublicClient, createWalletClient, http } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const rpcUrl = process.env.MAINNET_RPC_URL!;     // e.g. an Alchemy or Infura https URL

export const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(rpcUrl),
  batch: { multicall: true },                    // aggregate parallel readContract calls
});

// Server-side only: the key comes from the environment, never from source code
const account = privateKeyToAccount(process.env.DEPLOYER_PRIVATE_KEY as `0x${string}`);
export const walletClient = createWalletClient({ account, chain: mainnet, transport: http(rpcUrl) });
```

In a browser use `createWalletClient({ chain: mainnet, transport: custom(window.ethereum) })` and `await walletClient.requestAddresses()`; pass `account` per call or set it on the client.

### Reads

```typescript
import { formatEther, formatUnits, parseAbi } from "viem";
import { normalize } from "viem/ens";

const erc20Abi = parseAbi([
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)",
  "function transfer(address to, uint256 amount) returns (bool)",
  "event Transfer(address indexed from, address indexed to, uint256 value)",
]);
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";   // USDC on Ethereum mainnet, 6 decimals

const eth = await publicClient.getBalance({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" });
console.log(formatEther(eth), "ETH");

const raw = await publicClient.readContract({
  address: USDC, abi: erc20Abi, functionName: "balanceOf",
  args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],       // result type is bigint
});
console.log(formatUnits(raw, 6), "USDC");

const [decimals, contractHeld] = await publicClient.multicall({  // one RPC round trip
  contracts: [
    { address: USDC, abi: erc20Abi, functionName: "decimals" },
    { address: USDC, abi: erc20Abi, functionName: "balanceOf", args: [USDC] },
  ],
  allowFailure: false,                                         // return values directly, throw on failure
});

const ensAddress = await publicClient.getEnsAddress({ name: normalize("vitalik.eth") });
```

### Writes: simulate, send, wait

`writeContract` does not check that the call will succeed. Simulate first; `simulateContract` returns a ready-to-send `request` and throws the decoded revert reason.

```typescript
import { parseEther, parseUnits } from "viem";

const { request } = await publicClient.simulateContract({
  account, address: USDC, abi: erc20Abi, functionName: "transfer",
  args: ["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", parseUnits("25", 6)],   // 25 USDC
});
const hash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error(`Transfer reverted in block ${receipt.blockNumber}`);

// Plain ETH transfer
const ethHash = await walletClient.sendTransaction({
  to: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", value: parseEther("0.05"),
});
```

### Events

```typescript
// Live: polling over http, push over a webSocket transport
const unwatch = publicClient.watchContractEvent({
  address: USDC, abi: erc20Abi, eventName: "Transfer",
  onLogs: (logs) => logs.forEach((l) => console.log(l.args.from, "->", l.args.to, l.args.value)),
});
// call unwatch() to stop

// Historical: keep the block range small
const latest = await publicClient.getBlockNumber();
const logs = await publicClient.getContractEvents({
  address: USDC, abi: erc20Abi, eventName: "Transfer",
  fromBlock: latest - 500n, toBlock: latest,
});
```

### Errors

```typescript
import { BaseError, ContractFunctionRevertedError } from "viem";

try {
  await publicClient.simulateContract({ /* ... */ });
} catch (err) {
  if (err instanceof BaseError) {
    const revert = err.walk((e) => e instanceof ContractFunctionRevertedError);
    if (revert instanceof ContractFunctionRevertedError) console.error(revert.reason ?? revert.data?.errorName);
    else console.error(err.shortMessage);
  }
}
```

### Other pieces worth knowing

- Local testing: `createTestClient` with `anvil` from `viem/chains` and `http("http://127.0.0.1:8545")` to mine blocks and set balances.
- Fees and gas: `estimateGas`, `estimateFeesPerGas`; viem fills EIP-1559 fees by default.
- Generating types from a Solidity project: use the Wagmi CLI or Foundry/Hardhat artifacts to get ABIs as `const` TypeScript.
- Smart accounts (ERC-4337) live under `viem/account-abstraction`; check the docs for the bundler client before relying on them.

## Examples

### Example 1: Show a wallet's USDC balance

Request: "Print the USDC balance of vitalik.eth with two decimals."

```typescript
// balance.ts  (run: MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/$ALCHEMY_KEY npx tsx balance.ts)
import { createPublicClient, http, parseAbi, formatUnits } from "viem";
import { mainnet } from "viem/chains";
import { normalize } from "viem/ens";

const client = createPublicClient({ chain: mainnet, transport: http(process.env.MAINNET_RPC_URL) });
const abi = parseAbi(["function balanceOf(address) view returns (uint256)"]);

const owner = await client.getEnsAddress({ name: normalize("vitalik.eth") });
if (!owner) throw new Error("vitalik.eth does not resolve");
const raw = await client.readContract({
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", abi, functionName: "balanceOf", args: [owner],
});
console.log(`${Number(formatUnits(raw, 6)).toFixed(2)} USDC`);
```

Result: one line such as `37.19 USDC`. Two RPC calls, both reads, no key or gas needed.

### Example 2: Send tokens on Sepolia with a safety check

Request: "Send 10 test tokens to 0x7099... on Sepolia and tell me when it is confirmed."

```typescript
import { createPublicClient, createWalletClient, http, parseAbi, parseUnits } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const transport = http(process.env.SEPOLIA_RPC_URL);
const account = privateKeyToAccount(process.env.TEST_WALLET_KEY as `0x${string}`);
const pub = createPublicClient({ chain: sepolia, transport });
const wallet = createWalletClient({ account, chain: sepolia, transport });
const abi = parseAbi(["function transfer(address to, uint256 amount) returns (bool)"]);

const { request } = await pub.simulateContract({
  account, address: process.env.TOKEN_ADDRESS as `0x${string}`, abi, functionName: "transfer",
  args: ["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", parseUnits("10", 18)],
});
const hash = await wallet.writeContract(request);
const receipt = await pub.waitForTransactionReceipt({ hash, confirmations: 2 });
console.log(receipt.status, hash);
```

Result: `success 0x...` after two confirmations. If the balance is too low, `simulateContract` throws `ContractFunctionExecutionError` with the revert reason and nothing is broadcast.

## Guidelines

- Keep private keys in environment variables or a secrets manager, and use a throwaway key on testnets. Never log an account object or a key.
- Declare ABIs `as const` or with `parseAbi`; without that, types collapse to `unknown` and `functionName` is not checked.
- Amounts are `bigint`: use `parseEther`, `parseUnits`, `formatUnits` with the token's real `decimals` (USDC has 6, most others 18). Never mix `number` and `bigint`.
- Always simulate before writing and wait for the receipt; a transaction hash does not mean success. Check `receipt.status`.
- `getContractEvents`/`getLogs` over wide block ranges fail or need an archive node on many providers; page through ranges of a few thousand blocks and use an authenticated endpoint.
- `watchContractEvent` polls over HTTP (`pollingInterval`, default 4 seconds); use a `webSocket` transport for push delivery.
- Use `normalize` from `viem/ens` on user-typed ENS names so look-alike characters do not resolve to the wrong address.
- Set `chain` on the wallet client: viem uses it to check the connected wallet's network and to build the transaction.
- For a quick script that only reads, ethers.js and viem are equivalent; choose viem when you want typed ABIs, small bundles or wagmi compatibility.

Files in this skill

  • SKILL.md5 KB
  • _scores.json1.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…