智能合約教學(6) Web3聊天室
依照預告,這次要利用區塊鏈功能來打造一個Web3線上聊天室
Web3線上聊天室基本上就是全部的文章都會上鏈,不過這次寫的聊天室功能很陽春,沒有修改或刪除等功能,唯一有的額外功能是可以設定使用者帳戶的名字,比起位址要更好認出是誰
那麼就直接先來體驗一下:https://vcckvv.github.io/SmartContract/CoolChat/
使用的合約在這裡:https://goerli.etherscan.io/address/0xc9e45809aaca0c8aa485d1bd558276e03e9451f6#code
這裡簡單介紹一下使用的方法:
1.沒有MetaMask錢包的人要安裝MetaMask錢包瀏覽器外掛
2.因為發文章或設定名字都需要goerli測試鏈的gas費,所以要先到goerli的水龍頭網站領取代幣:https://goerlifaucet.com/
3.接下來打開CoolChat的網頁,會自動跳出MetaMask視窗要求和錢包帳戶連線以及切換到goerli測試鏈
4.然後設定帳戶名字,在「名字」的文字輸入框中輸入想要的名字,設定上最多30個文字,且不能設成別人已經設定的名字,接下來按下「設定名字」的按鈕,MetaMask會跳出交易簽署視窗,confirm後等待上鏈完成就算設定成功,goerli上鏈時間一般在半分鐘內就會完成,且MetaMask會自動跳出成功的通知
稍微提醒一下,在簽署交易時檢查所在鏈、費用、和合約內容算是蠻重要的事,大家最好養成習慣
5.要發文也是相同的做法,在「輸入文章內容」下面的文字輸入框中輸入文章內容,設定上最多1000個文字,接下來按下「設定名字」的按鈕,MetaMask會跳出交易簽署視窗,confirm後等待上鏈完成就算發文成功,此時頁面會自動更新,可以看到自己的文章顯示在列表的上面
注意簽署交易的速度不要太快,要等上一筆的交易上鏈後再發,否則一般都會導致交易失敗,這可能跟交易的nonce數的某些安全限制有關
6.設定上每頁最多顯示20篇文章,文章列表最下面可以選擇要看的頁面,點選後會自動捲動頁面到最上面,選擇不同頁面時不會更新總文章數,所以不會點一點就因為新文章的出現而顯示出不連續的文章編號,如果要看更新的文章只要重新整理頁面就可以了
7.這個聊天室寫下的東西都會永久記錄到goerli測試鏈,所以除非goerli測試鏈被官方停掉,都不需要擔心資料消失的問題,網頁本身實際上也只是靜態網頁,複製放到其他伺服器上也能馬上運作
接下來看一下完整的合約程式碼:
https://github.com/vcckvv/SmartContract/blob/gh-pages/CoolChat/CoolChat.sol
---------------------------------------
pragma solidity ^0.8.0;
// SPDX-License-Identifier: CC-BY-SA-4.0
contract CoolChat {
address founderAddr;
mapping (address => string) addrNameMap;
mapping (string => bool) nameUseMap;
address[] writerArray;
string[] writerNameArray;
uint256[] timeArray;
string[] articleArray;
constructor() {
founderAddr = msg.sender;
}
function getArticleCount() public view returns (uint256 index) {
return articleArray.length;
}
function getArticle(uint256 index) public view returns (address, string memory, uint256, string memory) {
require(index<articleArray.length);
return (writerArray[index], writerNameArray[index], timeArray[index], articleArray[index]);
}
function getArticles(uint256 index, uint256 count) public view returns (address[] memory, string[] memory, uint256[] memory, string[] memory) {
require(count>=1);
require(index+count-1<articleArray.length);
address[] memory outWriterArray=new address[](count);
string[] memory outWriterNameArray=new string[](count);
uint256[] memory outTimeArray=new uint256[](count);
string[] memory outArticleArray=new string[](count);
for(uint i=0; i<count; i++){
outWriterArray[i]=writerArray[index+i];
outWriterNameArray[i]=writerNameArray[index+i];
outTimeArray[i]=timeArray[index+i];
outArticleArray[i]=articleArray[index+i];
}
return (outWriterArray, outWriterNameArray, outTimeArray, outArticleArray);
}
function addArticle(string memory str) public {
require(bytes(str).length>0 && bytes(str).length<=3000);//utf8中文字最多1000字
writerArray.push(msg.sender);
writerNameArray.push(addrNameMap[msg.sender]);
timeArray.push(block.timestamp);
articleArray.push(str);
}
function testMassArticle(uint256 num) public {
require(msg.sender==founderAddr);
string memory name=addrNameMap[msg.sender];
string[4] memory strArray=["0", "1", "2", "3"];
for(uint i=0; i<num; i++){
writerArray.push(msg.sender);
writerNameArray.push(name);
timeArray.push(block.timestamp);
articleArray.push(strArray[i%4]);
}
}
function isUsedName(string memory name) public view returns (bool) {
return nameUseMap[name];
}
function getWriterName(address addr) public view returns (string memory) {
return addrNameMap[addr];
}
function setWriterName(string memory name) public {
require(bytes(name).length>0 && bytes(name).length<=90);//utf8中文字最多30字
require(nameUseMap[name]==false);
string memory oldName=addrNameMap[msg.sender];
if(bytes(oldName).length!=0){
nameUseMap[oldName]=false;
}
nameUseMap[name]=true;
addrNameMap[msg.sender]=name;
}
}
---------------------------------------
解說:
pragma solidity ^0.8.0;
// SPDX-License-Identifier: CC-BY-SA-4.0
這次換到版本0.8.0主要是因為會用到新的版本的功能,舊版的預設上沒辦法回傳陣列形式的變數,所以就改到了新版
SPDX-License-Identifier這段雖然是註解,但是沒加入的話會產生警告,基本上這個功能就是用來宣告版權,如果有其他偏好的版權的話可以參考這個網頁的代碼來設定:https://spdx.org/licenses/
---------------------------------------
address founderAddr;
用來存放建立合約者的帳戶位址,設定這個之後可以讓某些函數只能讓建立合約者來操作
---------------------------------------
mapping (address => string) addrNameMap;
mapping (string => bool) nameUseMap;
映射的資料結構,就是給一個key值然後取出相應的資料的這樣的資料結構,不過使用上和C++的map的功能差很多,這個沒辦法從小到大一筆一筆資料列出來,如果想要更複雜的操作的資料結構都需要自己寫
addrNameMap用來儲存帳戶位址的名字,nameUseMap則用來判斷名字是不是已經有人使用
---------------------------------------
address[] writerArray;
string[] writerNameArray;
uint256[] timeArray;
string[] articleArray;
文章的資料以陣列的形式來呈現,而且是把三種資料分開存放:寫的人的帳戶位址、寫的人設定的名字、文章時間、文章內容
其中文章時間是以timestamp形式來存放,所以是uint256,在javascript中使用以下的方式來轉換成一般的時間格式:
let time = new Date(timestamp * 1000);
let year = time.getFullYear();
let month = time.getMonth()+1;
let date = time.getDate();
let hour = time.getHours();
let min = time.getMinutes();
let sec = time.getSeconds();
---------------------------------------
constructor() {
founderAddr = msg.sender;
}
如之前所述,constructor()只有在建立合約時才會呼叫,所以這裡的msg.sender就是建立合約者的帳戶位址
---------------------------------------
function getArticleCount() public view returns (uint256 index) {
return articleArray.length;
}
取得目前的總文章數,articleArray.length就是文章陣列的資料數目
---------------------------------------
function getArticle(uint256 index) public view returns (address, string memory, uint256, string memory) {
require(index<articleArray.length);
return (writerArray[index], writerNameArray[index], timeArray[index], articleArray[index]);
}
取得一篇文章的資料,原本只寫了這個函數來讀取文章,不過實際測試時發現一篇一篇讀取的速度太慢了,所以下面又寫了一次讀取多個文章的函數getArticles
這裡用到了require()來檢查輸入參數是不是有問題,如果在require()中檢查出了問題就會讓這筆合約互動交易失效,之前做的所有更動都會復原,要注意如果是寫入性質的函數,一旦交易上鏈,那麼如果中途檢查錯誤而導致復原,這樣也會消耗到gas費
另外還有assert()、revert()、throw的寫法,不過一般用require()檢查就夠用了
---------------------------------------
function getArticles(uint256 index, uint256 count) public view returns (address[] memory, string[] memory, uint256[] memory, string[] memory) {
require(count>=1);
require(index+count-1<articleArray.length);
address[] memory outWriterArray=new address[](count);
string[] memory outWriterNameArray=new string[](count);
uint256[] memory outTimeArray=new uint256[](count);
string[] memory outArticleArray=new string[](count);
for(uint i=0; i<count; i++){
outWriterArray[i]=writerArray[index+i];
outWriterNameArray[i]=writerNameArray[index+i];
outTimeArray[i]=timeArray[index+i];
outArticleArray[i]=articleArray[index+i];
}
return (outWriterArray, outWriterNameArray, outTimeArray, outArticleArray);
}
取得多篇文章的資料,從index開始取得count個文章,然後回傳4個陣列變數,注意4個陣列在一開始就必須設定好空間大小,因為memory的陣列不能使用push()一個一個動態塞入,只有storage的鏈上陣列資料才能這樣做,下面提到的函數addArticle()就是用動態的方式塞入新資料
值得一提的是,在JavaScript中有回傳多個變數時是以陣列的形式來呈現:
let data = await contract.getArticles(startIndex, getCount);
接著就可以用data[0][index]、data[1][index]、data[2][index]、data[3][index]來讀取各別文章的資料
---------------------------------------
function addArticle(string memory str) public {
require(bytes(str).length>0 && bytes(str).length<=3000);//utf8中文字最多1000字
writerArray.push(msg.sender);
writerNameArray.push(addrNameMap[msg.sender]);
timeArray.push(block.timestamp);
articleArray.push(str);
}
增加一篇文章的函數,utf8一個中文字一般會有3 bytes,所以限制1000字的話要給3000 bytes限制,不過這也意味著實際上可以塞到3000個英文字
值得注意的是string沒有提供length的功能,所以要先轉成bytes才能知道string的長度
block.timestamp
代表現在區塊的時間
---------------------------------------
function testMassArticle(uint256 num) public {
require(msg.sender==founderAddr);
string memory name=addrNameMap[msg.sender];
string[4] memory strArray=["0", "1", "2", "3"];
for(uint i=0; i<num; i++){
writerArray.push(msg.sender);
writerNameArray.push(name);
timeArray.push(block.timestamp);
articleArray.push(strArray[i%4]);
}
}
因為測試的時候需要產生大量的文章,所以一篇一篇產生太浪費時間也比較浪費gas,這裡寫了一個函數可以大量新增文章,使用require(msg.sender==founderAddr)
來確保只有建立合約的人才能使用這個測試函數,這個就是之前提到的管理員專用的函數
---------------------------------------
function isUsedName(string memory name) public view returns (bool) {
return nameUseMap[name];
}
檢查名字是否已經被使用
---------------------------------------
function getWriterName(address addr) public view returns (string memory) {
return addrNameMap[addr];
}
取得帳戶位址現在設定的名字
---------------------------------------
function setWriterName(string memory name) public {
require(bytes(name).length>0 && bytes(name).length<=90);//utf8中文字最多30字
require(nameUseMap[name]==false);
string memory oldName=addrNameMap[msg.sender];
if(bytes(oldName).length!=0){
nameUseMap[oldName]=false;
}
nameUseMap[name]=true;
addrNameMap[msg.sender]=name;
}
設定帳戶位址的名字,除了基本的檢查之外,要注意把舊的名字設回沒有使用
solidity中要檢查空字串的話要直接看其長度是否為0
---------------------------------------
合約的程式碼解說就到這裡,至於HTML、JavaScript的程式碼就比較沒什麼想特別說的,Web3的部份的功能都是之前有提到過的,想看程式碼的人可以到這裡看:https://github.com/vcckvv/SmartContract/blob/gh-pages/CoolChat/index.html
這次的合約程式碼雖然比之前複雜,不過基本的邏輯運作還是很簡單,下一篇要來挑戰更難的去中心化交易所,會用到比較複雜的資料結構和介面操作