Skip to content

Use the @eth-optimism/viem package to transfer ERC-20 tokens #1470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
be63b65
update tut
krofax Mar 6, 2025
9fc42e5
testing
krofax Mar 6, 2025
5806584
added a viem
krofax Mar 6, 2025
333c337
fix nextra
krofax Mar 6, 2025
16e768c
updated lock file
krofax Mar 6, 2025
daeec8b
fix merge conflict
krofax Mar 6, 2025
fda10d5
updated lockfile
krofax Mar 6, 2025
7b16cf7
update netlify build
krofax Mar 6, 2025
f3ddcdf
updated the lock
krofax Mar 6, 2025
455696a
updated branch
krofax Mar 6, 2025
a4cf8bd
updating tuts
krofax Mar 6, 2025
a47ce5e
update tut
krofax Mar 10, 2025
22c650c
updated codebase and contents
krofax Mar 21, 2025
3fb2bf2
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
krofax Mar 21, 2025
723c4b8
Update pages/app-developers/tutorials/bridging/cross-dom-bridge-erc20…
krofax Mar 21, 2025
3fb0ccf
updated the codebase
krofax Mar 27, 2025
b0a6f00
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
krofax Mar 27, 2025
01cf9ec
updated the codebase and import
krofax Mar 28, 2025
f14cc9b
updated the scripts and file imports
krofax Apr 3, 2025
b448e9a
updated the file imports
krofax Apr 3, 2025
aa7d0a3
updated codes, contents and file imports
krofax Apr 3, 2025
f4d741f
updated codebase
krofax Apr 3, 2025
66850f9
tiny updates
krofax Apr 3, 2025
2b25d0c
Add detailed descriptions
krofax Apr 4, 2025
dd7db17
remove weird css bg color
krofax Apr 4, 2025
eeb11c0
fixed file imports
krofax Apr 4, 2025
dfea4bb
fixed another broken file imports
krofax Apr 4, 2025
beae074
fix typo
krofax Apr 4, 2025
5072187
fix hash
krofax Apr 7, 2025
e88aace
update text
krofax Apr 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ commands:
command: pnpm setup # Run setup to configure the global bin directory
- run:
name: Install dependencies
command: npm install -g pnpm && pnpm install
command: npm install -g pnpm && pnpm install --no-frozen-lockfile
- save_cache:
key: v1-pnpm-cache-{{ checksum "pnpm-lock.yaml" }}
paths:
Expand Down
8 changes: 6 additions & 2 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[build]
command = "pnpm install --no-frozen-lockfile && pnpm build"
publish = ".next"

