构建一个WEB3应用

本文是一个从零到一的DApp开发教程,包括了从编写合约和UI交互到部署上线的一套完整流程,通过学习构建一个去中心化的图书馆,使开发者能够轻松掌握如何在TRON网络上部署自己的DApp。

开发前准备

安装

1.Nodej v10+

# node -v
v10.24.1

2.TronLink

这里安装TronLink的Google Chrome插件。

我们在做什么?

我们将要构建一个包括以下功能的去中心化图书馆:

  • 租书
  • 查看可借书目
  • 添加书

完整的项目代码从这里下载,运行 npm install 安装依赖。

数据结构

对于借书者来说,通常,需要关心书的名字、内容、是否可借以及价格。基于此,我们在合约中创建一个名为Book的结构,它包括了如下的属性:

struct Book {
       string name;
       string description;
       bool valid; // false if been borrowed
       uint256 price; // TRX per day
       address owner; // owner of the book
   }

我们希望图书馆能通过一个映射来记录每一本书。为此,这里分别创建了一个bookId的属性,以及bookId到Book的映射关系,命名为books。

uint256 public bookId;

mapping (uint256 => Book) public books;

我们还需要记录每一本书的租借信息,包括借书者以及起止时间。

与Book相同,创建一个名为Tracking的结构来记录这些信息。这个结构包含了下面的属性:

struct Tracking {
       uint256 bookId;
       uint256 startTime;  // start time, in timestamp
       uint256 endTime;    // end time, in timestamp
       address borrower;   // borrower's address
}

同样的,我们也需要建立一种映射关系,来管理每一次租借记录:

uint256 public trackingId;

mapping(uint256 => Tracking) public trackings;

定义功能和事件

我们需要为图书馆添加一些基本功能,包括:

  • 为图书馆添加书籍 - addBook
  • 借一本书 - borrowBook
  • 从图书馆移除这本书 - deleteBook

addBook

/**
* @dev Add a Book with predefined `name`, `description` and `price`
* to the library.
* * Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {NewBook} event.
*/
function addBook(string memory name, string memory description, uint256 price) public returns (bool) {
       Book memory book = Book(name, description, true, price, _msgSender());

       books[bookId] = book;

       emit NewBook(bookId++);

       return true;
}

/**
* @dev Emitted when a new book is added to the library.
* Note bookId starts from 0.
*/
event NewBook(uint256 bookId);

borrowBook

/**
* @dev Borrow a book has `_bookId`. The rental period starts from
* `startTime` ends with `endTime`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a `NewRental` event.
*/
function borrowBook(uint256 _bookId, uint256 startTime, uint256 endTime) public payable returns(bool) {
       Book storage book = books[_bookId];

       require(book.valid == true, "The book is currently on loan");

       require(_msgValue() == book.price * _days(startTime, endTime), "Incorrect fund sent.");

       _sendTRX(book.owner, _msgValue());

       _createTracking(_bookId, startTime, endTime);

       emit NewRental(_bookId, trackingId++);
 }

deleteBook

/**
* @dev Delete a book from the library. Only the book's owner or the
* library's owner is authorised for this operation.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a `DeleteBook` event.
*/
function deleteBook(uint256 _bookId) public returns(bool) {
       require(_msgSender() == books[_bookId].owner || isOwner(),
               "You are not authorised to delete this book.");
      
       delete books[_bookId];

       emit DeleteBook(_bookId);
    return true;
}

在borrowBook方法中,我们用到了两个工具方法_sendTRX和_createTracking。我们不希望用户调用这些方法,因此,遵照Solidity的规则,我们把它们标记为internal,表示仅能在合约内部被调用。

_sendTRX

/**
* @dev Send TRX to the book's owner.
*/
function _sendTRX(address receiver, uint256 value) internal {
    payable(address(uint160(receiver))).transfer(value);
}

_createTracking

/**
* @dev Create a new rental tracking.
*/
function _createTracking(uint256 _bookId, uint256 startTime, uint256 endTime) internal {
     trackings[trackingId] = Tracking(_bookId, startTime, endTime, _msgSender());

     Book storage book = books[_bookId];

     book.valid = false;
}

现在我们已经完成了合约的编写工作,接下来就该部署上线了。

部署和测试

我们用TronIDE来编译和部署合约。为了方便测试,建议使用TRON的Nile测试网进行首次部署。

📘

Nile测试网

Nile是TRON的一个先行测试网,主网中待上线的新特性都会在Nile上先行发布。请点击Nile官方网站以了解更多详情。

部署合约需要质押或者燃烧TRX以支付能量,在此处获取一些测试币,每个地址限领一次。

已登录的TronLink会自动与TronIDE链接,请确保选择了正确的网络:

474

如果你是首次使用TronIDE,需要先激活两个模块:Solidity Compiler和DEPLOYMENT。

