> ## Documentation Index
> Fetch the complete documentation index at: https://docs.optimism.io/llms.txt
> Use this file to discover all available pages before exploring further.

# L1에서 트랜잭션 제출하기

> Viem으로 입금(디파짓) 트랜잭션과 출금을 처리하는 방법을 알아봅니다.

> [!NOTE]
>
> **OP Stack 학습하기 — 14단계 중 11단계.**
> 입금(디파짓)과 출금이 어떻게 동작하는지 읽어 보았습니다. 이 프로젝트에서는 Viem을 사용하여
> 코드로 양방향 흐름을 처음부터 끝까지 진행합니다. 완료한 후에는
> [결함 증명(fault proof) 설명](https://docs.optimism.io/ko/op-stack/fault-proofs/explainer.md)으로
> 계속 진행하십시오.

이 튜토리얼에서는 [Viem](https://viem.sh/op-stack)을 사용하여 크로스 도메인 트랜잭션을 처리하는 방법을 설명합니다.

* [입금 트랜잭션(deposited transaction)](https://specs.optimism.io/protocol/deposits.html): 입금(deposit)이라고도 하며, L1에서 시작되어 L2에서 실행되는 트랜잭션입니다. L1에서 임의의 L2 트랜잭션을 제출하는 데 사용할 수 있습니다.

* [출금(withdrawal)](https://specs.optimism.io/protocol/withdrawals.html): L2에서 시작되어 L1에서 실행되는 트랜잭션으로 최종 확정되는 크로스 도메인 트랜잭션입니다. `OptimismPortal`을 통해 L2에서 L1으로 임의의 메시지를 보내는 데 사용할 수 있습니다.

입금 트랜잭션과 출금 모두 ETH와 데이터를 전송할 수 있습니다.

## 지원되는 네트워크

Viem은 모든 [OP Stack 네트워크](https://viem.sh/op-stack/chains)를 지원합니다.
OP Stack 네트워크는 기본적으로 Viem에 포함되어 있습니다.
기본적으로 포함되지 않은 네트워크를 사용하려면 Viem의 체인 [구성](https://viem.sh/op-stack/chains#configuration)에 추가할 수 있습니다.

## 의존성

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

## 데모 프로젝트 생성

이 튜토리얼에서는 Viem 라이브러리를 사용합니다.
Viem는 [Node.js](https://nodejs.org/en/) 라이브러리이므로, 이를 사용하려면 Node.js 프로젝트를 생성해야 합니다.

**1. 프로젝트 폴더 만들기**

```bash theme={null}
mkdir bridge-eth
cd bridge-eth
```

**2. 프로젝트 초기화**

```bash theme={null}
pnpm init
```

**3. 의존성 설치**

```bash theme={null}
pnpm add viem@^2.51.0
```

## Sepolia에서 ETH 받기

이 튜토리얼에서는 Sepolia에서 OP Sepolia로 ETH를 브리징하는 방법을 설명합니다.
따라 하려면 Sepolia에서 약간의 ETH가 필요합니다.

> [!NOTE]
>
> [이 faucet](https://sepoliafaucet.com)을 사용하여 Sepolia에서
> ETH를 받을 수 있습니다.

## 환경에 개인 키 추가

트랜잭션에 서명하려면 개인 키가 필요합니다.
`export` 명령으로 개인 키를 환경 변수로 설정하십시오.
이 개인 키가 Sepolia에서 ETH를 보유한 주소에 해당하는지 확인하십시오.

> [!NOTE]
>
> 이 튜토리얼을 위해 새 지갑을 만들고 싶으신가요?
> [`cast`](https://book.getfoundry.sh/getting-started/installation)가 설치되어 있다면 터미널에서 `cast wallet new`를 실행하여 새 지갑을 만들고 개인 키를 얻을 수 있습니다.

```bash theme={null}
export TUTORIAL_PRIVATE_KEY=0x...
```

## Node REPL 시작

Node REPL을 사용하여 Viem와 상호작용합니다.
Node REPL을 시작하려면 터미널에서 다음 명령을 실행하십시오.

```bash theme={null}
node
```

그러면 JavaScript 코드를 실행할 수 있는 Node REPL 프롬프트가 열립니다.

## 의존성 가져오기

Node REPL 세션에 몇 가지 의존성을 가져와야 합니다.

**1. Viem 및 기타 패키지 가져오기**

```js theme={null}
const { createPublicClient, http, createWalletClient, parseEther, formatEther } = require('viem');
const { sepolia, optimismSepolia } = require('viem/chains');
const { privateKeyToAccount } = require('viem/accounts');
const { getL2TransactionHashes, publicActionsL1, publicActionsL2, walletActionsL1, walletActionsL2, isSuperGameType } = require('viem/op-stack');
```

**2. 개인 키 불러오기 및 계정 설정**

```js theme={null}
const PRIVATE_KEY = process.env.TUTORIAL_PRIVATE_KEY;
const account = privateKeyToAccount(PRIVATE_KEY);
```

**3. Sepolia 네트워크에서 데이터를 읽기 위한 L1 public client 생성**

```js theme={null}
const publicClientL1 = createPublicClient({
    chain: sepolia,
    transport: http("https://ethereum-sepolia-rpc.publicnode.com"),
}).extend(publicActionsL1()) 
```

**4. Sepolia에서 트랜잭션을 전송하기 위한 L1 wallet client 생성**

```js theme={null}
const walletClientL1 = createWalletClient({
    account,
    chain: sepolia,
    transport: http("https://ethereum-sepolia-rpc.publicnode.com"),
}).extend(walletActionsL1());
```

**5. OP Sepolia와 상호작용하기 위한 L2 public client 생성**

```js theme={null}
const publicClientL2 = createPublicClient({
    chain: optimismSepolia,
    transport: http("https://sepolia.optimism.io"),
}).extend(publicActionsL2());
```

**6. OP Sepolia에서 L2 wallet client 생성**

```js theme={null}
const walletClientL2 = createWalletClient({
    account,
    chain: optimismSepolia,
    transport: http("https://sepolia.optimism.io"),
}).extend(walletActionsL2());
```

## Sepolia에서 ETH 받기

L2로 브리징할 수 있는 L1의 ETH가 필요합니다.
[이 faucet](https://sepoliafaucet.com)에서 Sepolia ETH를 받을 수 있습니다.

## ETH 입금하기

이제 L1에 ETH가 있으므로, [ETH 브리징](https://docs.optimism.io/ko/app-developers/guides/bridging/standard-bridge.md#bridging-eth)에 설명된 방법 외에도 아래 예제에 나온 방식으로 ETH를 입금할 수 있습니다.

컨트랙트 계정을 사용하는 경우 [주소 별칭(Address Aliasing)](https://specs.optimism.io/protocol/deposits.html#address-aliasing)에 주의해야 합니다.

> [tab] depositETH
>
> **1. L1에서 지갑 잔액 확인**
>
> 나중에 입금이 정상적으로 처리되었는지 확인할 수 있도록 L1에 보유한 ETH 수량을 확인합니다.
>
> ```js theme={null}
> const l1Balance = await publicClientL1.getBalance({ address: account.address });
> console.log(`L1 Balance: ${formatEther(l1Balance)} ETH`); 
> ```
>
> > [!NOTE]
> >
> > `viem`의 `formatEther` 메서드를 사용하여 잔액을 ether 단위로 변환했습니다.
>
> **2. 입금 트랜잭션 생성**
>
> [buildDepositTransaction](https://viem.sh/op-stack/actions/buildDepositTransaction)을 사용하여 L2에서의 입금 트랜잭션 매개변수를 구성합니다.
>
> 선택적 매개변수 `mint`와 `value`의 의미를 반드시 이해하십시오. 원한다면 다른 사람의 주소를 `to` 값으로 사용할 수도 있습니다.
>
> ```js theme={null}
> const depositArgs = await publicClientL2.buildDepositTransaction({
> mint: parseEther("0.01"),
> to: account.address,
> });
> ```
>
> **3. 입금 트랜잭션 전송**
>
> L1에서 입금 트랜잭션을 전송하고 L1 트랜잭션 해시를 로그로 출력합니다.
>
> ```js theme={null}
> const depositHash = await walletClientL1.depositTransaction(depositArgs);
> console.log(`Deposit transaction hash on L1: ${depositHash}`);
> ```
>
> **4. L1 트랜잭션 대기**
>
> L1 트랜잭션이 처리될 때까지 기다린 후 영수증을 로그로 출력합니다.
>
> ```js theme={null}
> const depositReceipt = await publicClientL1.waitForTransactionReceipt({ hash: depositHash });
> console.log('L1 transaction confirmed:', depositReceipt);
> ```
>
> **5. L2 트랜잭션 해시 추출**
>
> L1 영수증에서 대응하는 L2 트랜잭션 해시를 추출하여 로그로 출력합니다.
> 이 해시는 L2에서의 입금 트랜잭션을 나타냅니다.
>
> ```js theme={null}
> const [l2Hash] = getL2TransactionHashes(depositReceipt);
> console.log(`Corresponding L2 transaction hash: ${l2Hash}`);
> ```
>
> **6. L2 트랜잭션 처리 대기**
>
> L2 트랜잭션이 처리되고 확인될 때까지 기다린 후, 완료를 검증하기 위해 L2 영수증을 로그로 출력합니다.
>
> ```js theme={null}
> const l2Receipt = await publicClientL2.waitForTransactionReceipt({
> hash: l2Hash,
> }); 
> console.log('L2 transaction confirmed:', l2Receipt);
> console.log('Deposit completed successfully!');
> ```

> [tab] 전체 코드
>
> ```js theme={null}
> const { createPublicClient, http, createWalletClient, parseEther, formatEther } = require('viem');
> const { sepolia, optimismSepolia } = require('viem/chains');
> const { privateKeyToAccount } = require('viem/accounts');
> const { getL2TransactionHashes, publicActionsL1, publicActionsL2, walletActionsL1, walletActionsL2, isSuperGameType } = require('viem/op-stack');
>
> const PRIVATE_KEY = process.env.TUTORIAL_PRIVATE_KEY;
> const account = privateKeyToAccount(PRIVATE_KEY);
>
> const publicClientL1 = createPublicClient({
> chain: sepolia,
> transport: http("https://ethereum-sepolia-rpc.publicnode.com"),
> }).extend(publicActionsL1()) 
>
> const walletClientL1 = createWalletClient({
> account,
> chain: sepolia,
> transport: http("https://ethereum-sepolia-rpc.publicnode.com"),
> }).extend(walletActionsL1());
>
> const publicClientL2 = createPublicClient({
> chain: optimismSepolia,
> transport: http("https://sepolia.optimism.io"),
> }).extend(publicActionsL2());
>
> const walletClientL2 = createWalletClient({
> account,
> chain: optimismSepolia,
> transport: http("https://sepolia.optimism.io"),
> }).extend(walletActionsL2());
>
> const l1Balance = await publicClientL1.getBalance({ address: account.address });
> console.log(`L1 Balance: ${formatEther(l1Balance)} ETH`); 
>
> async function depositETH() {
>
> const depositArgs = await publicClientL2.buildDepositTransaction({
> mint: parseEther("0.01"),
> to: account.address,
> });
>
> const depositHash = await walletClientL1.depositTransaction(depositArgs);
> console.log(`Deposit transaction hash on L1: ${depositHash}`);
>
> const depositReceipt = await publicClientL1.waitForTransactionReceipt({ hash: depositHash });
> console.log('L1 transaction confirmed:', depositReceipt);
>
> const [l2Hash] = getL2TransactionHashes(depositReceipt);
> console.log(`Corresponding L2 transaction hash: ${l2Hash}`);
>
> const l2Receipt = await publicClientL2.waitForTransactionReceipt({
> hash: l2Hash,
> }); 
> console.log('L2 transaction confirmed:', l2Receipt);
> console.log('Deposit completed successfully!');
> } 
> ```

## ETH 출금하기

방금 L1에서 L2로 ETH를 브리징했습니다.
잘하셨습니다!
이제 과정을 반대로 반복하여 L2에서 L1으로 ETH를 브리징합니다.

[ETH 브리징](https://docs.optimism.io/ko/app-developers/guides/bridging/standard-bridge.md#bridging-eth)에 설명된 방법 외에도 아래 예제에 나온 방식으로 ETH를 출금할 수 있습니다.

> [tab] withdrawETH
>
> **1. 출금 트랜잭션 생성**
>
> `buildInitiateWithdrawal`을 사용하여 출금 매개변수를 생성합니다.
> 출금 금액을 `wei`로 변환하고 L1에서의 수신자를 지정합니다.
>
> ```js theme={null}
> //Add the same imports used in DepositETH function
> const withdrawalArgs = await publicClientL1.buildInitiateWithdrawal({
> value: parseEther('0.005'),
> to: account.address,
> });
> ```
>
> **2. 출금 실행**
>
> L2에서 출금 트랜잭션을 전송합니다. 이 트랜잭션은 L2에서 출금 과정을 시작하며, 출금을 추적하기 위한 트랜잭션 해시를 로그로 출력합니다.
>
> ```js theme={null}
> const withdrawalHash = await walletClientL2.initiateWithdrawal(withdrawalArgs);
> console.log(`Withdrawal transaction hash on L2: ${withdrawalHash}`);
> ```
>
> **3. L2 트랜잭션 확인**
>
> 트랜잭션이 포함된 L2 Output이 제안(propose)될 때까지 (최대) 1시간 기다린 후, 블록 번호 등 중요한 세부 정보가 담긴 영수증을 로그로 출력합니다.
>
> ```js theme={null}
> const withdrawalReceipt = await publicClientL2.waitForTransactionReceipt({ hash: withdrawalHash });
> const withdrawalBlock = await publicClientL2.getBlock({
> blockNumber: withdrawalReceipt.blockNumber
> });
> const respectedGameType = await publicClientL1.readContract({
> abi: [{
> inputs: [],
> name: 'respectedGameType',
> outputs: [{ type: 'uint32' }],
> stateMutability: 'view',
> type: 'function'
> }],
> address: publicClientL2.chain.contracts.portal[publicClientL1.chain.id].address,
> functionName: 'respectedGameType'
> });
> const proveContext = isSuperGameType(Number(respectedGameType))
> ? { l2Timestamp: withdrawalBlock.timestamp }
> : {};
> console.log('L2 transaction confirmed:', withdrawalReceipt);
> ```
>
> **4. 출금 증명 대기**
>
> 다음으로, L2에서 출금이 발생했음을 L1의 bridge에 증명해야 합니다. 이를 위해 먼저 출금을 증명할 준비가 될 때까지 기다려야 합니다.
>
> ```js theme={null}
> const { game, withdrawal } = await publicClientL1.waitToProve({
> ...proveContext,
> receipt: withdrawalReceipt,
> targetChain: walletClientL2.chain
> });
> ```
>
> L2에서 출금을 증명하기 위한 매개변수를 구성합니다.
>
> ```js theme={null}
> const proveArgs = await publicClientL2.buildProveWithdrawal({
> game,
> withdrawal,
> });
> ```
>
> **5. L1에서 출금 증명**
>
> 출금을 증명할 준비가 되면 L1 트랜잭션을 전송하여 L2에서 출금이 발생했음을 증명합니다.
>
> ```js theme={null}
> const proveHash = await walletClientL1.proveWithdrawal(proveArgs);
>
> const proveReceipt = await publicClientL1.waitForTransactionReceipt({ hash: proveHash });
> ```
>
> **6. 출금 최종 확정 대기**
>
> 출금 트랜잭션을 최종 확정하려면 먼저 최종 확정 기간을 기다려야 합니다.
> 이는 결함 증명 기간이 지난 후에만 가능합니다. OP Mainnet에서는 7일이 걸립니다.
>
> ```js theme={null}
> const awaitWithdrawal = await publicClientL1.waitToFinalize({
> targetChain: walletClientL2.chain,
> withdrawalHash: withdrawal.withdrawalHash,
> });
> ```
>
> > [!NOTE]
> >
> > 현재 OP Sepolia에서 결함 증명을 테스트하고 있으므로, 출금 소요 시간은
> > Mainnet 소요 시간과 동일합니다.
>
> **7. 출금 최종 확정**
>
> ```js theme={null}
> const finalizeHash = await walletClientL1.finalizeWithdrawal({
> targetChain: walletClientL2.chain,
> withdrawal,
> });
> ```
>
> **8. 출금이 최종 확정될 때까지 대기**
>
> ```js theme={null}
> const finalizeReceipt = await publicClientL1.waitForTransactionReceipt({
> hash: finalizeHash
> });
> ```

> [tab] 전체 코드
>
> ```js theme={null}
> //Add the same imports used in DepositETH function
> const withdrawalArgs = await publicClientL1.buildInitiateWithdrawal({
> value: parseEther('0.005'),
> to: account.address,
> });
>
> const withdrawalHash = await walletClientL2.initiateWithdrawal(withdrawalArgs);
> console.log(`Withdrawal transaction hash on L2: ${withdrawalHash}`);
>
> const withdrawalReceipt = await publicClientL2.waitForTransactionReceipt({ hash: withdrawalHash });
> const withdrawalBlock = await publicClientL2.getBlock({
> blockNumber: withdrawalReceipt.blockNumber
> });
> const respectedGameType = await publicClientL1.readContract({
> abi: [{
> inputs: [],
> name: 'respectedGameType',
> outputs: [{ type: 'uint32' }],
> stateMutability: 'view',
> type: 'function'
> }],
> address: publicClientL2.chain.contracts.portal[publicClientL1.chain.id].address,
> functionName: 'respectedGameType'
> });
> const proveContext = isSuperGameType(Number(respectedGameType))
> ? { l2Timestamp: withdrawalBlock.timestamp }
> : {};
> console.log('L2 transaction confirmed:', withdrawalReceipt);
>
> const { game, withdrawal } = await publicClientL1.waitToProve({
> ...proveContext,
> receipt: withdrawalReceipt,
> targetChain: walletClientL2.chain
> });
>
> const proveArgs = await publicClientL2.buildProveWithdrawal({
> game,
> withdrawal,
> });
>
> const proveHash = await walletClientL1.proveWithdrawal(proveArgs);
>
> const proveReceipt = await publicClientL1.waitForTransactionReceipt({ hash: proveHash });
>
> const awaitWithdrawal = await publicClientL1.waitToFinalize({
> targetChain: walletClientL2.chain,
> withdrawalHash: withdrawal.withdrawalHash,
> });
>
> const finalizeHash = await walletClientL1.finalizeWithdrawal({
> targetChain: walletClientL2.chain,
> withdrawal,
> });
>
> const finalizeReceipt = await publicClientL1.waitForTransactionReceipt({
> hash: finalizeHash
> });
>
> ```

> [!NOTE]
>
> `waitToProve` 및 `waitToFinalize` 액션을 실행하기 전에 [getWithdrawalStatus](https://viem.sh/op-stack/actions/getWithdrawalStatus)로 확인하는 것을 권장합니다.
>
> ```js theme={null}
> const status = await publicClientL1.getWithdrawalStatus({
> ...proveContext,
> receipt: withdrawalReceipt,
> targetChain: walletClientL2.chain
> })
> console.log(`Withdrawal status: ${status}`)
> ```

## L1에서 임의의 L2 트랜잭션 제출하기

EOA는 L2에서 실행되어야 하는 모든 트랜잭션을 L1에서 제출할 수 있습니다. 이를 통해 [Sequencer가 중단된 경우](https://docs.optimism.io/ko/op-stack/transactions/forced-transaction.md)에도 사용자가 L2의 컨트랙트와 상호작용할 수 있습니다.

호출자가 L1의 컨트랙트인 경우 [주소 별칭(Address Aliasing)](https://specs.optimism.io/protocol/deposits.html#address-aliasing)에 주의해야 합니다.

[OP Mainnet으로 ERC-20 토큰 브리징하기](https://docs.optimism.io/ko/app-developers/tutorials/bridging/cross-dom-bridge-erc20.md) 튜토리얼을 막 완료했다면, L2에서 실행될 ERC-20 전송 트랜잭션을 L1에서 시작해 볼 수 있습니다.

`encodeFunctionData`와 `erc20Abi`는 Viem에서 가져올 수 있습니다.

```js theme={null}
const oneToken = parseEther('1')
// L2 faucet token contract
const to = "0xD08a2917653d4E460893203471f0000826fb4034"

const data = encodeFunctionData({
  abi: erc20Abi,
  functionName: "transfer",
  args: [
    "0x000000000000000000000000000000000000dEaD", // recipient
    oneToken / 2n, 
  ],
});

const args = await publicClientL2.buildDepositTransaction({
  account,
  data,
  to,
});
```

## `OptimismPortal`을 사용하여 L2에서 L1으로 임의의 메시지 보내기

`L2ToL1MessagePasser` 컨트랙트의 `initiateWithdrawal` 함수는 `_target` 주소와 `_data` 바이트를 인자로 받습니다. 이 값들은 챌린지 기간이 지난 후 `finalizeWithdrawalTransaction`이 실행될 때 L1의 CALL opcode로 전달됩니다.

즉, 설계상 `OptimismPortal` 컨트랙트를 사용하여 L1에서 임의의 트랜잭션을 보낼 수 있으며, 이때 `OptimismPortal`이 `msg.sender` 역할을 합니다.

## 중요 고려 사항

> [!NOTE]
>
> * 이 튜토리얼의 목적은 입금 트랜잭션과 출금을 소개하는 것입니다. 먼저 [standard bridge](https://docs.optimism.io/ko/app-developers/guides/bridging/standard-bridge.md)와 [messenger](https://docs.optimism.io/ko/app-developers/guides/bridging/messaging.md)가 사용 사례의 요구 사항을 충족하는지 검토해야 합니다.
> * 입금 트랜잭션을 다룰 때는 [주소 별칭(Address Aliasing)](https://specs.optimism.io/protocol/deposits.html#address-aliasing)의 영향을 고려하십시오.
> * 출금을 다룰 때는 [OptimismPortal이 L1에서 임의의 메시지를 보낼 수 있다는 점](https://specs.optimism.io/protocol/withdrawals.html#optimismportal-can-send-arbitrary-messages-on-l1)을 고려하십시오.
> * 챌린지 기간: 7일간의 출금 챌린지 기간은 보안에 매우 중요합니다.
> * Gas 비용: 출금에는 L2와 L1 양쪽의 트랜잭션이 포함되며, 각각 gas 수수료가 발생합니다.
> * 개인 키 관리: 실제 애플리케이션에서는 안전한 키 관리 방식을 사용하십시오.
> * RPC 엔드포인트 보안: API 키(또는 모든 RPC 엔드포인트)를 안전하게 보관하십시오.
