Builders
Developer Tools
SDK Javascript
Trace Cross Domain Transactions

Trace Cross Domain Transactions

This tutorial teaches you how to trace individual cross-domain transactions between L1 Ethereum and OP Mainnet using the Optimism SDK (opens in a new tab). To see how to send these messages, see the cross domain tutorial or the tutorials on how to transfer ETH and ERC-20.

The SDK supports multiple OP Chains: OP, Base, etc. To see whether a specific OP Chain is supported directly, see the SDK documentation (opens in a new tab). Chains that aren't officially supported just take a few extra steps. Get the L1 contract addresses, and provide them to the SDK (opens in a new tab). Once you do that, you can use the SDK normally.

Before You Begin

Make a Project Folder and Initialize It

mkdir project-folder-name
cd folder-name
pnpm init

Create Two Files for the Folder

  • Create a .env file
.env file
L1URL=<<< L1 URL goes here >>>
L2URL=<<< L2 URL goes here >>>
  • Create a hardhat.config.js file
hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require('dotenv').config()
 
// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();
 
  for (const account of accounts) {
    console.log(account.address);
  }
});
 
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
 
/**
 * @type import('hardhat/config').HardhatUserConfig
 */
 
 
module.exports = {
  solidity: "0.8.13",
  networks: {
    "l1": {
       url: process.env.L1URL,
    },
    "l2": {
       url: process.env.L1URL,
    }
  }
};

Edit '.env' file to Specify the URLs for L1 and L2

For the transactions in this tutorial, we will use Goerli. However, you can use the same code to trace mainnet transactions.

Install the dependencies

pnpm add @eth-optimism/sdk@^3.0.0 dotenv@^16.0.0 ethers@^5.6.1 ethereum-waffle@^3.2.0 @nomiclabs/hardhat-ethers@^2.0.0 @nomiclabs/hardhat-waffle@^2.0.3 hardhat@^2.12.4

Create a CrossDomainMessenger (opens in a new tab)

optimismSDK = require("@eth-optimism/sdk")
l1Provider = new ethers.providers.JsonRpcProvider(process.env.L1URL)
l2Provider = new ethers.providers.JsonRpcProvider(process.env.L2URL)
l1ChainId = (await l1Provider._networkPromise).chainId
l2ChainId = (await l2Provider._networkPromise).chainId  
crossChainMessenger = new optimismSDK.CrossChainMessenger({
  l1ChainId: l1ChainId,    
  l2ChainId: l2ChainId,          
  l1SignerOrProvider: l1Provider,
  l2SignerOrProvider: l2Provider,
})

Tracing a deposit

We are going to trace this deposit (opens in a new tab).

Get the message status

l1TxHash = "0x80da95d06cfe8504b11295c8b3926709ccd6614b23863cdad721acd5f53c9052"
await crossChainMessenger.getMessageStatus(l1TxHash)

The list of message statuses and their meaning is in the SDK documentation (opens in a new tab). 6 means the message was relayed successfully.

Get the message receipt

l2Rcpt = await crossChainMessenger.getMessageReceipt(l1TxHash)

In addition to l2Rcpt.transactionReceipt, which contains the standard transaction receipt, you get l2Rcpt.receiptStatus with the transaction status. 1 means successful relay (opens in a new tab).

Get the hash of the L2 transaction (l2Rcpt.transactionReceipt.transactionHash)

l2TxHash = l2Rcpt.transactionReceipt.transactionHash

You can view this transaction on Etherscan (opens in a new tab).

Check for Transferred Assets

To see if actual assets were transferred, you can parse the event log. In OP Mainnet terminology, deposit refers to any transaction going from L1 Ethereum to OP Mainnet, and withdrawal refers to any transaction going from OP Mainnet to L1 Ethereum, whether or not there are assets attached.

The event names and their parameters are usually available on Etherscan, but you can't just copy and paste, you need to make a few changes:

  • Add event before each event.
  • Change the index_topic_<n> strings to indexed, and put them after the type rather than before.