[build.environment]
PNPM_VERSION = "10.2.0"
NODE_VERSION = "20.11.0"
NODE_VERSION = "20.11.0"
NPM_FLAGS = "--no-frozen-lockfile"
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@eth-optimism/contracts-ts": "^0.17.0",
"@eth-optimism/tokenlist": "^9.0.9",
"@eth-optimism/viem": "^0.3.3",
"@feelback/react": "^0.3.4",
"@growthbook/growthbook-react": "^1.3.1",
"@headlessui/react": "^2.1.8",
Expand All @@ -46,7 +47,7 @@
"react-dom": "^18.2.0",
"search-insights": "^2.15.0",
"toml": "^3.0.0",
"viem": "^2.21.18"
"viem": "^2.21.37"
},
"devDependencies": {
"@double-great/remark-lint-alt-text": "^1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion pages/app-developers/transactions/estimates.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ It's important to properly estimate the cost of a transaction on OP Mainnet befo
Here you'll learn how to estimate both of the components that make up the total cost of an OP Mainnet transaction, the [execution gas fee](./fees#execution-gas-fee) and the [L1 data fee](./fees#l1-data-fee).
Make sure to read the guide on [Transaction Fees on OP Mainnet](./fees) for a detailed look at how these fees work under the hood.

## Execution gas fee
## Execution gas fee

<Callout type="info">
Estimating the execution gas fee on OP Mainnet is just like estimating the execution gas fee on Ethereum.
Expand Down
263 changes: 143 additions & 120 deletions pages/app-developers/tutorials/bridging/cross-dom-bridge-erc20.mdx

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

274 changes: 183 additions & 91 deletions public/tutorials/cross-dom-bridge-erc20.js
Original file line number Diff line number Diff line change
@@ -1,93 +1,185 @@
(async () => {

const optimism = require("@eth-optimism/sdk")
const ethers = require("ethers")

const privateKey = process.env.TUTORIAL_PRIVATE_KEY

const l1Provider = new ethers.providers.StaticJsonRpcProvider("https://rpc.ankr.com/eth_sepolia")
const l2Provider = new ethers.providers.StaticJsonRpcProvider("https://sepolia.optimism.io")
const l1Wallet = new ethers.Wallet(privateKey, l1Provider)
const l2Wallet = new ethers.Wallet(privateKey, l2Provider)

const l1Token = "0x5589BB8228C07c4e15558875fAf2B859f678d129"
const l2Token = "0xD08a2917653d4E460893203471f0000826fb4034"

const erc20ABI = [{ constant: true, inputs: [{ name: "_owner", type: "address" }], name: "balanceOf", outputs: [{ name: "balance", type: "uint256" }], type: "function" }, { inputs: [], name: "faucet", outputs: [], stateMutability: "nonpayable", type: "function" }]

const l1ERC20 = new ethers.Contract(l1Token, erc20ABI, l1Wallet)

console.log('Getting L1 tokens from faucet...')
tx = await l1ERC20.faucet()
await tx.wait()

console.log('L1 balance:')
console.log((await l1ERC20.balanceOf(l1Wallet.address)).toString())

const oneToken = 1000000000000000000n

const messenger = new optimism.CrossChainMessenger({
l1ChainId: 11155111, // 11155111 for Sepolia, 1 for Ethereum
l2ChainId: 11155420, // 11155420 for OP Sepolia, 10 for OP Mainnet
l1SignerOrProvider: l1Wallet,
l2SignerOrProvider: l2Wallet,
})

console.log('Approving L1 tokens for deposit...')
tx = await messenger.approveERC20(l1Token, l2Token, oneToken)
await tx.wait()

console.log('Depositing L1 tokens...')
tx = await messenger.depositERC20(l1Token, l2Token, oneToken)
await tx.wait()

console.log('Waiting for deposit to be relayed...')
await messenger.waitForMessageStatus(tx.hash, optimism.MessageStatus.RELAYED)

console.log('L1 balance:')
console.log((await l1ERC20.balanceOf(l1Wallet.address)).toString())

const l2ERC20 = new ethers.Contract(l2Token, erc20ABI, l2Wallet)

console.log('L2 balance:')
console.log((await l2ERC20.balanceOf(l2Wallet.address)).toString())

console.log('Withdrawing L2 tokens...')
const withdrawal = await messenger.withdrawERC20(l1Token, l2Token, oneToken)
await withdrawal.wait()

console.log('L2 balance:')
console.log((await l2ERC20.balanceOf(l2Wallet.address)).toString())

console.log('Waiting for withdrawal to be provable...')
await messenger.waitForMessageStatus(withdrawal.hash, optimism.MessageStatus.READY_TO_PROVE)

console.log('Proving withdrawal...')
await messenger.proveMessage(withdrawal.hash)

console.log('Waiting for withdrawal to be relayable...')
await messenger.waitForMessageStatus(withdrawal.hash, optimism.MessageStatus.READY_FOR_RELAY)

// Wait for the next block to be produced, only necessary for CI because messenger can return
// READY_FOR_RELAY before the RPC we're using is caught up to the latest block. Waiting for an
// additional block ensures that the RPC is caught up and the message can be relayed. Users
// should not need to do this when running the tutorial.
const maxWaitTime = Date.now() + 120000 // 2 minutes in milliseconds
const currentBlock = await l1Provider.getBlockNumber()
while (await l1Provider.getBlockNumber() < currentBlock + 1) {
if (Date.now() > maxWaitTime) {
throw new Error('Timed out waiting for block to be produced')
}
await new Promise(resolve => setTimeout(resolve, 1000))
}

console.log('Relaying withdrawal...')
await messenger.finalizeMessage(withdrawal.hash)

console.log('Waiting for withdrawal to be relayed...')
await messenger.waitForMessageStatus(withdrawal.hash, optimism.MessageStatus.RELAYED)

console.log('L1 balance:')
console.log((await l1ERC20.balanceOf(l1Wallet.address)).toString())

})()
const viem = await import('viem');
const { createPublicClient, createWalletClient, http, formatEther } = viem;
const accounts = await import('viem/accounts');
const { privateKeyToAccount } = accounts;
const viemChains = await import('viem/chains');
const { optimismSepolia, sepolia } = viemChains;
const opActions = await import('@eth-optimism/viem/actions');
const { depositERC20, withdrawOptimismERC20 } = opActions;

const l1Token = "0x5589BB8228C07c4e15558875fAf2B859f678d129";
const l2Token = "0xD08a2917653d4E460893203471f0000826fb4034";
const oneToken = 1000000000000000000n

const PRIVATE_KEY = process.env.TUTORIAL_PRIVATE_KEY || '';
const account = privateKeyToAccount(PRIVATE_KEY);

const L1_RPC_URL = 'https://rpc.ankr.com/eth_sepolia/<YOU_API_KEY>';
const L2_RPC_URL = 'https://sepolia.optimism.io';

const publicClientL1 = createPublicClient({
chain: sepolia,
transport: http(L1_RPC_URL),
});

const walletClientL1 = createWalletClient({
account,
chain: sepolia,
transport: http(L1_RPC_URL),
});

const publicClientL2 = createPublicClient({
chain: optimismSepolia,
transport: http(L2_RPC_URL),
});

const walletClientL2 = createWalletClient({
account,
chain: optimismSepolia,
transport: http(L2_RPC_URL),
});

const erc20ABI = [
{
inputs: [
{
internalType: "address",
name: "account",
type: "address",
},
],
name: "balanceOf",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "faucet",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "spender",
type: "address"
},
{
internalType: "uint256",
name: "value",
type: "uint256"
}
],
name: "approve",
outputs: [
{
internalType: "bool",
name: "",
type: "bool"
}
],
stateMutability: "nonpayable",
type: "function"
},
];

