This document contains guidelines for AI agents working on the Viem codebase.
Guidelines for authoring docs and guides under site/pages/.
- Do not use em dashes (
—) in docs. Rewrite with a colon, comma, parentheses, or separate sentences instead.
- Use Title Case for all headings. Capitalize the first and last word and all major words
(nouns, verbs, adjectives, adverbs, pronouns). Keep minor words lowercase unless they are the
first or last word: articles (
a,an,the), short coordinating conjunctions (and,but,or,nor,for,so,yet), and short prepositions (in,on,to,of,for,with,as, etc.). - Examples:
Send a Transaction,Pay Fees with Stablecoins,Set a Default Fee Token,See More. - Code identifiers inside a heading keep their original casing (e.g.
### Prefer Sync Actions, but## sendTransactionSyncwhen the heading is the identifier).
- Link out to every action (and other API) referenced in prose, on first mention. Tempo Actions
link to
/tempo/actions/<namespace>.<action>; core Viem actions link to their/docs/...page.
- A guide's main body is a
## Recipessection: independent, self-contained tasks, each a###subheading with no enforced order. Do not use step-by-step "Walkthrough" sections. - Do not repeat client setup as its own recipe. Open the Recipes section with a prerequisite
line linking to Getting Started, e.g. "These recipes assume you have
set up a Tempo client." Code examples still include a
viem.config.tstab via[!include ~/snippets/tempo/viem.config.ts:setup]. - Always show imports in code examples. Do not use the twoslash
// ---cut---directive to hide import statements. Eachexample.tsblock starts with its imports (includingimport { client } from './viem.config'), then a blank line, then the example body. - Guide section order:
## Overview→## Recipes→## Best Practices→## See More.
When generating actions (in src/tempo/actions/), follow these guidelines.
An example of a generated action set can be found in src/tempo/actions/token.ts.
- All actions must be based on precompile contract specifications in
test/tempo/docs/specs/. - It could be likely that some interfaces may be inconsistent between the specs (
test/tempo/docs/specs) and the precompiles (test/tempo/crates/contracts/src/precompiles). Always prefer the precompile interfaces over the specs. - If the specification is unclear or missing details, prompt the developer for guidance rather than making assumptions
All actions must include comprehensive JSDoc with:
- Function description - What the action does
@exampleblock - Complete working example showing:- Required imports (
createClient,http, action imports) - Client setup with chain and transport
- Action usage with realistic parameters
- Expected return value handling (if applicable)
- Required imports (
@paramtags - For each parameter (client, parameters)@returnstag - Description of the return value
Example:
/**
* Gets the pool ID for a token pair.
*
* @example
* ```ts
* import { createClient, http } from 'viem'
* import { tempo } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* chain: tempo.extend({ feeToken: '0x20c0000000000000000000000000000000000001' })
* transport: http(),
* })
*
* const poolId = await Actions.amm.getPoolId(client, {
* userToken: '0x...',
* validatorToken: '0x...',
* })
* ```
*
* @param client - Client.
* @param parameters - Parameters.
* @returns The pool ID.
*/For view/pure functions that only read state:
- Use
readContractfromviem/actions - Return type should use
ReadContractReturnType - Parameters extend
ReadParameters
For state-changing functions, both variants must be implemented:
1. Standard Async Variant
- Uses
writeContractfromviem/actions - Returns transaction hash
- Async operation that doesn't wait for confirmation
export async function myAction<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: myAction.Parameters<chain, account>,
): Promise<myAction.ReturnValue> {
return myAction.inner(writeContract, client, parameters)
}2. Sync Variant (*Sync)
- Named with
Syncsuffix (e.g.,mintSync,burnSync,rebalanceSwapSync) - Uses
writeContractSyncfromviem/actions - Waits for transaction confirmation
- Returns both the receipt and extracted event data
- Must use
extractEventto get return values (notsimulateContract)
export async function myActionSync<
chain extends Chain | undefined,
account extends Account | undefined,
>(
client: Client<Transport, chain, account>,
parameters: myActionSync.Parameters<chain, account>,
): Promise<myActionSync.ReturnValue> {
const { throwOnReceiptRevert = true, ...rest } = parameters
const receipt = await myAction.inner(writeContractSync, client, {
...rest,
throwOnReceiptRevert,
} as never)
const { args } = myAction.extractEvent(receipt.logs)
return {
...args,
receipt,
} as never
}All actions must include the following components within their namespace:
// Read actions
export type Parameters = ReadParameters & Args
// Write actions
export type Parameters<
chain extends Chain | undefined = Chain | undefined,
account extends Account | undefined = Account | undefined,
> = WriteParameters<chain, account> & Args Arguments must be documented with JSDoc.
export type Args = {
/** JSDoc for each argument */
argName: Type
}// Read actions
export type ReturnValue = ReadContractReturnType<typeof Abis.myAbi, 'functionName', never>
// Write actions
export type ReturnValue = WriteContractReturnTypeWrite actions must include an ErrorType export. Use BaseErrorType from viem as a placeholder with a TODO comment for future exhaustive error typing:
// TODO: exhaustive error type
export type ErrorType = BaseErrorTypeRequired for all actions - enables composition with other viem actions:
/**
* Defines a call to the `functionName` function.
*
* Can be passed as a parameter to:
* - [`estimateContractGas`](https://viem.sh/docs/contract/estimateContractGas): estimate the gas cost of the call
* - [`simulateContract`](https://viem.sh/docs/contract/simulateContract): simulate the call
* - [`sendCalls`](https://viem.sh/docs/actions/wallet/sendCalls): send multiple calls
*
* @example
* ```ts
* import { createClient, http, walletActions } from 'viem'
* import { tempo } from 'viem/chains'
* import { Actions } from 'viem/tempo'
*
* const client = createClient({
* chain: tempo.extend({ feeToken: '0x20c0000000000000000000000000000000000001' })
* transport: http(),
* }).extend(walletActions)
*
* const hash = await client.sendTransaction({
* calls: [actions.amm.myAction.call({ arg1, arg2 })],
* })
* ```
*
* @param args - Arguments.
* @returns The call.
*/
export function call(args: Args) {
return defineCall({
address: Addresses.contractName,
abi: Abis.contractName,
args: [/* transformed args */],
functionName: 'functionName',
})
}The call function enables these use cases:
sendCalls- Batch multiple calls in one transactionsendTransactionwithcalls- Send transaction with multiple operationsmulticall- Execute multiple calls in parallelestimateContractGas- Estimate gas costssimulateContract- Simulate execution
Required for all actions that emit events:
/**
* Extracts the `EventName` event from logs.
*
* @param logs - The logs.
* @returns The `EventName` event.
*/
export function extractEvent(logs: Log[]) {
const [log] = parseEventLogs({
abi: Abis.contractName,
logs,
eventName: 'EventName',
strict: true,
})
if (!log) throw new Error('`EventName` event not found.')
return log
}/** @internal */
export async function inner<
action extends typeof writeContract | typeof writeContractSync,
chain extends Chain | undefined,
account extends Account | undefined,
>(
action: action,
client: Client<Transport, chain, account>,
parameters: Parameters<chain, account>,
): Promise<ReturnType<action>> {
const { arg1, arg2, ...rest } = parameters
const call = myAction.call({ arg1, arg2 })
return (await action(client, {
...rest,
...call,
} as never)) as never
}Organize actions using namespace pattern:
export async function myAction(...) { ... }
export namespace myAction {
export type Parameters = ...
export type Args = ...
export type ReturnValue = ...
export async function inner(...) { ... } // for write actions
export function call(args: Args) { ... }
export function extractEvent(logs: Log[]) { ... } // for mutate actions
}When encountering situations that require judgment:
- Specification ambiguities: Prompt developer for clarification
- Missing contract details: Request ABI or specification update
- Event structure uncertainty: Ask for event definition
- Parameter transformations: Confirm expected input/output formats
- Edge cases: Discuss handling strategy with developer
- Action names should match contract function names (in camelCase)
- Sync variants use
Syncsuffix (e.g.,myActionSync) - Event names in
extractEventshould match contract event names exactly - Namespace components should be exported within the action's namespace
Tests should be co-located with actions in *action-name*.test.ts files. Reference contract tests in test/tempo/crates/precompiles/ for expected behavior.
See src/tempo/actions/token.test.ts for a comprehensive example of test patterns and structure.
Organize tests by action name with a default test case and behavior-specific tests:
describe('actionName', () => {
test('default', async () => {
// Test the primary/happy path scenario
const { receipt, ...result } = await Actions.namespace.actionSync(client, {
param1: value1,
param2: value2,
})
expect(receipt).toBeDefined()
expect(result).toMatchInlineSnapshot(`...`)
})
test('behavior: specific edge case', async () => {
// Test specific behaviors, edge cases, or variations
})
test('behavior: error conditions', async () => {
// Test error handling
await expect(
actions.namespace.actionSync(client, { ... })
).rejects.toThrow()
})
})
describe.todo('unimplementedAction')