2848

激活后,最左边的菜单栏应该是这个样子的

2856

新建一个合约文件,Library.sol,然后把完整的合约代码粘贴进来

2876

本文的示例合约需要在Compiler处选择0.8.0+commit7c2e641来进行编译,编译成功后的合约就可以进行部署了。

需要在如图所示的地方填写合适的feelimit以保证能够成功部署,这里feelimit填写1000000000,更多关于feelimit请参考这里

2862

在弹出的TronLink对话框中,点击Accept为本次部署进行签名。

2862

当合约部署成功后,如图所示的位置会显示出本合约可以调用的方法

2836

构建DApp

首先将上一步部署的合约地址粘贴到 utils.js 中的 libraryContractAddress 变量中。

连接UI到TronLink

下一步我们需要将UI连接到TronLink Chrome 钱包, TronLink Chrome扩展程序会将TronWeb对象注入每个浏览器页面,使得 DApp 能与 TRON 网络进行交互。

在dapp-ui/plugins/utils.js中,创建如下函数来获取智能合约对象,并将其保存到全局变量中,接下来就可以直接使用全局变量来与合约交互了。

export async function setLibraryContract() {
     bookRentContract = await  window.tronWeb.contract().at('TMH1jAzAjCp2GdWm7hXSmhYyD3iKdpExoZ');
}

定义功能和函数

当我们的UI能够连接到TronLink之后,需要考虑UI如何跟智能合约进行交互。所以我们要创建一个合约对象,表示去中心化图书馆智能合约。

图书馆DApp需要支持三个主要功能:

  • 为图书馆添加书籍
  • 查看所有可借书籍
  • 借书

在index.vue中调用setLibraryContract()初始化合约对象。

async mounted() {
   // init contract object
   await setLibraryContract();
   // fetch all books
   const books = await fetchAllBooks();
   this.posts = books;
 },

添加书籍

首先创建添加书籍表单,用于用户发布书籍出租信息。在后端,它将与library合约的addBook函数交互。

在dapp-ui/components/bookForm.vue的postAd()函数中添加如下代码:

postAd() {
     // convert price from TRX to SUN
     postBookInfo(this.title,this.description,tronWeb.toSun(this.price));
}

在dapp-ui/plugins/utils.js的postBookInfo()中添加如下代码:

const result = await bookRentContract.addBook(name,description,price).send({
   feeLimit:100_000_000,
   callValue:0,
   shouldPollResponse:true
 });

查询所有可借书籍

通过fetchAllBooks()函数获取书籍列表,列出所有可借书籍。

在dapp-ui/plugins/utils.js的fetchAllBooks()函数中添加如下代码:

const books = [];
 
 const bookId  = await bookRentContract.bookId().call();
 //iterate from 0 till bookId
 for (let i = 0; i < bookId; i++){
   const book = await bookRentContract.books(i).call()
   if(book.name!="") // filter the deleted books
   {
     books.push(
       {id: i,name: book.name,description: book.description,price: tronWeb.fromSun(book.price)}
     )
   } 
 }
return books

在index.vue中调用fetchAllBooks()来获取书籍信息,并显示到主页上。

借阅书籍

用户查看书籍信息后,可以借阅这本书。
在dapp-ui/components/detailsModal.vue的book()函数中添加如下代码:

// get Start date
     const startDay = this.getDayOfYear(this.startDate)
     // get End date
     const endDay = this.getDayOfYear(this.endDate)
     // price calculation
     const totalPrice =tronWeb.toSun(this.propData.price) * (endDay - startDay)
     // call metamask.bookProperty
     borrowBook(this.propData.id, startDay, endDay, totalPrice)

dapp-ui/plugins/utils.js,在borrowBook()函数中添加如下代码:

const result = await bookRentContract.borrowBook(spaceId,checkInDate,checkOutDate).send({
   feeLimit:100_000_000,
   callValue:totalPrice,
   shouldPollResponse:true
 });

至此,图书馆DApp开发完毕。

运行DApp

确保tronLink为登录状态,然后执行如下命令来启动服务:

npm run dev

在浏览器地址栏输入:localhost:3000,查看前端页面。

936

点击右上角的”Rent Your Books”按钮,发布一条图书租赁信息。信息包括,图书名称,图书简要描述,图书借阅一天的价格。

974

信息填写完成后,点击”Submit”按钮,这些信息将发送给library合约的addBook函数,创建了一条触发合约的交易,然后将出现TronLink的弹框,要求确认并签名,如下所示:

974 974

交易成功上链后,这条租赁信息将显示到页面上:

974

点击”View”可查看书籍详细信息, 选择租赁的时间段, 然后点击”Lent Now”发起租赁请求,将触发library合约的borrowBook函数调用, 需要签名及广播,然后完成租赁交易。

974