Tests

The Mimic Protocol Test Library (@mimicprotocol/test-ts) provides tools for simulating task execution to validate expected task behavior under different scenarios.


1. Getting Started

1.1. Installation

# Install dependencies
yarn add @mimicprotocol/test-ts

# Optional
yarn add @mimicprotocol/sdk

1.2. Basic test structure

Every test follows this structure:

import { runTask, /* types */ } from '@mimicprotocol/test-ts'

describe('Task', () => {
  // 1. Define context, inputs, mocks
  const taskDir = './build'
  const context = { /* required fields */ }
  const inputs = { /* manifest inputs */ }

  it('produces the expected intents', async () => {
    // 2. Execute the task
    const result = await runTask(taskDir, context, { inputs, /* needed mocks */ })
    
    // 3. Check the task outputs
    expect(result.success).to.be.true
    expect(result.intents).to.have.lengthOf(N)
  })
})

1.3. Project setup

Create a basic task project:

# Initialize a new Mimic project
mimic init -d my-automation-task && cd my-automation-task

# Test your task
mimic test

2. Task Runner Reference

This section describes the parameters and outputs of the runTask function.

2.1. Parameters

2.1.1. Task directory

The directory where the compiled task .wasm is located.

const taskDir = './build'

2.1.2. Context

The context includes the fields needed during task execution.

import { Context } from '@mimicprotocol/test-ts'

const context: Context = {
  user: '0xAddress',
  settlers: [{ address: '0xAddress', chainId: 10 }], // One per chain used in the task
  timestamp: Date.now(), // number (in milliseconds)
}

2.1.3. Inputs

The values for the inputs defined in the manifest. For example, if the manifest declares:

inputs:
  - chainId: uint32
  - token: address
  - amount: string
  - feeAmount: uint256

Then, the inputs may be:

const inputs = {
  chainId: 10,
  token: '0xAddress',
  amount: '1.5', // 1.5 tokens
  feeAmount: '100000', // 0.1 tokens (6 decimals)
}

2.1.4. Prices mock

The responses for the price queries made in the task.

For example, if the task does:

import { environment } from '@mimicprotocol/lib-ts'

const price = environment.getPrice(dai)
// or
const amountInUsd = amountInDai.toUsd()
// or
const wethAmount = usdcAmount.toTokenAmount(weth)

Then, the prices mock may be:

import { GetPriceMock } from '@mimicprotocol/test-ts'
import { fp } from '@mimicprotocol/sdk'

const prices: GetPriceMock[] = [
  // Mock for `getPrice` and `toUsd`
  {
    request: { token: '0xDAI', chainId: 10 },
    response: [fp(1).toString()], // 1 DAI = 1 USD
  },
  // Mocks for `toTokenAmount`
  {
    request: { token: '0xUSDC', chainId: 10 },
    response: [fp(0.99).toString()], // 1 USDC = 0.99 USD
  },
  {
    request: { token: '0xWETH', chainId: 10 },
    response: [fp(4200).toString()], // 1 WETH = 4200 USD
  },
]

2.1.5. Relevant tokens mock

The responses for the relevant tokens queries made in the task.

For example, if the task does:

import { ChainId, environment } from '@mimicprotocol/lib-ts'

const userTokens = environment.getRelevantTokens(
  context.user,
  [ChainId.OPTIMISM],
  USD.zero(),
  [USDC, USDT],
  ListType.AllowList
)

Then, the relevant tokens mock may be:

import { GetRelevantTokensMock } from '@mimicprotocol/test-ts'

const relevantTokens: GetRelevantTokensMock[] = [
  {
    request: {
      owner: '0xAddress',
      chainIds: [10],
      usdMinAmount: '0',
      tokenFilter: 0, // AllowList = 0, DenyList = 1
      tokens: [
        { address: '0xUSDC', chainId: 10 },
        { address: '0xUSDT', chainId: 10 },
      ],
    },
    response: [
      {
        timestamp: context.timestamp,
        balances: [
          { token: { address: '0xUSDC', chainId: 10 }, balance: '10000000' }, // 10 USDC
          { token: { address: '0xUSDT', chainId: 10 }, balance: '10000' }, // 0.01 USDT
        ],
      },
    ],
  },
]

2.1.6. Contract calls mock

The responses for the contract calls made in the task. Only for read functions, i.e., those that are not intended to generate intents. Note: Token decimals and symbol are sometimes queried behind the scenes and need mocks in those cases.

For example, if the task does:

// `TokenAmount#fromStringDecimal` calls `decimals`
const tokenAmount = TokenAmount.fromStringDecimal(USDC, '10.5')

// `TokenAmount#toString` calls `symbol`
log.info(`Transfer amount: ${tokenAmount}`)

// `balanceOf` needs a mock
const tokenContract = new ERC20(USDC, ChainId.OPTIMISM)
const balance = tokenContract.balanceOf(recipient)

// `mint` does not need a mock
tokenContract.mint(recipient, amount).build().send()

Then, the calls mock may be:

import { ContractCallMock } from '@mimicprotocol/test-ts'
import { Interface } from 'ethers'

import ERC20Abi from '../abis/ERC20.json'

const ERC20Interface = new Interface(ERC20Abi)

const calls: ContractCallMock[] = [
  {
    request: { 
      chainId: 10,
      to: '0xUSDC',
      fnSelector: ERC20Interface.getFunction('decimals').selector,
    },
    response: { value: '6', abiType: 'uint8' },
  },
  {
    request: { 
      chainId: 10,
      to: '0xUSDC',
      fnSelector: ERC20Interface.getFunction('symbol').selector,
    },
    response: { value: 'USDC', abiType: 'string' },
  },
  {
    request: {
      chainId: 10,
      to: '0xUSDC',
      fnSelector: ERC20Interface.getFunction('balanceOf').selector,
      params: [{ value: '0xAddress', abiType: 'address' }],
    },
    response: { value: '10000000', abiType: 'uint256' }, // 10 USDC
  },
]

2.1.7. Subgraph queries mock

The responses for the subgraph queries made in the task.

For example, if the task does:

import { ChainId, environment } from '@mimicprotocol/lib-ts'

const response = environment.subgraphQuery(
  ChainId.OPTIMISM,
  'QmSubgraphId',
  '{ tokens { symbol holders } }'
)

Then, the subgraph queries mock may be:

import { SubgraphQueryMock } from '@mimicprotocol/test-ts'

const subgraphQueries: SubgraphQueryMock[] = [
  {
    request: {
      timestamp: context.timestamp,
      chainId: 10,
      subgraphId: 'QmSubgraphId',
      query: '{ tokens { id symbol } }',
    },
    response: {
      blockNumber: 1,
      data: '{ "tokens": [{ "symbol": "WETH", "holders": "1857" }] }',
    },
  },
]

2.2. Output

The runTask function returns an object containing the following fields:

  • success - Boolean. True if the execution ended properly, or false if it had an error.

  • timestamp - Number. Execution timestamp in milliseconds.

  • fuelUsed - Number. Amount of fuel used during the execution.

  • intents - Array of intents produced by the execution.

  • logs - Array of logs produced by the execution. It may include error logs.

2.2.1. Intents

The fields depend on the type of intents produced by the execution.

Transfer

If the task creates a transfer intent:

const USDC = ERC20Token.fromString('0xUSDC', ChainId.OPTIMISM)
const recipient = Address.fromString('0xRecipient')
const amount = BigInt.fromStringDecimal('1', USDC.decimals)
const maxFee = BigInt.fromStringDecimal('0.1', USDC.decimals)

Transfer.create(USDC, amount, recipient, maxFee).send()

Then, the test should be:

import { Chains, OpType } from '@mimicprotocol/sdk'
import { runTask, Transfer } from '@mimicprotocol/test-ts'

it('produces the expected intent', async () => {
    const result = await runTask(/* parameters */)
    expect(result.success).to.be.true
    
    expect(result.intents).to.have.lengthOf(1)
    const intent = result.intents[0] as Transfer
    
    expect(intent.op).to.be.equal(OpType.Transfer)
    expect(intent.settler).to.be.equal(context.settlers[0].address)
    expect(intent.user).to.be.equal(context.user)
    expect(intent.chainId).to.be.equal(Chains.Optimism)

    expect(intent.transfers).to.have.lengthOf(1)
    expect(intent.transfers[0].token).to.be.equal('0xUSDC')
    expect(intent.transfers[0].amount).to.be.equal('1000000') // 1 USDC
    expect(intent.transfers[0].recipient).to.be.equal('0xRecipient')

    expect(intent.maxFees).to.have.lengthOf(1)
    expect(intent.maxFees[0].token).to.be.equal('0xUSDC')
    expect(intent.maxFees[0].amount).to.be.equal('100000') // 0.1 USDC
})

Swap

If the task creates a swap intent:

const USDC = ERC20Token.fromString('0xUSDC', ChainId.OPTIMISM)
const amountIn = BigInt.fromStringDecimal('1', USDC.decimals)

