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:
- Function description - What the action does
@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)
@param tags - For each parameter (client, parameters)
@returns tag - Description of the return value
Example:
typescript
1/**
2 * Gets the pool ID for a token pair.
3 *
4 * @example
5 * ```ts
6 * import { createClient, http } from 'viem'
7 * import { tempo } from 'tempo.ts/chains'
8 * import { Actions } from 'tempo.ts/viem'
9 *
10 * const client = createClient({
11 * chain: tempo({ feeToken: '0x20c0000000000000000000000000000000000001' })
12 * transport: http(),
13 * })
14 *
15 * const poolId = await Actions.amm.getPoolId(client, {
16 * userToken: '0x...',
17 * validatorToken: '0x...',
18 * })
19 * ```
20 *
21 * @param client - Client.
22 * @param parameters - Parameters.
23 * @returns The pool ID.
24 */
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
typescript
1export async function myAction<
2 chain extends Chain | undefined,
3 account extends Account | undefined,
4>(
5 client: Client<Transport, chain, account>,
6 parameters: myAction.Parameters<chain, account>,
7): Promise<myAction.ReturnValue> {
8 return myAction.inner(writeContract, client, parameters)
9}
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)
typescript
1export async function myActionSync<
2 chain extends Chain | undefined,
3 account extends Account | undefined,
4>(
5 client: Client<Transport, chain, account>,
6 parameters: myActionSync.Parameters<chain, account>,
7): Promise<myActionSync.ReturnValue> {
8 const { throwOnReceiptRevert = true, ...rest } = parameters
9 const receipt = await myAction.inner(writeContractSync, client, {
10 ...rest,
11 throwOnReceiptRevert,
12 } as never)
13 const { args } = myAction.extractEvent(receipt.logs)
14 return {
15 ...args,
16 receipt,
17 } as never
18}
Namespace Properties
All actions must include the following components within their namespace:
1. Parameters Type
typescript
1// Read actions
2export type Parameters = ReadParameters & Args
3
4// Write actions
5export type Parameters<
6 chain extends Chain | undefined = Chain | undefined,
7 account extends Account | undefined = Account | undefined,
8> = WriteParameters<chain, account> & Args
2. Args Type
Arguments must be documented with JSDoc.
typescript
1export type Args = {
2 /** JSDoc for each argument */
3 argName: Type
4}
3. ReturnValue Type
typescript
1// Read actions
2export type ReturnValue = ReadContractReturnType<typeof Abis.myAbi, 'functionName', never>
3
4// Write actions
5export 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:
typescript
1// TODO: exhaustive error type
2export type ErrorType = BaseErrorType
5. call Function
Required for all actions - enables composition with other viem actions:
typescript
1/**
2 * Defines a call to the `functionName` function.
3 *
4 * Can be passed as a parameter to:
5 * - [`estimateContractGas`](https://viem.sh/docs/contract/estimateContractGas): estimate the gas cost of the call
6 * - [`simulateContract`](https://viem.sh/docs/contract/simulateContract): simulate the call
7 * - [`sendCalls`](https://viem.sh/docs/actions/wallet/sendCalls): send multiple calls
8 *
9 * @example
10 * ```ts
11 * import { createClient, http, walletActions } from 'viem'
12 * import { tempo } from 'tempo.ts/chains'
13 * import { Actions } from 'tempo.ts/viem'
14 *
15 * const client = createClient({
16 * chain: tempo({ feeToken: '0x20c0000000000000000000000000000000000001' })
17 * transport: http(),
18 * }).extend(walletActions)
19 *
20 * const hash = await client.sendTransaction({
21 * calls: [actions.amm.myAction.call({ arg1, arg2 })],
22 * })
23 * ```
24 *
25 * @param args - Arguments.
26 * @returns The call.
27 */
28export function call(args: Args) {
29 return defineCall({
30 address: Addresses.contractName,
31 abi: Abis.contractName,
32 args: [/* transformed args */],
33 functionName: 'functionName',
34 })
35}
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
Required for all actions that emit events:
typescript
1/**
2 * Extracts the `EventName` event from logs.
3 *
4 * @param logs - The logs.
5 * @returns The `EventName` event.
6 */
7export function extractEvent(logs: Log[]) {
8 const [log] = parseEventLogs({
9 abi: Abis.contractName,
10 logs,
11 eventName: 'EventName',
12 strict: true,
13 })
14 if (!log) throw new Error('`EventName` event not found.')
15 return log
16}
7. inner Function (for write actions)
typescript
1/** @internal */
2export async function inner<
3 action extends typeof writeContract | typeof writeContractSync,
4 chain extends Chain | undefined,
5 account extends Account | undefined,
6>(
7 action: action,
8 client: Client<Transport, chain, account>,
9 parameters: Parameters<chain, account>,
10): Promise<ReturnType<action>> {
11 const { arg1, arg2, ...rest } = parameters
12 const call = myAction.call({ arg1, arg2 })
13 return (await action(client, {
14 ...rest,
15 ...call,
16 } as never)) as never
17}
Namespace Structure
Organize actions using namespace pattern:
typescript
1export async function myAction(...) { ... }
2
3export namespace myAction {
4 export type Parameters = ...
5 export type Args = ...
6 export type ReturnValue = ...
7
8 export async function inner(...) { ... } // for write actions
9 export function call(args: Args) { ... }
10 export function extractEvent(logs: Log[]) { ... } // for mutate actions
11}
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:
typescript
1describe('actionName', () => {
2 test('default', async () => {
3 // Test the primary/happy path scenario
4 const { receipt, ...result } = await Actions.namespace.actionSync(client, {
5 param1: value1,
6 param2: value2,
7 })
8
9 expect(receipt).toBeDefined()
10 expect(result).toMatchInlineSnapshot(`...`)
11 })
12
13 test('behavior: specific edge case', async () => {
14 // Test specific behaviors, edge cases, or variations
15 })
16
17 test('behavior: error conditions', async () => {
18 // Test error handling
19 await expect(
20 actions.namespace.actionSync(client, { ... })
21 ).rejects.toThrow()
22 })
23})
24
25describe.todo('unimplementedAction')