Skip to main content

Transaction types on Celo

This page contains an explainer on transaction types supported on Celo and a demo to make specific transactions.

IMPORTANT This repo is for educational purposes only. The information provided here may be inaccurate. Please don’t rely on it exclusively to implement low-level client libraries.

Summary

Celo has support for all Ethereum transaction types (i.e. "100% Ethereum compatibility") and a single Celo transaction type.

Actively supported on Celo

ChainTransaction type#SpecificationRecommendedSupportComment
Dynamic fee transaction v2123CIP-64Active 🟢Supports paying gas in custom fee currencies
Dynamic fee transaction2EIP-1559 (CIP-42)Active 🟢Typical Ethereum transaction
Access list transaction1EIP-2930 (CIP-35)Active 🟢Does not support dynamically changing base fee per gas
Legacy transaction0Ethereum Yellow Paper (CIP-35)Active 🟢Does not support dynamically changing base fee per gas

Scheduled for deprecation on Celo

ChainTransaction type#SpecificationRecommendedSupportComment
Dynamic fee transaction124CIP-42Security 🟠Deprecation warning published in Gingerbread hard fork
Legacy transaction0Celo Mainnet launch (Blockchain client v1.0.0)Security 🟠Deprecation warning published in Gingerbread hard fork

The stages of support are:

  • Active support 🟢: the transaction type is supported and recommended for use.
  • Security support 🟠: the transaction type is supported but not recommended for use because it might be deprecated in the future.
  • Deprecated 🔴: the transaction type is not supported and not recommended for use.

Client library support

Legend:

  • = support for the recommended Ethereum transaction type (2)
  • = support for the recommended Celo transaction type (123)
  • ✅ = available
  • ❌ = not available
Client libraryLanguagesincesinceComment
viemTS/JS>1.19.5---
ethersTS/JSSupport via fork in
celo-ethers-wrapper
celo-ethers-wrapperTS/JS>2.0.0---
web3jsTS/JSSupport via fork in
contractkit
contractkitTS/JS>5.0.0---
Web3jJava---
rust-ethersRust---
browniePython---

Background

Legacy transactions

Ethereum originally had one format for transactions (now called "legacy transactions"). A legacy transaction contains the following transaction parameters: nonce, gasPrice, gasLimit, recipient, amount, data, and chaindId.

To produce a valid "legacy transaction":

  1. the transaction parameters are RLP-encoded:

    RLP([nonce, gasprice, gaslimit, recipient, amount, data, chaindId, 0, 0])
  2. the RLP-encoded transaction is hashed (using Keccak256).

  3. the hash is signed with a private key using the ECDSA algorithm, which generates the v, r, and s signature parameters.

  4. the transaction and signature parameters above are RLP-encoded to produce a valid signed transaction:

    RLP([nonce, gasprice, gaslimit, recipient, amount, data, v, r, s])

A valid signed transaction can then be submitted on-chain, and its raw parameters can be parsed by RLP-decoding the transaction.

Typed transactions

Over time, the Ethereum community has sought to add new types of transactions such as dynamic fee transactions (EIP-1559: Fee market change for ETH 1.0 chain) or optional access list transactions (EIP-2930: Optional access lists) to supported new desired behaviors on the network.

To allow new transactions to be supported without breaking support with the legacy transaction format, the concept of typed transactions was proposed in EIP-2718: Typed Transaction Envelope, which introduces a new high-level transaction format that is used to implement all future transaction types.

Distinguishing between legacy and typed transactions

Whereas a valid "legacy transaction" is simply an RLP-encoded list of transaction parameters, a valid "typed transactions" is an arbitrary byte array prepended with a transaction type, where:

  • a transaction type, is a number between 0 (0x00) and 127 (0x7f) representing the type of the transaction, and

  • a transaction payload, is arbitrary byte data that encodes raw transaction parameters in compliance with the specified transaction type.

To distinguish between legacy transactions and typed transactions at the client level, the EIP designers observed that the first byte of a legacy transaction would never be in the range [0, 0x7f] (or [0, 127]), and instead always be in the range [0xc0, 0xfe] (or [192, 254]).

With that observation, transactions can be decoded with the following heuristic:

  • read the first byte of a transaction
  • if it's bigger than 0x7f (127), then it's a legacy transaction. To decode it, you must read all bytes (including the first byte just read) and interpret them as a legacy transaction.
  • else, if it's smaller or equal to 0x7f (127), then it's a typed transaction. To decode it you must read the remaining bytes (excluding the first byte just read) and interpret them according to the specified transaction type.

Every transaction type is defined in an EIP, which specifies how to encode as well as decode transaction payloads. This means that a typed transaction can only be interpreted with knowledge of its transaction type and a relevant decoder.

List of transaction types on Celo

Legacy transaction (0)

NOTE This transaction type is 100% compatible with Ethereum and has no Celo-specific parameters.

Although legacy transactions are never formally prepended with the 0x00 transaction type, they are commonly referred to as "type 0" transactions.

Access list transaction (1)

NOTE This transaction type is 100% compatible with Ethereum and has no Celo-specific parameters.

Dynamic fee transaction (2)