abi = [
  "event Transfer (address indexed from, address indexed to, uint256 value)",
  "event Mint (address indexed _account, uint256 _amount)",
  "event DepositFinalized (address indexed _l1Token, address indexed _l2Token, address indexed  _from, address _to, uint256 _amount, bytes _data)",
  "event RelayedMessage (bytes32 indexed msgHash)"
]
iface = new ethers.utils.Interface(abi)
logEvents = l2Rcpt.transactionReceipt.logs.map(x => {
   try {
   res = iface.parseLog(x)
   res.address = x.address
   return res
   } catch (e) {}
}).filter(e => e != undefined)

The try .. catch syntax is necessary because not all the log entries can be parsed by iface.

Locate Mint Events

When an asset is deposited, it is actually locked in the bridge on L1 and an equivalent asset is minted on L2. To see transferred assets, look for Mint events.

mints = logEvents.filter(x => x.name == 'Mint')
for(i = 0; i<mints.length; i++)
  console.log(`Asset: ${mints[i].address}, amount ${mints[0].args._amount / 1e18}`)

Tracing a withdrawal

We are going to trace this withdrawal (opens in a new tab).

Get the message status

l2TxHash = "0x548f9eed01498e1b015aaf2f4b8c538f59a2ad9f450aa389bb0bde9b39f31053"
await crossChainMessenger.getMessageStatus(l2TxHash)

The list of message statuses and their meaning is in the SDK documentation (opens in a new tab). 6 means the message was relayed successfully.

Get the message receipt

l1Rcpt = await crossChainMessenger.getMessageReceipt(l2TxHash)

In addition to l1Rcpt.transactionReceipt, which contains the standard transaction receipt, you get l1Rcpt.receiptStatus with the transaction status. 1 means successful relay (opens in a new tab).

Get the hash of the L1 transaction (l1Rcpt.transactionReceipt.transactionHash)

l1TxHash = l1Rcpt.transactionReceipt.transactionHash

You can view this transaction on Etherscan (opens in a new tab).

Check for Transferred Assets

In OP Mainnet terminology, deposit refers to any transaction going from L1 Ethereum to OP Mainnet, and withdrawal refers to any transaction going from OP Mainnet to L1 Ethereum, whether or not there are assets attached. To see if actual assets were transferred, you can parse the event log. This is how you parse the event log of the L2 transaction.

The event names and their parameters are usually available on Etherscan, but you can't just copy and paste, you need to make a few changes:

  • Add event before each event.
  • Change the index_topic_<n> strings to indexed, and put them after the type rather than before.
abi = [
  "event Transfer (address indexed from, address indexed to, uint256 value)",
  "event Burn (address indexed _account, uint256 _amount)",
  "event SentMessage (address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit)",
  "event WithdrawalInitiated (address indexed _l1Token, address indexed _l2Token, address indexed _from, address _to, uint256 _amount, bytes _data)"
]
iface = new ethers.utils.Interface(abi)
l2Rcpt = await l2Provider.getTransactionReceipt(l2TxHash)
events = l2Rcpt.logs.map(x => {
  res = iface.parseLog(x)
  res.address = x.address
  return res
})
logEvents = l2Rcpt.logs.map(x => {
   try {
   res = iface.parseLog(x)
   res.address = x.address
   return res
   } catch (e) {}
}).filter(e => e != undefined)

The try .. catch syntax is necessary because not all the log entries can be parsed by iface.

Locate Burn Events

When an asset is withdrawn, it is burned on L2, and then the bridge on L1 releases the equivalent asset. To see transferred assets, look for Burn events.

burns = logEvents.filter(x => x.name == 'Burn')
for(i = 0; i<burns.length; i++)
  console.log(`Asset: ${burns[i].address}, amount ${burns[0].args._amount / 1e18}`)