這邊比較困難一點,至少會一些 javascript 開發的經驗會比較容易上手
因為要準備使用 React 開發單頁式的網站來跟 Metamask 錢包互動
樣式什麼的就先不管了
主要目標兩個
- 能透過網站和自己的 Metamask 錢包互動
- 能和自己的合約互動
先建立一個基本的 React 空專案
npx create-react-app nic_meta_web
cd nic_meta_web
yarn start
成功後可以看到一個空的網站,轉著一個 LOGO
安裝套件,準備跟 Web3 互動
yarn add web3 @web3-react/core @web3-react/injected-connector
在 index.js 頁面用 Web3 Provider 注入
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Web3 from 'web3'
import { Web3ReactProvider } from '@web3-react/core'
function getLibrary(provider) {
return new Web3(provider)
}
ReactDOM.render(
<React.StrictMode>
<Web3ReactProvider getLibrary={getLibrary}>
<App />
</Web3ReactProvider>
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
重新啟動 yarn start 之後會發現
該死的 webpack 5 會噴錯
只好解決一下
yarn add react-app-rewired
//config-overrides.js
const webpack = require('webpack');
module.exports = function override(config) {
const fallback = config.resolve.fallback || {};
Object.assign(fallback, {
"crypto": require.resolve("crypto-browserify"),
"stream": require.resolve("stream-browserify"),
"assert": require.resolve("assert"),
"http": require.resolve("stream-http"),
"https": require.resolve("https-browserify"),
"os": require.resolve("os-browserify"),
"url": require.resolve("url")
})
config.resolve.fallback = fallback;
config.plugins = (config.plugins || []).concat([
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer']
})
])
return config;
}
然後修改腳本的啟動設定,用 rewired 來做
// package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
重新 yarn start
後重見光明
新增 src/web3.js
// src/web3.js
import Web3 from 'web3';
window.ethereum.enable();
const web3 = new Web3(window.web3.currentProvider);
export default web3;
修改 src/app.js
// src/app.js
import { InjectedConnector } from '@web3-react/injected-connector'
import { useWeb3React } from "@web3-react/core"
const App = () => {
const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42],
})
const { account, activate } = useWeb3React()
async function connect() {
try {
await activate(injected)
} catch (ex) {
console.log(ex)
}
}
return (
<div className="App">
<p>ETH Address: {account}</p>
<button onClick={connect} >Connect to MetaMask</button>
</div>
);
}
export default App;
看見網頁的按鈕後,嘗試按下去進行連結
正確的話會看到你已經可以跟錢包進行基本的連線互動
取得連線後,應該可以看到 ETH 的地址
接下來要能夠 Mint 自己做好的合約,需要回到 remix 編輯器拿到 ABI
ABI 是 (Application Binary Interface)的縮寫
簡單說,API 是程式間互動的介面
這個介面包含程式提供外部存取的 functions、variables
ABI 也是程式間互動的介面
因為智能合約在被編譯之後會轉為 binary code
所以同樣的介面,但傳遞是 binary 格式的資料
ABI 的部分就是描述如何 decode/encode 程式間傳遞的 binary 資料
找到編輯器左邊下方,去 Copy ABI 之後
建立 src/Nft.js
// src/Nft.js
import web3 from './web3';
const contractAddress = "your_contract_address";
const abi = ["your_abi"]
// @ts-ignore
export default new web3.eth.Contract(abi, contractAddress);
把 ABI 貼上去之後會變成以下這樣(有點長喔 XD)
import web3 from './web3';
const contractAddress = "0x0c8e8d8431B92194563a2F2CAA208E35d799500A";
const abi = [
{
"inputs": [
{
"internalType": "string",
"name": "initBaseURI",
"type": "string"
},
{
"internalType": "string",
"name": "initNotRevealedUri",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"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": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"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": [],
"name": "MAX_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "_isAuctionActive",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "_isSaleActive",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "_revealed",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"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": "baseExtension",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "flipReveal",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "flipSaleActive",
"outputs": [],
"stateMutability": "nonpayable",
"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": "maxBalance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxMint",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenQuantity",
"type": "uint256"
}
],
"name": "mintNicMeta",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "mintPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "notRevealedUri",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"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": "string",
"name": "_newBaseExtension",
"type": "string"
}
],
"name": "setBaseExtension",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_newBaseURI",
"type": "string"
}
],
"name": "setBaseURI",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_maxBalance",
"type": "uint256"
}
],
"name": "setMaxBalance",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_maxMint",
"type": "uint256"
}
],
"name": "setMaxMint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_mintPrice",
"type": "uint256"
}
],
"name": "setMintPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_notRevealedURI",
"type": "string"
}
],
"name": "setNotRevealedURI",
"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": "index",
"type": "uint256"
}
],
"name": "tokenByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenOfOwnerByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"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"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
// @ts-ignore
export default new web3.eth.Contract(abi, contractAddress);
NFT 的 ABI 設定好之後,就可以把 method 丟到首頁,做一個按鈕來 Call
// src/App.js
import { InjectedConnector } from '@web3-react/injected-connector'
import { useWeb3React } from "@web3-react/core"
import web3 from './web3';
import Nft from "./Nft"
const App = () => {
const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42],
})
const { account, activate } = useWeb3React()
async function connect() {
try {
await activate(injected)
} catch (ex) {
console.log(ex)
}
}
const mint = async () => {
const accounts = await web3.eth.getAccounts();
await Nft.methods.mintNicMeta(1).send({
from: accounts[0],
value: web3.utils.toWei("0.03", 'ether'),
});
}
return (
<div className="App">
<p>ETH Address: {account}</p>
<button onClick={connect} >Connect to MetaMask</button>
<button onClick={mint} >Mint</button>
</div>
);
}
export default App;
當你按下 Mint 之後,觸發的互動跳出,表示成功!
總結
基本上能夠做到這裡,就知道怎麼讓網頁跟你的合約互動
剩下的就是製作更多的玩法及檢查等等
瞭解了互動的基本原理後,要成為網路上所說的「科學家」,直接透過程式去跟合約互動也不遠了
大致上的思路就是可能搞個 node.js 把 web3 套件裝一裝,能夠知道合約在哪裡、要怎麼對合約呼叫跟查詢等等
以技術來說的決勝點就是誰寫的程式比較快、策略比較好
剩下的就不會只是開發技術,更多的是邏輯跟蒐集資料的能力了
這系列的筆記就先寫到這裡啦
參考來源
- All In One NFT Website Development 系列
- 【Ethereum 智能合約開發筆記】深入智能合約 ABI
- NFT 这么火,你懂 ERC721 么
- 2021 快速建立 ERC721 標準智能合約並且 Mint NFT
- A React hook to fetch NFT metadata from anywhere
- web3-react: Connect Users to MetaMask (or any wallet) From Your Frontend
本系列其他文章