const WETH = ERC20Token.fromString('0xWETH', ChainId.OPTIMISM)
const minAmountOut = BigInt.fromStringDecimal('0.001', WETH.decimals)

Swap.create(ChainId.OPTIMISM, USDC, amountIn, WETH, minAmountOut).send()

Then, the test should be:

import { Chains, OpType } from '@mimicprotocol/sdk'
import { runTask, Swap } from '@mimicprotocol/test-ts'

it('produces the expected intent', async () => {
    const result = await runTask(/* parameters */)
    expect(result.success).to.be.true
    
    expect(result.intents).to.have.lengthOf(1)
    const intent = result.intents[0] as Swap
    
    expect(intent.op).to.be.equal(OpType.Swap)
    expect(intent.settler).to.be.equal(context.settlers[0].address)
    expect(intent.user).to.be.equal(context.user)
    expect(intent.sourceChain).to.be.equal(Chains.Optimism)
    expect(intent.destinationChain).to.be.equal(Chains.Optimism)

    expect(intent.tokensIn).to.have.lengthOf(1)
    expect(intent.tokensIn[0].token).to.be.equal('0xUSDC')
    expect(intent.tokensIn[0].amount).to.be.equal('1000000') // 1 USDC

    expect(intent.tokensOut).to.have.lengthOf(1)
    expect(intent.tokensOut[0].token).to.be.equal('0xWETH')
    expect(intent.tokensOut[0].minAmount).to.be.equal('1' + '0'.repeat(15)) // 0.001 WETH
    expect(intent.tokensOut[0].recipient).to.be.equal(context.user)
    
    expect(intent.maxFees).to.have.lengthOf(0)
})

Call

If the task creates a call intent:

const USDC = ERC20Token.fromString('0xUSDC', ChainId.OPTIMISM)
const amount = BigInt.fromStringDecimal('1', USDC.decimals)
const maxFee = TokenAmount.fromStringDecimal(USDC, '0.1')
const data = ERC20Utils.encodeApprove(spender, amount)

EvmCallBuilder.forChain(ChainId.OPTIMISM)
  .addCall(USDC, data)
  .addUser(smartAccount)
  .addMaxFee(maxFee)
  .build()
  .send()

Then, the test should be:

import { Chains, OpType } from '@mimicprotocol/sdk'
import { EvmCall, runTask } from '@mimicprotocol/test-ts'
import { Interface } from 'ethers'

import ERC20Abi from '../abis/ERC20.json'

const ERC20Interface = new Interface(ERC20Abi)

it('produces the expected intent', async () => {
    const result = await runTask(/* parameters */)
    expect(result.success).to.be.true
    
    expect(result.intents).to.have.lengthOf(1)
    const intent = result.intents[0] as EvmCall

    expect(intent.op).to.be.equal(OpType.EvmCall)
    expect(intent.settler).to.be.equal(context.settlers[0].address)
    expect(intent.user).to.be.equal('0xSmartAccount')
    expect(intent.chainId).to.be.equal(Chains.Optimism)

    expect(intent.calls).to.have.lengthOf(1)
    expect(intent.calls[0].target).to.be.equal(USDC)
    expect(intent.calls[0].value).to.be.equal('0')
    const data = ERC20Interface.encodeFunctionData('approve', ['0xSpender', '1000000'])
    expect(intent.calls[0].data).to.be.equal(data)
    
    expect(intent.maxFees).to.have.lengthOf(1)
    expect(intent.maxFees[0].token).to.be.equal('0xUSDC')
    expect(intent.maxFees[0].amount).to.be.equal('100000') // 0.1 USDC
})

2.2.2. Logs

For example, if the task does:

import { log } from '@mimicprotocol/lib-ts'

if (inputs.token == USDC) log.info('Task started')
else throw new Error('Token not supported')

Then, the test should be:

import { runTask } from '@mimicprotocol/test-ts'

describe('when the token is USDC', () => {
  it('executes properly', async () => {
    const result = await runTask(/* 0xUSDC */)
    expect(result.success).to.be.true

    expect(result.logs).to.have.lengthOf(1)
    expect(result.logs[0]).to.include('Task started')
  })
})

describe('when the token is not USDC', () => {
  it('throws an error', async () => {
    const result = await runTask(/* 0xWETH */)
    expect(result.success).to.be.false

    expect(result.logs).to.have.lengthOf(1)
    expect(result.logs[0]).to.include('Token not supported')
  })
})

Last updated