Skip to content

Latest commit

 

History

History
370 lines (295 loc) · 11.1 KB

File metadata and controls

370 lines (295 loc) · 11.1 KB

Viem Agent Guidelines

This document contains guidelines for AI agents working on the Viem codebase.

Documentation (Site)

Guidelines for authoring docs and guides under site/pages/.

Prose

  • Do not use em dashes () in docs. Rewrite with a colon, comma, parentheses, or separate sentences instead.

Headings

  • 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 ## sendTransactionSync when the heading is the identifier).

Links

  • 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.

Guides

  • A guide's main body is a ## Recipes section: 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.ts tab 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. Each example.ts block starts with its imports (including import { client } from './viem.config'), then a blank line, then the example body.
  • Guide section order: ## Overview## Recipes## Best Practices## See More.

Tempo Code Generation

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.

Source of Truth

  • 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

Documentation Requirements

All actions must include comprehensive JSDoc with:

  1. Function description - What the action does
  2. @example block - 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)
  3. @param tags - For each parameter (client, parameters)
  4. @returns tag - 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.
 */

Action Types

Read-Only Actions

For view/pure functions that only read state:

  • Use readContract from viem/actions
  • Return type should use ReadContractReturnType
  • Parameters extend ReadParameters

Mutate-Based Actions

For state-changing functions, both variants must be implemented:

1. Standard Async Variant

  • Uses writeContract from viem/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 Sync suffix (e.g., mintSync, burnSync, rebalanceSwapSync)
  • Uses writeContractSync from viem/actions
  • Waits for transaction confirmation
  • Returns both the receipt and extracted event data
  • Must use extractEvent to get return values (not simulateContract)
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
}

Namespace Properties

All actions must include the following components within their namespace:

1. Parameters Type

// 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 

2. Args Type

Arguments must be documented with JSDoc.

export type Args = {
  /** JSDoc for each argument */
  argName: Type
}

3. ReturnValue Type

// Read actions
export type ReturnValue = ReadContractReturnType<typeof Abis.myAbi, 'functionName', never>

// Write actions
export type ReturnValue = WriteContractReturnType

4. ErrorType Type (for write actions)

Write 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 = BaseErrorType

5. call Function

Required 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 transaction
  • sendTransaction with calls - Send transaction with multiple operations
  • multicall - Execute multiple calls in parallel
  • estimateContractGas - Estimate gas costs
  • simulateContract - Simulate execution

6. extractEvent Function (for mutate-based actions)

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
}

7. inner Function (for write actions)

/** @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
}

Namespace Structure

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
}

Decision-Making

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

Naming Conventions

  • Action names should match contract function names (in camelCase)
  • Sync variants use Sync suffix (e.g., myActionSync)
  • Event names in extractEvent should match contract event names exactly
  • Namespace components should be exported within the action's namespace

Testing

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.

Test 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')