// Thats proof of work
【前言】
今天終於來到這個 Project 測試的最後一部囉,如果這個測試完成的話,就真的可以等待產品上架然後推上主網了!那廢話不多說就直接進到最後一步的 Minting dAPP 吧,這個步驟的目的是可以讓顧客在我們網頁上 Mint 我們 Deploy 的 NFT!
【Do We Need a Back-End?】
首先我們先探討一下,這個部分我們需不需要後端呢?其實區塊鏈的目的就是建造一個去中心化的資料庫,理想的情況下我們應該把資料存在鏈上,並且利用智能合約打開一個 API 接口讓 dAPP 可以取用需要的資源,或做交易之類的動作。如果存在鏈上的成本太高,也可以利用 IPFS 或類似的分散式資料庫,來把資料存在雲端去中心化系統之中!但如果擁有一個後端伺服器的話,就等於是中心化系統了對吧!
【ABI】
那要在 web3.js 或 ethers.js 之中連動到智能合約,那我們就必須得到智能合約的 ABI,可以利用下面這張圖的指示取得自己智能合約的 ABI。
得到 ABI 之後在前後加上括號,因為複製出來的沒有 "abi":
,無法取用。之後把它存 src/contracts/SmartContract.json
之中!
{
"abi": [
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
},
{
"internalType": "string",
"name": "_initBaseURI",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
...
}
]
}
【Reducer → Store → Action → APP】
下一步我們要使用 Reducer 來傳遞資料,因為在 React Components 之中傳遞物件是一個非常大的課題,這邊我先不多說,就直接看程式碼吧!
├───App.js
├───Contract
│ └───SmartContract.json
├───Redux
****│ ├───store.js
│ ├───blockchain
│ │ ├───blockchainAction.js
│ │ └───**blockchainReducer.js**
└───└───data
├───dataAction.js
└───**dataReducer.js**
在 src/redux/blockchain/blockchainReducer.js
之中,我們先初始化一些所需的資料,並且處理「取得帳戶以及合約地址」的動作。
const initialState = {
loading: false,
account: null,
smartContract: null,
web3: null,
errorMsg: "",
};
const blockchainReducer = (state = initialState, action) => {
switch (action.type) {
case "CONNECTION_REQUEST":
return {
...initialState,
loading: true,
};
case "CONNECTION_SUCCESS":
return {
...state,
loading: false,
account: action.payload.account,
smartContract: action.payload.smartContract,
web3: action.payload.web3,
};
case "CONNECTION_FAILED":
return {
...initialState,
loading: false,
errorMsg: action.payload,
};
case "UPDATE_ACCOUNT":
return {
...state,
account: action.payload.account,
};
default:
return state;
}
};
export default blockchainReducer;
src/redux/data/dataReducer.js
const initialState = {
loading: false,
name: "",
error: false,
errorMsg: "",
};
const dataReducer = (state = initialState, action) => {
switch (action.type) {
case "CHECK_DATA_REQUEST":
return {
...initialState,
loading: true,
};
case "CHECK_DATA_SUCCESS":
return {
...initialState,
loading: false,
name: action.payload.name,
};
case "CHECK_DATA_FAILED":
return {
...initialState,
loading: false,
error: true,
errorMsg: action.payload,
};
default:
return state;
}
};
export default dataReducer;
【Reducer → Store → Action → APP】
├───App.js
├───Contract
│ ├───SmartContract.json
├───Redux
****│ ├───**store.js**
│ ├───blockchain
│ │ ├───blockchainAction.js
│ │ └───blockchainReducer.js
└───└───data
├───dataAction.js
└───dataReducer.js
在 src/redux/store.js
之中,我們把其當作中間層,把上述兩個 Reducer 匯入。
import { applyMiddleware, compose, createStore, combineReducers } from "redux";
import thunk from "redux-thunk";
import blockchainReducer from "./blockchain/blockchainReducer";
import dataReducer from "./data/dataReducer";
const rootReducer = combineReducers({
blockchain: blockchainReducer,
data: dataReducer,
});
const middleware = [thunk];
const composeEnhancers = compose(applyMiddleware(...middleware));
const configureStore = () => {
return createStore(rootReducer, composeEnhancers);
};
const store = configureStore();
export default store;
【Reducer → Store → Action → APP】
├───App.js
├───Contract
│ ├───SmartContract.json
├───Redux
****│ ├───store.js
│ ├───blockchain
│ │ ├───**blockchainAction.js**
│ │ └───blockchainReducer.js
└───└───data
├───**dataAction.js**
└───dataReducer.js
來到 src/redux/data/dataAction.js
之中,我們把存在 store.js
裡面的資料抓進來。這邊 call 出 Name。
// log
import store from "../store";
const fetchDataRequest = () => {
return {
type: "CHECK_DATA_REQUEST",
};
};
const fetchDataSuccess = (payload) => {
return {
type: "CHECK_DATA_SUCCESS",
payload: payload,
};
};
const fetchDataFailed = (payload) => {
return {
type: "CHECK_DATA_FAILED",
payload: payload,
};
};
export const fetchData = (account) => {
return async (dispatch) => {
dispatch(fetchDataRequest());
try {
let name = await store
.getState()
.blockchain.smartContract.methods.name()
.call();
dispatch(
fetchDataSuccess({
name,
})
);
} catch (err) {
console.log(err);
dispatch(fetchDataFailed("Could not load data from contract."));
}
};
};
在 src/redux/blockchain/blockchainAction.js
之中,我們也把資料匯入。因為會使用到 ABI 所以記得要把 SmartContract.json
匯入。此外我們當前是在測試網之中測試,所以 networkId
要特別去改。
// constants
import Web3 from "web3";
import SmartContract from "../../contracts/SmartContract.json";
// log
import { fetchData } from "../data/dataActions";
const connectRequest = () => {
return {
type: "CONNECTION_REQUEST",
};
};
const connectSuccess = (payload) => {
return {
type: "CONNECTION_SUCCESS",
payload: payload,
};
};
const connectFailed = (payload) => {
return {
type: "CONNECTION_FAILED",
payload: payload,
};
};
const updateAccountRequest = (payload) => {
return {
type: "UPDATE_ACCOUNT",
payload: payload,
};
};
export const connect = () => {
return async (dispatch) => {
dispatch(connectRequest());
if (window.ethereum) {
let web3 = new Web3(window.ethereum);
try {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const networkId = await window.ethereum.request({
method: "net_version",
});
// const NetworkData = await SmartContract.networks[networkId];
if (networkId == 4) {
const SmartContractObj = new web3.eth.Contract(
SmartContract.abi,
// NetworkData.address
"0xd768c48ae485325f834a4480188b4a67da89d8e0"
);
dispatch(
connectSuccess({
account: accounts[0],
smartContract: SmartContractObj,
web3: web3,
})
);
// Add listeners start
window.ethereum.on("accountsChanged", (accounts) => {
dispatch(updateAccount(accounts[0]));
});
window.ethereum.on("chainChanged", () => {
window.location.reload();
});
// Add listeners end
} else {
dispatch(connectFailed("Change network to Polygon."));
}
} catch (err) {
dispatch(connectFailed("Something went wrong."));
}
} else {
dispatch(connectFailed("Install Metamask."));
}
};
};
export const updateAccount = (account) => {
return async (dispatch) => {
dispatch(updateAccountRequest({ account: account }));
dispatch(fetchData(account));
};
};
特別說一下這邊為什麼可以直接使用 window.ethereum
的原因我們之前介紹 ethers.js 有解釋過,還記得當初我在【連動 MetaMask】的時候對這個有很大的疑惑呢!
【Reducer → Store → Action → APP】
├───**App.js**
├───Contract
│ ├───SmartContract.json
├───Redux
****│ ├───store.js
│ ├───blockchain
│ │ ├───blockchainAction.js
│ │ └───blockchainReducer.js
└───└───data
├───dataAction.js
└───dataReducer.js
在 src/App.js
先把 store
傳入。
...
import store from "./redux/store";
import { Provider } from "react-redux";
...
export function AccountBox(props) {
...
return (
<Provider store={store}>
...
<SignupForm />
</Provider>
);
}
【Minting Form】
首先導入套件,以及需要的資料。
import { useDispatch, useSelector } from "react-redux";
import { connect } from "./redux/blockchain/blockchainActions";
import { fetchData } from "./redux/data/dataActions";
...
初始化,並且利用 useDispatch, useSelector
把剛剛寫的 blockchain 抓進來。
export function SignupForm(props) {
...
const [inputID, setInputID] = useState("");
const [claiming, setClaiming] = useState(false);
const dispatch = useDispatch();
const blockchain = useSelector((state) => state.blockchain);
useEffect(() => {
if (blockchain.account !== "" && blockchain.smartContract !== null) {
dispatch(fetchData(blockchain.account));
}
}, [blockchain.smartContract, dispatch]);
這邊利用我們之前在介紹 web3.js 提到的 methods 來獲取我們在撰寫智能合約裡面寫的 mint 函數。以及要把價格改成以 Wei 為單位。
const claimNFTs = (_amount) => {
setClaiming(true);
blockchain.smartContract.methods.mint(blockchain.account, _amount).send({
from: blockchain.account,
value: blockchain.web3.utils.toWei((0.02 * _amount).toString(), "ether"),
// 這裡記得要傳入字串
}).once("error", (err) => {
setClaiming(false);
}).then((receipt) => {
setClaiming(false);
})
};
// IN SMART CONTRACT
function mint(address _to, uint256 _mintAmount) public payable {
...
}
最後我們把前端的東西安排好就好!這裡可以直接選要 Mint 的數量並且連線到智能合約,最後按下 Mint 就大功告成了!
return (
<BoxContainer>
...
{blockchain.account === "" || blockchain.smartContract === null ? (
<SubmitButton1
onClick={(e) => {
e.preventDefault();
dispatch(connect());
}} >
{"Connect!"}
</SubmitButton1>
) : (
<SubmitButton1 disabled={claiming}
onClick={(e) => {
e.preventDefault();
if(inputID === ""){
claimNFTs(1);
}
else{
claimNFTs(inputID);
}
}} >
{claiming ? "Claiming" : "Mint!"}
</SubmitButton1>
)}
...
</BoxContainer>
);
}
【小結】
到這裡全部的測試流程都完成啦,明天就可以進入上主鏈,然後打完收工!!!真是開心我快要可以休息了,自從搞了這個 Project 真的是沒有一天睡好嗚嗚。
【參考資料 - Minting dAPP】
How do you get a json file (ABI) from a known contract address?
Designing a minting app
Code a 10000 NFT Minting Dapp part 1
How to Mint an NFT (Part 2/3 of NFT Tutorial Series) | ethereum.org
?How to Mint an NFT Using Web3.js
?How to Mint an NFT with Ethers.js
Create a Complete NFT App - Smart contract, Backend, Frontend