console.log('Getting tokens from faucet...');
const tx = await walletClientL1.writeContract({
address: l1Token,
abi: erc20ABI,
functionName: 'faucet',
account,
});
console.log('Faucet transaction:', tx);

// Wait for the transaction to be mined
await publicClientL1.waitForTransactionReceipt({ hash: tx });

const l1Balance = await publicClientL1.readContract({
address: l1Token,
abi: erc20ABI,
functionName: 'balanceOf',
args: [account.address]
});
console.log(`L1 Balance after receiving faucet: ${formatEther(l1Balance)}`);

console.log('Approving tokens for bridge...');
const bridgeAddress = optimismSepolia.contracts.l1StandardBridge[sepolia.id].address;
const approveTx = await walletClientL1.writeContract({
address: l1Token,
abi: erc20ABI,
functionName: 'approve',
args: [bridgeAddress, oneToken],
});
console.log('Approval transaction:', approveTx);

// Wait for approval transaction to be mined
await publicClientL1.waitForTransactionReceipt({ hash: approveTx });

console.log('Depositing tokens to L2...');
const depositTx = await depositERC20(walletClientL1, {
tokenAddress: l1Token,
remoteTokenAddress: l2Token,
amount: oneToken,
targetChain: optimismSepolia,
to: account.address,
minGasLimit: 200000,
});
console.log(`Deposit transaction hash: ${depositTx}`);

const depositReceipt = await publicClientL1.waitForTransactionReceipt({ hash: depositTx });
console.log(`Deposit confirmed in block ${depositReceipt.blockNumber}`);
console.log("Token bridging initiated! The tokens will arrive on L2 in a few minutes.");

console.log('Waiting for tokens to arrive on L2...');

await new Promise(resolve => setTimeout(resolve, 60000)); // 1 minute
const l1BalanceAfterDeposit = await publicClientL1.readContract({
address: l1Token,
abi: erc20ABI,
functionName: 'balanceOf',
args: [account.address]
});
console.log(`L1 Balance after deposit: ${formatEther(l1BalanceAfterDeposit)}`);

const l2BalanceAfterDeposit = await publicClientL2.readContract({
address: l2Token,
abi: erc20ABI,
functionName: 'balanceOf',
args: [account.address]
});
console.log(`L2 Balance after deposit: ${formatEther(l2BalanceAfterDeposit)}`);

console.log('Withdrawing tokens back to L1...');
const withdrawTx = await withdrawOptimismERC20(walletClientL2, {
tokenAddress: l2Token,
amount: oneToken / 2n,
to: account.address,
minGasLimit: 200000,
});
console.log(`Withdrawal transaction hash: ${withdrawTx}`);

const withdrawReceipt = await publicClientL2.waitForTransactionReceipt({ hash: withdrawTx });
console.log(`Withdrawal initiated in L2 block ${withdrawReceipt.blockNumber}`);
console.log("Withdrawal process initiated! It will take 7 days for the tokens to be available on L1.");
Comment on lines +164 to +175
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing withdrawal completion steps.

The code initiates a withdrawal from L2 to L1 but doesn't include the necessary "prove" and "finalize" steps required to complete the withdrawal after the challenge period. This makes the tutorial incomplete as users won't be able to access their tokens on L1.

I can provide implementation for the complete withdrawal process using proveWithdrawalTransaction and finalizeWithdrawalTransaction actions from the @eth-optimism/viem package. Would you like me to draft this code?

🧰 Tools
🪛 ESLint

[error] 164-164: Extra semicolon.

(semi)


[error] 170-170: Extra semicolon.

(semi)


[error] 171-171: Extra semicolon.

(semi)


[error] 173-173: Extra semicolon.

(semi)


[error] 174-174: Extra semicolon.

(semi)


[error] 175-175: Extra semicolon.

(semi)


const l2Balance = await publicClientL2.readContract({
address: l2Token,
abi: erc20ABI,
functionName: 'balanceOf',
args: [account.address]
});
console.log(`L2 Balance after withdrawal: ${formatEther(l2Balance)}`);

})();
6 changes: 6 additions & 0 deletions styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,10 @@ div.footer-columns {
align-items: center;
gap: 2rem;
}
}

code .line .highlighted {
background-color: transparent !important;
--tw-shadow-color: transparent !important;
box-shadow: none !important;
}