---
# System prepended metadata

title: 區塊鏈全端工程師 W3 鏈上互動課程講義

---

鏈上互動
===
Outline
---
* 以太坊交易
* 銘文底層技術
* 發起交易、銘刻銘文
* 與智能合約互動

以太坊交易
---
### 傳統以太坊交易
* nonce：這是一個計數器，用於確保每個交易都是唯一的。它代表特定地址發出的交易數量。
* gasPrice：這是用戶願意為每個gas單位支付的價格。以太坊的交易費用基於gas系統，其中每個交易或智能合約的執行都會消耗一定量的gas。
* gasLimit：這是用戶為一個交易設置的最大gas消耗量。它用於確保交易不會因為執行複雜的操作而消耗過多的資源。
* to：這是交易的接收者地址。對於創建新智能合約的交易，此欄位可能留空。
* value：這是交易中要傳輸的以太幣（ETH）數量。
* data：這個欄位包含了交易的輸入數據，尤其在執行智能合約時非常重要。
* v,r,s：這些是與交易的數字簽名相關的參數。這些參數用於驗證交易的真實性和安全性。
```json=
{
  "nonce": "0x15",                   // 交易計數，表示特定賬戶的交易數量
  "gasPrice": "0x4a817c800",         // Gas 價格，用戶願意為每個 Gas 單位支付的價格
  "gasLimit": "0x5208",              // Gas 限制，交易可以消耗的最大 Gas 數量
  "to": "0x3535353535353535353535353535353535353535",  // 接收者的地址
  "value": "0x0",                    // 轉移的 ETH 數量，這裡是 0
  "data": "0x",                      // 與交易相關的數據，這裡是空
  "v": "0x1c",                       // 簽名參數
  "r": "0x...（十六進制數據）",     // 簽名參數
  "s": "0x...（十六進制數據）"      // 簽名參數
}

```

### EIP-1559 新的交易格式
* nonce：這是一個計數器，用於確保每個交易都是唯一的。它代表特定地址發出的交易數量。
* <b>maxPriorityFeePerGas（最大優先費用）</b>：這是用戶願意為每個 Gas 單位支付給礦工的最大額外費用，以優先處理其交易。
* <b>maxFeePerGas（最大費用）</b>：這是用戶為每個 Gas 單位願意支付的總最大費用，包括基本費用和優先費用。
* gasLimit：這是用戶為交易設置的最大 Gas 消耗量。
* to（接收者）：交易的接收者地址。對於創建新智能合約的交易，此欄位可能留空。
* value：交易中要傳輸的以太幣（ETH）數量。
* data：包含交易的輸入數據，尤其是在執行智能合約時非常重要。
* <b>accesslist（訪問列表，可選）</b>：這是一個包含地址和存儲鍵的列表，用於預先聲明交易將訪問的賬戶和存儲槽，以便更好地計算 Gas 費用。
* <b>chainId</b>：指定交易所屬的以太坊網絡，以防止重放攻擊。
* v, r, s: ：用於確保交易的安全性和來自指定發送者的真實性。

```json=
{
  "nonce": "0x1",                             // 交易計數，表示特定賬戶的交易數量
  "maxPriorityFeePerGas": "0x3B9ACA00",        // 最大優先費用/每單位Gas（願意支付給礦工的額外費用）
  "maxFeePerGas": "0x4E3B29200",               // 最大費用/每單位Gas（包括基本費用和優先費用）
  "gasLimit": "0x5208",                        // Gas限制，交易可以消耗的最大Gas數量
  "to": "0xAbc123...（接收者地址）",           // 接收者的地址
  "value": "0x2386F26FC10000",                 // 轉移的ETH數量
  "data": "0x...",                             // 與交易相關的數據
  "accessList": [                              // 訪問列表（可選）
    {
      "address": "0x...（地址）",
      "storageKeys": [
        "0x...（存儲鍵）",
        "0x...（另一個存儲鍵）"
      ]
    }
    // 可能還有更多地址和存儲鍵
  ],
  "chainId": "0x1",                            // 指定交易所屬的以太坊網絡
  "type": "0x2",                               // 交易類型，0x2 表示 EIP-1559 交易
  "v": "0x25",                                 // 簽名參數
  "r": "0x1C...（十六進制數據）",             // 簽名參數
  "s": "0x3D...（十六進制數據）"              // 簽名參數
}

```

