智能合約教學(6) Web3聊天室

0 14
Avatar for vcckvv
Written by
2 years ago

依照預告,這次要利用區塊鏈功能來打造一個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

這次的合約程式碼雖然比之前複雜,不過基本的邏輯運作還是很簡單,下一篇要來挑戰更難的去中心化交易所,會用到比較複雜的資料結構和介面操作

1
$ 0.00
Avatar for vcckvv
Written by
2 years ago

Comments