Bridging ETH
Bridge ETH from Ethereum to GIWA, and from GIWA to Ethereum.
Requirements
Make sure the following are installed.
Set up development environment
In this tutorial, we’ll use viem. Viem is a Node.js library, so we’ll start by creating a Node.js project.
Prepare a wallet
You’ll need a wallet to bridge ETH.
Get Sepolia ETH
For a deposit (Ethereum -> GIWA), you need ETH on the Ethereum Sepolia network. Use this faucet to get Sepolia ETH.
Configure the Chain Client
Set up chain client for ETH bridging.
import {defineChain, createPublicClient, http, createWalletClient} from "viem";
import {privateKeyToAccount} from "viem/accounts";
import {publicActionsL1, publicActionsL2, walletActionsL1, walletActionsL2} from "viem/op-stack";
import {sepolia} from "viem/chains";
// GIWA Sepolia chain config
export const giwaSepolia = defineChain({
id: 91342,
name: 'Giwa Sepolia',
nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: {
default: {
http: ['https://sepolia-rpc.giwa.io'],
},
},
contracts: {
multicall3: {
address: '0xcA11bde05977b3631167028862bE2a173976CA11',
},
l2OutputOracle: {},
disputeGameFactory: {
[sepolia.id]: {
address: '0x37347caB2afaa49B776372279143D71ad1f354F6',
},
},
portal: {
[sepolia.id]: {
address: '0x956962C34687A954e611A83619ABaA37Ce6bC78A',
},
},
l1StandardBridge: {
[sepolia.id]: {
address: '0x77b2ffc0F57598cAe1DB76cb398059cF5d10A7E7',
},
},
},
testnet: true,
});
// Prepare the wallet
export const PRIVATE_KEY = process.env.TEST_PRIVATE_KEY as `0x${string}`;
export const account = privateKeyToAccount(PRIVATE_KEY);
// Client for reading Ethereum Sepolia chain data
export const publicClientL1 = createPublicClient({
chain: sepolia,
transport: http(),
}).extend(publicActionsL1())
// Client for sending transactions on Ethereum Sepolia
export const walletClientL1 = createWalletClient({
account,
chain: sepolia,
transport: http(),
}).extend(walletActionsL1());
// Client for reading GIWA Sepolia chain data
export const publicClientL2 = createPublicClient({
chain: giwaSepolia,
transport: http(),
}).extend(publicActionsL2());
// Client for sending transactions on GIWA Sepolia
export const walletClientL2 = createWalletClient({
account,
chain: giwaSepolia,
transport: http(),
}).extend(walletActionsL2());
Deposit ETH (Ethereum -> GIWA)
Let’s bridge ETH from Ethereum to GIWA.
When you run the code below, your ETH on Ethereum is actually sent to the OptimismPortal contract. The same amount is then minted and credited to your GIWA wallet. This process is known as Lock-and-Mint.
Writing the code
import {publicClientL1, publicClientL2, account, walletClientL1} from './config';
import {formatEther, parseEther} from "viem";
import {getL2TransactionHashes} from "viem/op-stack";
async function main() {
// Before bridging to GIWA, check your ETH balance on your Layer 1 wallet.
const l1Balance = await publicClientL1.getBalance({ address: account.address });
console.log(`L1 Balance: ${formatEther(l1Balance)} ETH`);
// Build the params to send a deposit transaction on Layer 1.
const depositArgs = await publicClientL2.buildDepositTransaction({
mint: parseEther("0.001"),
to: account.address,
});
// Send the deposit transaction on Layer 1.
// In this step, your ETH is sent to the OptimismPortal contract.
const depositHash = await walletClientL1.depositTransaction(depositArgs);
console.log(`Deposit transaction hash on L1: ${depositHash}`);
// Wait until the Layer 1 transaction above is fully processed.
const depositReceipt = await publicClientL1.waitForTransactionReceipt({ hash: depositHash });
console.log('L1 transaction confirmed:', depositReceipt);
// From the Layer 1 receipt, pre-compute the hash of the corresponding Layer 2 deposit transaction.
const [l2Hash] = getL2TransactionHashes(depositReceipt);
console.log(`Corresponding L2 transaction hash: ${l2Hash}`);
// Wait until the computed Layer 2 deposit transaction is processed.
// This usually takes about 1–3 minutes.
const l2Receipt = await publicClientL2.waitForTransactionReceipt({
hash: l2Hash,
});
console.log('L2 transaction confirmed:', l2Receipt);
console.log('Deposit completed successfully!');
}
main().then();
Withdraw ETH (GIWA -> Ethereum)
Have you successfully sent ETH from Ethereum to GIWA? Now let’s bridge it back in the opposite direction — from GIWA to Ethereum.
When you run the code below, your ETH on GIWA is actually sent to the L2ToL1MessagePasser contract. The same amount is then released to your Ethereum wallet. In this case, the ETH that was originally locked in the OptimismPortal contract during the deposit gets unlocked. This process is called Burn-and-Unlock.
Writing the code
import {publicClientL1, publicClientL2, account, walletClientL1, walletClientL2} from './config';
import {formatEther, parseEther} from "viem";
async function main() {
// Before bridging back to Ethereum, check your ETH balance on your Layer 2 wallet.
const l2Balance = await publicClientL2.getBalance({ address: account.address });
console.log(`L2 Balance: ${formatEther(l2Balance)} ETH`);
// Build the params to initiate a withdrawal on Layer 2.
const withdrawalArgs = await publicClientL1.buildInitiateWithdrawal({
to: account.address,
value: parseEther("0.00005"),
});
// Initiate the withdrawal on Layer 2.
// In this step, your ETH is sent to the L2ToL1MessagePasser contract.
const withdrawalHash = await walletClientL2.initiateWithdrawal(withdrawalArgs);
console.log(`Withdrawal transaction hash on L2: ${withdrawalHash}`);
// Wait until the Layer 2 transaction above is fully processed.
const withdrawalReceipt = await publicClientL2.waitForTransactionReceipt({ hash: withdrawalHash });
console.log('L2 transaction confirmed:', withdrawalReceipt);
// Wait until the L2 withdrawal transaction can be proven on L1.
// This can take up to 2 hours.
const { output, withdrawal } = await publicClientL1.waitToProve({
receipt: withdrawalReceipt,
targetChain: walletClientL2.chain
});
// Build the params to prove the withdrawal on Layer 1.
const proveArgs = await publicClientL2.buildProveWithdrawal({
output,
withdrawal,
});
// Prove the withdrawal on Layer 1.
const proveHash = await walletClientL1.proveWithdrawal(proveArgs);
console.log(`Prove transaction hash on L1: ${proveHash}`);
// Wait until the Layer 1 transaction above is fully processed.
const proveReceipt = await publicClientL1.waitForTransactionReceipt({ hash: proveHash });
console.log('Prove transaction confirmed:', proveReceipt);
// Wait until the withdrawal can be finalized.
// This period is called the challenge period and takes about 7 days.
await publicClientL1.waitToFinalize({
targetChain: walletClientL2.chain,
withdrawalHash: withdrawal.withdrawalHash,
});
// Finalize the withdrawal on Layer 1.
const finalizeHash = await walletClientL1.finalizeWithdrawal({
targetChain: walletClientL2.chain,
withdrawal,
});
console.log(`Finalize transaction hash on L1: ${finalizeHash}`);
// Wait until the Layer 1 transaction above is fully processed.
const finalizeReceipt = await publicClientL1.waitForTransactionReceipt({
hash: finalizeHash
});
console.log('Finalize transaction confirmed:', finalizeReceipt);
// Read the withdrawal status on Layer 1.
// Because withdrawals take a long time to complete, you can query status with this if needed.
const status = await publicClientL1.getWithdrawalStatus({
receipt: withdrawalReceipt,
targetChain: walletClientL2.chain,
});
console.log('Withdrawal completed successfully!');
}
main().then();
Why does a withdrawal take so long?
Prove wait time: To prove a withdrawal transaction from L2 on L1, a dispute game must be initiated on L1 that includes the L2 transaction. On GIWA, dispute games are started at most every 2 hours, so it may take up to 2 hours before the withdrawal can be proven.
Challenge Period: GIWA is an L2 built on the OP Stack and adopts the Optimistic Rollup model. To confirm that the L2 state at a given point is correct, a dispute game must be completed. This challenge period takes about 7 days, so a withdrawal cannot be finalized until approximately 7 days have passed.
Learn more
Check out the viem docs for additional resources.
Last updated