### 以太坊上的交易基本上分兩種
* 轉帳
-> 不含附加訊息
```json=
{
    "from": "0x17F6adf05b64C033f8f5Fb360d6Be467ad9508BF",
    "to": "0x1a3c……4d8",
    "value": "10000000000000",
    "data": "0x",
    "gasLimit": "300000",
    ....
}
```
-> 含附加訊息
```json=
{
    "from": "0x17F6adf05b64C033f8f5Fb360d6Be467ad9508BF",
    "to": "0x1a3c……4d8",
    "value": "10000000000000",
    "data": "0x0118b9c115129006502d63ae0eb3c701502c4a99e1320803b000ce38ebc9805a",
    "gasLimit": "300000",
    ....
}
```
* 智能合約互動
```json=
{
    "from": "0x17F6adf05b64C033f8f5Fb360d6Be467ad9508BF",
    "to": "0x1a3c……4d8",
    "value": "10000000000000",
    "data": "0x0118b9c115129006502d63ae0eb3c701502c4a99e1320803b000ce38ebc9805a",
    "gasLimit": "300000",
    ....
}
```
* 前8位（4個字節）： function selector。
* 其後：將function，所需的參數，進行abi.encode。
* 結合上述兩項，即為最終data內容。
    

實際鏈上交易
---
https://etherscan.io/tx/0xbf636aa5c7405377b42bfc6b154a09f4458f5e91c9d256741e9f0ec96a7c4caa

