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
- Install Node.js (opens in a new tab), pnpm (opens in a new tab), and yarn (opens in a new tab).
- Access to L1 (Ethereum mainnet) and L2 (OP Mainnet) providers.
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
L1URL=<<< L1 URL goes here >>>
L2URL=<<< L2 URL goes here >>>
- Create a
hardhat.config.js
file
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 toindexed
, 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 toindexed
, 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}`)