Viewing Transactions between Layers

Look up transactions passed between Mantle and Ethereum

Mantle v2 Tectonic has been released, please move to the new documentation!

In this tutorial, we'll be going over how to use the Mantle SDK to view transactions passed between Mantle network (L2) and Ethereum (L1). This function is particularly useful for dApps that make contract calls between L2 and L1.

Set up local environment

Before moving ahead, please make sure you have Node.js, Yarn, and Git installed and configured in your environment. We'll need to use them to download and run our example script.

Let's download the JS script that we'll be using to make SDK calls. The easiest way is to download the files from the Mantle GitHub repo using a Git command like so:

git clone

We need to configure the wallet that we're going to be using to send transactions. In the main directory, you'll see a .env.testnet file. You can specify your wallet private key in the PRIV_KEY field, and an L1_RPC URL for Goerli network.

Now, navigate to the ./sdk-view-tx directory and run the yarn command to download the necessary dependencies to your local environment, such as the ethers.js library, the SDK modules, and more. They can all be found in the node_modules directory once you successfully run Yarn.

Let's go over the files in the ./sdk-view-tx directory and see what purpose each of them serves.

  • index.js: the main JS script that sends requests to node RPCs on L1 and L2 to query on-chain data

  • package.json: specifies dependencies and commands for script automation

  • yarn.lock: specifies dependencies

Script Logic

We can now start looking at the code in index.js.

#! /usr/local/bin/node

const ethers = require("ethers")
const mantleSDK = require("@mantleio/sdk")

// Global variable because we need them almost everywhere
let crossChainMessenger

const setup = async () => {
  crossChainMessenger = new mantleSDK.CrossChainMessenger({
    l1ChainId: process.env.L1_CHAINID,
    l2ChainId: process.env.L2_CHAINID,
    l1SignerOrProvider: l1Wallet,
    l2SignerOrProvider: l2Wallet

We first create a crossChainMessenger object that we'll use to fetch and view transaction information. This operation is limited to fetching public on-chain data using view functions, so we don't need signers to send these requests. However, we do need chain ID values to direct requests to the correct node RPC.

// Only the part of the ABI we need to get the symbol
const ERC20ABI = [
    "constant": true,
    "inputs": [],
    "name": "symbol",
    "outputs": [
            "name": "",
            "type": "string"
    "payable": false,
    "stateMutability": "view",
    "type": "function"

const getSymbol = async l1Addr => {
  if (l1Addr == '0x0000000000000000000000000000000000000000')
    return "ETH"
  if (l1Addr == '0xc1dC2d65A2243c22344E725677A3E3BEBD26E604')
    return "MNT"

If the l1Addr field contains all zeroes, this implies the transferred token was $ETH. We also check the l1Addr for $MNT token's contract address.

 const l1Contract = new ethers.Contract(l1Addr, ERC20ABI, crossChainMessenger.l1SignerOrProvider)
  return await l1Contract.symbol()

Otherwise, we fetch the token symbol from the contract. The same query logic can be used for both L1 and L2 contracts.

// Describe a cross domain transaction, either deposit or withdrawal

const describeTx = async tx => {
  // Assume all tokens have decimals = 18
  console.log(`\tAmount: ${tx.amount / 1e18} ${await getSymbol(tx.l1Token)}`)
  console.log(`\tRelayed: ${await crossChainMessenger.getMessageStatus(tx.transactionHash)
    == mantleSDK.MessageStatus.RELAYED}`)

The response of crossDomainMessenger.getMessageStatus()is a MessageStatus enumerated value. What we're checking for is whether the deposit/withdrawal transaction is completed or still in progress.

const main = async () => {    
    await setup()
    const deposits = await crossChainMessenger.getDepositsByAddress(l1Wallet.address)
        console.log(`Deposits by address ${addr}`)
    for (var i=0; i<deposits.length; i++)
      await describeTx(deposits[i])

    const withdrawals = await crossChainMessenger.getWithdrawalsByAddress(l1Wallet.address)
        console.log(`\n\n\nWithdrawals by address ${addr}`)
    for (var i=0; i<withdrawals.length; i++)
      await describeTx(withdrawals[i])

The crossChainMessenger.getDepositsByAddress() function call returns records of all deposit transactions made by an address, and crossChainMessenger.getWithdrawalsByAddress() returns records of all withdrawal transactions sent by an address. Finally, we print the deposit and withdraw records to the console.

Running the Script

With L1 and L2 RPC endpoints and the wallet private key configured, you can go ahead and run the index.js script by simply running the yarn testnet command. If you have a local L2 or L1 instance running in your environment, you can switch up the respective RPC URLs in the .env.local configuration file in the main directory and use the yarn local command to run the script locally.


The output would look something like:

Deposits by address 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
        Amount: 1 L1EPT
        Relayed: true

Withdrawals by address 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
        Amount: 1 L1EPT
        Relayed: false


At this point, you should be able to look up deposits and withdrawals performed by any specific address. There are some additional tracing functions in CrossChainMessenger, but they are very similar in terms of operation.

Last updated