鏈上互動

Outline

  • 以太坊交易
  • 發起交易、銘刻銘文
  • 與智能合約互動
  • receive & fallback function

以太坊交易

傳統以太坊交易

  • nonce:這是一個計數器,用於確保每個交易都是唯一的。它代表特定地址發出的交易數量。
  • gasPrice:這是用戶願意為每個gas單位支付的價格。以太坊的交易費用基於gas系統,其中每個交易或智能合約的執行都會消耗一定量的gas。
  • gasLimit:這是用戶為一個交易設置的最大gas消耗量。它用於確保交易不會因為執行複雜的操作而消耗過多的資源。
  • to:這是交易的接收者地址。對於創建新智能合約的交易,此欄位可能留空。
  • value:這是交易中要傳輸的原生幣(Native Token)數量。
  • data:這個欄位包含了交易的輸入數據,尤其在執行智能合約時非常重要。
  • v,r,s:這些是與交易的數字簽名相關的參數。這些參數用於驗證交易的真實性和安全性。
{ "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:這是一個計數器,用於確保每個交易都是唯一的。它代表特定地址發出的交易數量。
  • maxPriorityFeePerGas(最大優先費用):這是用戶願意為每個 Gas 單位支付給礦工的最大額外費用,以優先處理其交易。
  • maxFeePerGas(最大費用):這是用戶為每個 Gas 單位願意支付的總最大費用,包括基本費用和優先費用。
    PS: Base + Priority = Gas Price

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

  • gasLimit:這是用戶為交易設置的最大 Gas 消耗量。
  • to(接收者):交易的接收者地址。對於創建新智能合約的交易,此欄位可能留空。
  • value:交易中要傳輸的以太幣(ETH)數量。
  • data:包含交易的輸入數據,尤其是在執行智能合約時非常重要。
  • accesslist(訪問列表,可選):這是一個包含地址和存儲鍵的列表,用於預先聲明交易將訪問的賬戶和存儲槽,以便更好地計算 Gas 費用。
  • chainId:指定交易所屬的以太坊網絡,以防止重放攻擊。https://chainlist.org/?search=
  • v, r, s: :用於確保交易的安全性和來自指定發送者的真實性。
{ "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...(十六進制數據)" // 簽名參數 }

以太坊上的交易基本上分三種

  • 轉帳
    -> 不含附加訊息
{ "from": "0x17F6adf05b64C033f8f5Fb360d6Be467ad9508BF", "to": "0x1a3c……4d8", "value": "10000000000000", "data": "0x", "gasLimit": "300000", .... }

-> 含附加訊息

{ "from": "0x17F6adf05b64C033f8f5Fb360d6Be467ad9508BF", "to": "0x1a3c……4d8", "value": "10000000000000", "data": "0x0118b9c115129006502d63ae0eb3c701502c4a99e1320803b000ce38ebc9805a", "gasLimit": "300000", .... }

https://etherscan.io/tx/0x23c4799784c91023204bd68a94ec7a963486f2485dc43c13d8b804d5301b8041

  • 智能合約互動
{ "from": "0x17F6adf05b64C033f8f5Fb360d6Be467ad9508BF", "to": "0x1a3c……4d8", "value": "10000000000000", "data": "0x0118b9c115129006502d63ae0eb3c701502c4a99e1320803b000ce38ebc9805a", "gasLimit": "300000", .... }
  • 前8位(4個字節): function selector。
  • 其後:將function,所需的參數,進行abi.encode。
  • 結合上述兩項,即為最終data內容。

實際鏈上交易

https://etherscan.io/tx/0xbf636aa5c7405377b42bfc6b154a09f4458f5e91c9d256741e9f0ec96a7c4caa

發起交易

鏈上互動套件

  • ethers.js
  • web3.js
  • viem

Ethers

  • 定義:一個Javascript的函式庫,用於與以太坊RPC交互的工具。

  • Ethers 更加簡單直觀,有內嵌安全檢查。

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

    圖源:https://foresightnews.pro/article/detail/16414

  • 運作架構圖

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

RPC Provider

環境安裝

mkdir <my-typescript-project> cd <my-typescript-project> npm init -y
  • Step5: 安裝Typescript。
npm install typescript --save-dev
  • Step6: 初始化typescript設定檔。會看到目錄下多出一個tsconfig.json
tsc --init
  • Step7: 安裝所需工具-ethers,指定版本為5.7.2
npm install ethers@5.7.2 --save
  • Step8: 安裝 @types/node & dotenv
npm i --save-dev @types/node npm install dotenv

發起交易

  • 下載讀取環境變量的工具
npm install dotenv
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

發起含訊息的交易

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"), data: ethers.utils.hexlify(ethers.utils.toUtf8Bytes('hello world')), }; // 發送交易 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-05-19 at 2.49.25 PM

與智能合約互動

  • 透過組成交易與合約互動
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為智能合約使用說明書
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();

範例合約

// 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 ++ ; } } }

補充:Fallback & Receive

  • receive
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract ReceiveExample { event Received(address sender, uint amount); // Receive function must be declared as external and payable. receive() external payable { // Emit an event with the sender and the amount received emit Received(msg.sender, msg.value); } }
  • fallback
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract FallbackExample { event Log(uint gas); // Fallback function must be declared as external. fallback() external payable { // Emit an event with the current gas left emit Log(gasleft()); } }