鏈上互動 === Outline --- * 以太坊交易 * 發起交易、銘刻銘文 * 與智能合約互動 * receive & fallback function 以太坊交易 --- ### 傳統以太坊交易 * nonce:這是一個計數器,用於確保每個交易都是唯一的。它代表特定地址發出的交易數量。 * gasPrice:這是用戶願意為每個gas單位支付的價格。以太坊的交易費用基於gas系統,其中每個交易或智能合約的執行都會消耗一定量的gas。 * gasLimit:這是用戶為一個交易設置的最大gas消耗量。它用於確保交易不會因為執行複雜的操作而消耗過多的資源。 * to:這是交易的接收者地址。對於創建新智能合約的交易,此欄位可能留空。 * value:這是交易中要傳輸的原生幣(Native Token)數量。 * 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 單位願意支付的總最大費用,包括基本費用和優先費用。 <b>PS: Base + Priority = Gas Price </b> ![未命名绘图.drawio (5)](https://hackmd.io/_uploads/SJV2rMvQ0.png) * gasLimit:這是用戶為交易設置的最大 Gas 消耗量。 * to(接收者):交易的接收者地址。對於創建新智能合約的交易,此欄位可能留空。 * value:交易中要傳輸的以太幣(ETH)數量。 * data:包含交易的輸入數據,尤其是在執行智能合約時非常重要。 * <b>accesslist(訪問列表,可選)</b>:這是一個包含地址和存儲鍵的列表,用於預先聲明交易將訪問的賬戶和存儲槽,以便更好地計算 Gas 費用。 * <b>chainId</b>:指定交易所屬的以太坊網絡,以防止重放攻擊。https://chainlist.org/?search= * 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", .... } ``` https://etherscan.io/tx/0x23c4799784c91023204bd68a94ec7a963486f2485dc43c13d8b804d5301b8041 * 智能合約互動 ```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 發起交易 --- ### 鏈上互動套件 * ethers.js * web3.js * viem 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 * Blast https://blastapi.io/ ### 環境安裝 * 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) 發起含訊息的交易 --- ```solidity= 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](https://hackmd.io/_uploads/Syp--mvmC.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(); ``` 範例合約 --- ```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 ++ ; } } } ``` 補充:Fallback & Receive --- * receive ```solidity= // 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 ```solidity= // 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()); } } ```