銘文底層技術
---
* 銘文始於比特幣網路
![Screenshot 2024-01-11 at 9.57.26 PM](https://hackmd.io/_uploads/S170Xup_T.png)

* 以太坊銘文
![Screenshot 2024-01-11 at 9.57.39 PM](https://hackmd.io/_uploads/r1CCmdT_p.png)

* 以太坊銘文

![Screenshot 2024-01-11 at 9.57.50 PM](https://hackmd.io/_uploads/ry2kNuaOa.png)



發起交易、銘刻銘文
---
### 鏈上互動工具

* ethers.js
* web3.js
* viem
* wagmi

Ethers
---
* 定義：一個Javascript的函式庫，用於與以太坊RPC交互的工具。

* Ethers 更加簡單直觀，有內嵌安全檢查。
![Screenshot 2024-01-06 at 2.56.09 PM](https://hackmd.io/_uploads/rJY9Y_LdT.png)
圖源：https://foresightnews.pro/article/detail/16414

* 運作架構圖

![Screenshot 2024-01-11 at 9.58.20 PM](https://hackmd.io/_uploads/HJcZVOadT.png)

圖源：https://docs.alchemy.com/docs/ethereum-frontend-libraries

### RPC Provider

* Infura:
https://app.infura.io/
* Alchemy
https://dashboard.alchemy.com/apps

### 環境安裝

* Step1: 下載Node js
https://nodejs.org/en
* Step2: VsCode安裝
https://code.visualstudio.com/
* Step3: 建立JS專案目錄，並初始化npm，會看到目錄下多出一個package.json。
```shell=
mkdir <my-typescript-project>
cd <my-typescript-project>
npm init -y
```
* Step5: 安裝Typescript。
```shell=
npm install typescript --save-dev
```
* Step6: 初始化typescript設定檔。會看到目錄下多出一個tsconfig.json
```shell=
tsc --init
```
* Step7: 安裝所需工具-ethers，指定版本為5.7.2
```shell=
npm install ethers@5.7.2 --save
```
* Step8: 安裝 @types/node & dotenv 
```shell=
npm i --save-dev @types/node
npm install dotenv   
```

### 發起交易

* 下載讀取環境變量的工具
```shell=
npm install dotenv
```
```typescript=
import {ethers} from "ethers";
import dotenv from 'dotenv';
dotenv.config();

const RPC_URL = process.env.RPC_URL;
const PRIVATE_KEY= process.env.PRIVATE_KEY;

const main = async()=>{

    if (!RPC_URL) {
        throw new Error("RPC_URL 環境變數未設置。");
    }

    if (!PRIVATE_KEY) {
        throw new Error("PRIVATE_KEY 環境變數未設置。");
    }

    const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
    const signer = new ethers.Wallet(PRIVATE_KEY, provider);

    try {
        // 創建交易
        const tx = {
            to: signer.address,
            value: ethers.utils.parseEther("0.1"),
        };

        // 發送交易
        const transaction = await signer.sendTransaction(tx);
        console.log("交易已發送：", transaction.hash);

        // 等待交易被確認
        const receipt = await transaction.wait();
        console.log("交易已確認：", receipt);

    } catch (error) {
        console.error("交易錯誤：", error);
    }

}


main();
```

![Screenshot 2024-01-11 at 9.58.44 PM](https://hackmd.io/_uploads/SkGmNOa_p.png)

### 銘刻銘文
```typescript=
import {ethers} from "ethers";

const RPC_URL = process.env.RPC_URL;
const PRIVATE_KEY= process.env.PRIVATE_KEY;

const main = async()=>{

    if (!RPC_URL) {
        throw new Error("RPC_URL 環境變數未設置。");
    }

    if (!PRIVATE_KEY) {
        throw new Error("PRIVATE_KEY 環境變數未設置。");
    }

    const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
    const signer = new ethers.Wallet(PRIVATE_KEY, provider);

    try {
        //erc-20
        const deploy_token_content = 'data:,{"p":"erc-20","op":"mint","tick":"midd","id":"15233","amt":"1000"}';
        
        // 創建交易
        const tx = {
            to: signer.address,
            value: ethers.utils.parseEther("0"),
            data: stringToHex(deploy_token_content),
        };

        // 發送交易
        const transaction = await signer.sendTransaction(tx);
        console.log("交易已發送：", transaction.hash);

        // 等待交易被確認
        const receipt = await transaction.wait();
        console.log("交易已確認：", receipt);

    } catch (error) {
        console.error("交易錯誤：", error);
    }

}

const stringToHex = (str: string)=>{
    return "0x"+ Buffer.from(str, 'utf8').toString('hex');
    
}

main();
```

![Screenshot 2024-01-11 at 9.59.17 PM](https://hackmd.io/_uploads/HJmHVO6u6.png)

與智能合約互動
---
* 透過組成交易與合約互動
```typescript=
import {ethers} from "ethers";
import dotenv from 'dotenv';
dotenv.config();

const RPC_URL = process.env.RPC_URL;
const PRIVATE_KEY= process.env.PRIVATE_KEY;

const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY!, provider);

const main = async()=>{

    if (!process.env.RPC_URL) {
        throw new Error("RPC_URL 環境變數未設置。");
    }

    if (!process.env.PRIVATE_KEY) {
        throw new Error("PRIVATE_KEY 環境變數未設置。");
    }


    // function & params
    const functionSignature = "mint(address,uint256)";
    const addressParam = "0x3D71D7DC971e9f8405f287A340E8f65a7a1d392a";
    const amountParam = 3;

    // function selector
    const functionSelector = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(functionSignature)).slice(0, 10);

    // abi.encode params
    const mintParams = abiEncode(['address','uint256'], [addressParam, amountParam]);

    const finalData = functionSelector + mintParams.slice(2);
    
    sendTx("0xf16EE2377aE7CE17D664b2b27B0eefc568b6a969", "0.03", finalData );
    
}

const abiEncode = (types: string[], params: any)=>{
    const encodeData = ethers.utils.defaultAbiCoder.encode(types, params);
    return encodeData;
}

const sendTx = async(to: string, value: string, data: string, )=>{
    try {
        // 創建交易
        const tx = {
            to: to, //合約地址
            value: ethers.utils.parseEther(value),
            data: data,
            gasLimit: ethers.utils.hexlify(1000000),
        };

        // 
        const transaction = await signer.sendTransaction(tx);
        console.log("交易已發送：", transaction);

        // 等待交易被確認
        const receipt = await transaction.wait();
        console.log("交易已確認：", receipt);

    } catch (error) {
        console.error("交易錯誤：", error);
    }
}


main();
```

* 透過更可讀的方式與智能合約互動
    * ABI為智能合約使用說明書
```typescript=
import {ethers} from "ethers";
import dotenv from 'dotenv';
dotenv.config();

const RPC_URL = process.env.RPC_URL;
const PRIVATE_KEY= process.env.PRIVATE_KEY;

const main = async()=>{

    if (!RPC_URL) {
        throw new Error("RPC_URL 環境變數未設置。");
    }

    if (!PRIVATE_KEY) {
        throw new Error("PRIVATE_KEY 環境變數未設置。");
    }

    const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
    const signer = new ethers.Wallet(PRIVATE_KEY, provider);

    const contract_address = "0x9c6471C2a6099993299Ca7E1a7E5a22D1F26902C";
    const nft_abi = '[{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"address","name":"owner","type":"address"}],"name":"ERC721IncorrectOwner","type":"error"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721InsufficientApproval","type":"error"},{"inputs":[{"internalType":"address","name":"approver","type":"address"}],"name":"ERC721InvalidApprover","type":"error"},{"inputs":[{"internalType":"address","name":"operator","type":"address"}],"name":"ERC721InvalidOperator","type":"error"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"ERC721InvalidOwner","type":"error"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"}],"name":"ERC721InvalidReceiver","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"ERC721InvalidSender","type":"error"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721NonexistentToken","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"counter","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"openBlindBox","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}]';

    const contract = new ethers.Contract(contract_address, nft_abi, signer);

    try {
        const res = await contract.mint("0x3D71D7DC971e9f8405f287A340E8f65a7a1d392a",3, {value: ethers.utils.parseEther("0.03")});
        console.log("交易資訊", res);
        await res.wait();

    } catch (error) {
        console.log(error);
    }
}


main();


main();
```

範例合約
---
```solidity=
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";


contract MyNFT is ERC721{

    using Strings for uint256;

    address owner;
    uint256 public maxSupply = 10; // 最大發行量
    bool private isOpened = false;//盲盒是否打開

    uint256 public counter = 0;

    modifier onlyOwner{
        require(msg.sender == owner);
        _;
    }

    constructor (string memory _name, string memory _symbol) ERC721(_name, _symbol){
        owner = msg.sender;
    }
    
    //開盲盒
    function openBlindBox() external onlyOwner{
        isOpened = true;
    }
    
    
    //設定NFT的baseURI(盲盒)
    function _baseURI() internal pure override returns (string memory) {
        return "ipfs://QmXxZBg4RnGxC2dDxfUSAmxgGooHsoncPQgCLiNw8kj3Ls/";
    }
    
    //查看NFT Metadata網址
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        if (!isOpened){
            return _baseURI();
        }
        return string(abi.encodePacked("ipfs://QmWQcaFFCm9ofyVN2ZwGbTGopLEbQ6QSc2Xn1C7ekKAYDF/", tokenId.toString(), ".json"));
        
    }

    // 實作mint function，主要用來demo確認用
    function mint (address to, uint256 amount) external payable{
        require(amount + counter <= maxSupply, "over max supply.");
        require(amount * 10000000000000000 == msg.value, "balance error!");
        // 迴圈批量鑄造NFT
        for(uint256 i=0; i < amount ; i++){
                // 鑄造 NFT, counter為NFT的tokenId
                _mint(to, counter);
                counter ++ ;
        }
    }

}
```

第三週作業
---
* 基礎題：
    * 部一個ERC721的合約，寫一個typescript/javascript，運用ethers，完成：
        * A錢包Mint並Approve 該NFT 給B地址。
        * B地址轉移該NFT到C地址。
    * 用ethers實作一個掏空某錢包地址的ETH的程式
    * 部署一個ERC20，用ethers實作一個掏空該ERC20的程式
* 進階題：
    * 研究Flashbot，並試著將兩筆交易打包在同一個區塊中。