NOTE This transaction type is 100% compatible with Ethereum and has no Celo-specific parameters.

Legacy transaction (0)

NOTE This transaction is not compatible with Ethereum and has three Celo-specific parameters: feecurrency, gatewayfeerecipient, and gatewayfee.

Warning This transaction type is scheduled for deprecation. A deprecation warning was published in the Gingerbread hard fork on Sep 26, 2023.

  • This transaction is defined as follows:

    RLP([nonce, gasprice, gaslimit, feecurrency, gatewayfeerecipient, gatewayfee, recipient, amount, data, v, r, s])
  • It was introduced on Celo during Mainnet launch on Apr 22, 2020 as specified in Blockchain client v1.0.0.

Dynamic fee transaction (124)

NOTE This transaction is not compatible with Ethereum and has three Celo-specific parameters: feecurrency, gatewayfeerecipient, and gatewayfee.

Warning This transaction type is scheduled for deprecation. A deprecation warning was published in the Gingerbread hard fork on Sep 26, 2023.

  • This transaction is defined as follows:

    0x7c || RLP([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, feecurrency, gatewayfeerecipient, gatewayfee, destination, amount, data, access_list, v, r, s])
  • It was introduced on Celo during the Celo Espresso hard fork on Mar 8, 2022 as specified in CIP-42: Modification to EIP-1559.

Dynamic fee transaction v2 (123)

NOTE This transaction is not compatible with Ethereum and has one Celo-specific parameter: feecurrency.

How to Send Transactions

Import Dependencies

import {
createPublicClient,
createWalletClient,
hexToBigInt,
http,
parseEther,
parseGwei,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { celoAlfajores } from "viem/chains";
import "dotenv/config"; // use to read private key from environment variable

Create Public and Wallet Client

const PRIVATE_KEY = process.env.PRIVATE_KEY;

/**
* Boilerplate to create a viem client
*/
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`);
const publicClient = createPublicClient({
chain: celoAlfajores,
transport: http(),
});
const walletClient = createWalletClient({
chain: celoAlfajores, // Celo testnet
transport: http(),
});

Function to print Transaction receipt

function printFormattedTransactionReceipt(transactionReceipt: any) {

const {
blockHash,
blockNumber,
contractAddress,
cumulativeGasUsed,
effectiveGasPrice,
from,
gasUsed,
logs,
logsBloom,
status,
to,
transactionHash,
transactionIndex,
type,
feeCurrency,
gatewayFee,
gatewayFeeRecipient
} = transactionReceipt;

const filteredTransactionReceipt = {
type,
status,
transactionHash,
from,
to
};

console.log(`Transaction details:`, filteredTransactionReceipt, `\n`);
}

Code to send Transaction Type (0)

/**
- Transation type: 0 (0x00)
- Name: "Legacy"
- Description: Ethereum legacy transaction
*/
async function demoLegacyTransactionType() {
console.log(`Initiating legacy transaction...`);
const transactionHash = await walletClient.sendTransaction({
account, // Sender
to: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", // Recipient (illustrative address)
value: parseEther("0.01"), // 0.01 CELO
gasPrice: parseGwei("20"), // Special field for legacy transaction type
});

const transactionReceipt = await publicClient.waitForTransactionReceipt({
hash: await transactionHash,
});

printFormattedTransactionReceipt(transactionReceipt);
}

Code to send Transaction Type (2)

/**
* Transaction type: 2 (0x02)
* Name: "Dynamic fee"
* Description: Ethereum EIP-1559 transaction
*/
async function demoDynamicFeeTransactionType() {
console.log(`Initiating dynamic fee (EIP-1559) transaction...`);
const transactionHash = await walletClient.sendTransaction({
account, // Sender
to: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", // Recipient (illustrative address)
value: parseEther("0.01"), // 0.01 CELO
maxFeePerGas: parseGwei("10"), // Special field for dynamic fee transaction type (EIP-1559)
maxPriorityFeePerGas: parseGwei("10"), // Special field for dynamic fee transaction type (EIP-1559)
});

const transactionReceipt = await publicClient.waitForTransactionReceipt({
hash: await transactionHash,
});

printFormattedTransactionReceipt(transactionReceipt);
}

Code to send Transaction Type (123)

/**
* Transaction type: 123 (0x7b)
* Name: "Dynamic fee"
* Description: Celo dynamic fee transaction (with custom fee currency)
*/
async function demoFeeCurrencyTransactionType() {
console.log(`Initiating custom fee currency transaction...`);
const transactionHash = await walletClient.sendTransaction({
account, // Sender
to: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", // Recipient (illustrative address)
value: parseEther("0.01"), // 0.01 CELO
feeCurrency: "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1", // cUSD fee currency
maxFeePerGas: parseGwei("10"), // Special field for dynamic fee transaction type (EIP-1559)
maxPriorityFeePerGas: parseGwei("10"), // Special field for dynamic fee transaction type (EIP-1559)
});

const transactionReceipt = await publicClient.waitForTransactionReceipt({
hash: await transactionHash,
});

printFormattedTransactionReceipt(transactionReceipt);
}