鏈上互動
===
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());
}
}
```