> For the complete documentation index, see [llms.txt](https://docs.giwa.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.giwa.io/giwa-chain/en/get-started/bridging/eth.md).

# Bridging ETH

### Requirements

Make sure the following are installed.

* [node](https://nodejs.org/ko)
* [pnpm](https://pnpm.io/)

### Set up development environment

In this tutorial, we’ll use viem. [Viem](https://viem.sh/) is a Node.js library, so we’ll start by creating a Node.js project.

{% stepper %}
{% step %}

#### Create a project folder

```bash
mkdir giwa-bridging-eth
cd giwa-bridging-eth
```

{% endstep %}

{% step %}

#### Initialize the project

```bash
pnpm init
```

{% endstep %}

{% step %}

#### Install dependencies

```bash
pnpm add -D tsx @types/node
pnpm add viem@^2.38.0
```

{% endstep %}
{% endstepper %}

### Prepare a wallet

You’ll need a wallet to bridge ETH.

{% stepper %}
{% step %}

#### Get Sepolia ETH

For a deposit (Ethereum -> GIWA), you need ETH on the Ethereum Sepolia network. [Use this faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) to get Sepolia ETH.

{% hint style="info" %}
Don’t have a wallet yet? Use [cast](https://getfoundry.sh/introduction/installation/) command to create one.
{% endhint %}
{% endstep %}

{% step %}

#### Set the Private Key environment variable

This tutorial requires multiple transaction signatures. To handle this, you’ll need to set your wallet private key as an environment variable.

```bash
export TEST_PRIVATE_KEY=0x...
```

{% endstep %}
{% endstepper %}

### Configure the Chain Client

Set up chain client for ETH bridging.

{% code title="src/config.ts" lineNumbers="true" %}

```typescript
import {defineChain, createPublicClient, http, createWalletClient} from "viem";
import {privateKeyToAccount} from "viem/accounts";
import {publicActionsL1, publicActionsL2, walletActionsL1, walletActionsL2} from "viem/op-stack";
import {sepolia, giwaSepolia} from "viem/chains";

// 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());

```

{% endcode %}

### 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](/giwa-chain/en/network-information/contracts.md#l1-contracts) contract. The same amount is then minted and credited to your GIWA wallet. This process is known as **Lock-and-Mint**.

{% stepper %}
{% step %}

#### Flow

1. Send a deposit transaction on Layer 1
2. The corresponding Layer 2 deposit transaction is created by the sequencer
3. Deposit complete
   {% endstep %}

{% step %}

#### Writing the code

{% code title="src/deposit\_eth.ts" lineNumbers="true" %}

```typescript
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();

```

{% endcode %}
{% endstep %}

{% step %}

#### Running

```bash
node --import=tsx src/deposit_eth.ts
```

{% endstep %}
{% endstepper %}

{% hint style="info" %}
Why does it take a few minutes for an L1 deposit to appear in my L2 wallet balance?

When you send an L1 deposit transaction, the L2 sequencer picks it up and creates a corresponding L2 deposit transaction. Because the L1 chain can experience reorgs, the L2 sequencer waits for the L1 deposit transaction to be finalized by a certain number of blocks (N blocks) before processing it. This ensures system stability and prevents inconsistencies.
{% endhint %}

### 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](/giwa-chain/en/network-information/contracts.md#l2-contracts) contract. The same amount is then released to your Ethereum wallet. In this case, the ETH that was originally locked in the [OptimismPortal](/giwa-chain/en/network-information/contracts.md#l1-contracts) contract during the deposit gets unlocked. This process is called **Burn-and-Unlock**.

{% hint style="info" %}
The ETH sent to the L2ToL1MessagePasser contract is effectively considered burned. While the balance still appears when queried, the contract includes a burn() function that can be called at any time to destroy the entire balance.
{% endhint %}

{% stepper %}
{% step %}

#### Flow

1. Send a withdrawal initiation transaction on Layer 2
2. Send a withdrawal prove transaction on Layer 1
3. Send a withdrawal finalize transaction on Layer 1
4. Withdrawal complete
   {% endstep %}

{% step %}

#### Writing the code

{% code title="src/withdraw\_eth.ts" lineNumbers="true" %}

```typescript
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();

```

{% endcode %}
{% endstep %}

{% step %}

#### Running

```bash
node --import=tsx src/withdraw_eth.ts
```

{% endstep %}
{% endstepper %}

{% hint style="warning" %}
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.
  {% endhint %}

## Learn more

Check out the [viem docs](https://viem.sh/op-stack) for additional resources.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.giwa.io/giwa-chain/en/get-started/bridging/eth.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
