Skip to main content
Compose has built-in functionality for handling mempool eviction and re-orgs, making it easy to get strong guarentees about your transactions without lots of bespoke code.

Confirmations

By default all transactions via wallet.writeContract() or state-changing methods on contracts will wait for one confirmation before resolving the method’s promise. This means that the transaction will be seen in at least one block by the time your task execution advances. But you can configure this further with the optional TransactionConfirmation logic. If your transaction doesn’t make it into the number of blocks specified the context function call will fail and retry logic will kick in. If it still doesn’t make it into the desired number of blocks after retries are exhausted then the promise will reject. Here’s an example for both wallet.writeContract() and on a contract class method
/// <reference types="../../.compose/types.d.ts" /> 

import { BitcoinOracleContract } from "../.compose/generated";

export async function main(
  { evm, env }: TaskContext,
  _args: any
) {
  const wallet = await evm.wallet();

  const { hash } = await wallet.writeContract(
    evm.chains.polygon,
    env.CONTRACT_ADDRESS,
    "reportPayouts(bytes32,uint256[])",
    [resultId, payouts],
    {
      // this will not resolve the promise until the transaction has been seen in 5 blocks
      // if the transaction doesn't make it into five blocks (maybe is evicted from mempool, etc), 
      // then your retry config will determine whether to retry the transaction
      confirmations: 5, 
    },
    {
      max_attempts: 5,
    }
  );

  const bitcoinOracleContract = new BitcoinOracleContract(env.ORACLE_ADDRESS, evm.chains.base, wallet);

  await bitcoinOracleContract.write.write(
    timestampAsBytes32, 
    priceAsBytes32,
    { 
      // this will not resolve the promise until the transaction has been seen in 3 blocks
      confirmations: 3, 
    }
  );
}

Reorgs

Reorgs could revert your transaction after the the promise is resolved and the confirmations are all seen. For example, if you have 5 confirmations but later on a 100 block reorg occurs, you can configure your transaction with various retry “behaviors” to handle that situation. Like with confirmations, reorg handling can be used both with wallet.writeContract() as well as any state-changing methods on a smart contract class. There are several behaviors that we support currently:
behaviordescriptionother params
replayre-executes transaction with new gas and noncen/a
logjust logs the reorg to your standard app logslogLevel (optional)
customallows you to pass a compose task to call for customer handlingtask (name of compose task)

Examples

/// <reference types="../../.compose/types.d.ts" /> 

import { BitcoinOracleContract } from "../.compose/generated";

export async function main(
  { evm, env }: TaskContext,
  _args: any
) {
  const wallet = await evm.wallet();

  const { hash } = await wallet.writeContract(
    evm.chains.polygon,
    env.CONTRACT_ADDRESS,
    "reportPayouts(bytes32,uint256[])",
    [resultId, payouts],
    {
      confirmations: 5, 
      onReorg: {
        action: {
          // this will replay the transaction with a fresh nonce and gas if the transaction is reorged off chain after the 5 confirmations specified below
          // it will then start watching again for the number of blocks specified in the "depth" property
          type: "replay", 
        }, 
        // we'll watch this transaction in the background for 200 blocks and replay it if the transaction receipt disappears
        depth: 200, 
      },
    },
    {
      max_attempts: 5,
    }
  );

  const { hash } = await wallet.writeContract(
    evm.chains.polygon,
    env.CONTRACT_ADDRESS,
    "reportPayouts(bytes32,uint256[])",
    [resultId, payouts],
    {
      confirmations: 5, 
      onReorg: {
        action: {
          // this will just log in your normal app logs if the transaction is reorged off chain after the 5 confirmations specified below
          type: "log", 
          logLevel: "warn", // defaults to "error"
        }, 
        // we'll watch this transaction in the background for 200 blocks and log if the transaction receipt disappears
        depth: 200, 
      },
    },
    {
      max_attempts: 5,
    }
  );

  const { hash } = await wallet.writeContract(
    evm.chains.polygon,
    env.CONTRACT_ADDRESS,
    "reportPayouts(bytes32,uint256[])",
    [resultId, payouts],
    {
      confirmations: 5, 
      onReorg: {
        action: {
          // this will send the transaction as a payload to the specified task if the transaction is reorged off chain after the 5 confirmations specified below
          type: "custom", 
          task: "reorg-reconciler", // the name of your compose task with custom re-org handling logic
        },
        // we'll watch this transaction in the background for 200 blocks and call your "reorg-reconciler" if the transaction receipt disappears
        depth: 200, 
      },
    },
    {
      max_attempts: 5,
    }
  );
}

Full TransactionConfirmation type

type ReplayOnReorg = {
  type: "replay";
};

type LogOnReorg = {
  type: "log";
  logLevel?: "error" | "info" | "warn"; // defaults to "error"
};

type Custom = {
  type: "custom",
  // your task will be sent with a payload the full transaction minus gas and nonce
  task: string; 
}

type OnReorgOptions = ReplayOnReorg | LogOnReorg | Custom;

export interface TransactionConfirmation {
  // this the number of block confirmations before we resolve the promise
  // i.e. "wait 5 blocks before proceeding to the next step in my task"
  confirmations?: number; 
  onReorg?: {
    action: OnReorgOptions;
    depth: number;
  };
}