-
Notifications
You must be signed in to change notification settings - Fork 266
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
Changes from 28 commits
be63b65
9fc42e5
5806584
333c337
16e768c
daeec8b
fda10d5
7b16cf7
f3ddcdf
455696a
a4cf8bd
a47ce5e
22c650c
3fb2bf2
723c4b8
3fb0ccf
b0a6f00
01cf9ec
f14cc9b
b448e9a
aa7d0a3
f4d741f
66850f9
2b25d0c
dd7db17
eeb11c0
dfea4bb
beae074
5072187
e88aace
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" |
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 🧰 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)}`); | ||
|
||
})(); |
Uh oh!
There was an error while loading. Please reload this page.