Xây dựng ứng dụng phi tập trung (Dapp) với Cocos Creator

Mở đầu

Trong thời đại blockchain đang rất phát triển, rất nhiều những ứng dụng truyền thống đang dần chuyển dịch sang hướng phi tập trung.

Những ứng dụng này được gọi là decentralized application, hay gọi tắt là dapp.

Dapp hiện giờ hầu hết được xây dựng dưới dạng web app, bởi môi trường và các công cụ tương tác đã được hoàn thiện khá tốt, ví dụ như metamask, hay các dapp browser.

Về mặt lý thuyết, ta cũng hoàn toàn có thể xây dựng được các dapp trên mobile, song hiện giờ vẫn còn một rào cản khá lớn khi các app store truyền thống như google play hay app store chưa chấp nhận các app như vậy. Một phần vì lý do bảo mật, một phần cũng vì sẽ ảnh hưởng đến hệ thống in-app purchase của các store đó.

Hi vọng trong tương lai với sự hoàn thiện hơn của blockchain thì các store sẽ có các cơ chế thông thoáng hơn với dapp.

Để xây dựng dapp chúng ta có thể sử dụng javascript thuần, hay reactjs, hay vuejs, hay Unity đều được, các bạn có thể tham khảo thêm các bài viết sau đây:

Trong bài viết này mình sẽ hướng dẫn các bạn xây dựng một web dapp trên nển tảng ethereum bằng một engine game khá nổi tiếng Cocos Creator. Các bạn hoàn toàn có thể mở rộng nó để làm các web game tích hợp ethereum cho mình.

Full Code: https://github.com/dotrungkien/eth-cocos-code-base

Note: Môi trường làm việc trong bài viết này là MacOS, tuy nhiên vẫn có thể chạy trên Window bình thường.

Cài đặt

Chúng ta cần cài đặt truffle để code contract, Cocos Creator để code một sample app đơn giản, và Metamask để tương tác với web dapp.

truffle

npm install -g truffle

Cocos Creator

Download và cài đặt tại đây: https://www.cocos.com/en/creator

Metamask

Cài đặt chrome extension tại đây: https://chrome.google.com/webstore/search/metamask?hl=en-US

Init project

Ứng dụng của chúng ta sẽ có 2 phần, phần contract và phần game, ta sẽ lần lượt tạo từng folder:

1 - Tạo thư mục gốc của ứng dụng:

mkdir hello-world && cd hello-world

2 - tạo thư mục contract:

mkdir contract && cd contract

3 - init truffle project

truffle init

khi này chúng ta sẽ có được một project để bắt đầu code contract rồi.

4 - init game project

Ta sẽ khởi động Cocos Creator lên và tạo một project mới, để project đơn giản và dễ hiểu nhất ta sẽ tạo project theo template Hello-world

OK ta đã sẵn sang để vào bước phát triển rồi.

Contract Development

Trong bài này ta sẽ làm một contract vô cùng đơn giản, chỉ có 2 phương thức và 1 biến mà thôi.

Contract

Vào thư mục contracts và tạo một file mới SimpleStore.sol với nội dung như sau:

pragma solidity ^0.5.0;

contract SimpleStore {
  uint256 value;

  event NewValueSet(uint256 indexed _value, address _sender);

  function set(uint256 newValue) public {
    value = newValue;
    emit NewValueSet(value, msg.sender);
  }

  function get() public view returns (uint256) {
    return value;
  }
}

ở đây ta sẽ dùng phiên bản solidity 0.5.0.

Truffle config

tại truffle-config.js ta sẽ config tất cả những thông tin về chain, compiler, accounts...

module.exports = {
  contracts_build_directory: '../game/assets/Contracts',
  networks: {
    development: {
      host: '127.0.0.1',
      port: 8545,
      network_id: '123456789'
    }
  }
};

Ta sẽ build contracts vào luôn một thư mục bên trong thư mục game, để chút nữa ta có thể sử dụng nó trong Cocos.

Với chain, để đơn giản nhất ta sẽ sử dụng một local chain, chạy bởi ganache-cli.

Chạy local chain

Nếu bạn chưa cài đặt ganache-cli thì hãy tiến hành cài đặt trước.

npm install -g ganache-cli

Ta sẽ chạy một local chain với chain id là 123456789 bằng lệnh:

ganache-cli -i 123456789

Khi này đã có một chain chạy tại địa chỉ http://localhost:8545 và 10 account, mỗi account có 100 ETH như thế này, mỗi account tương ứng với một private key bên dưới.

Available Accounts
==================
(0) 0x7c28281fb4fa65b40e3f47af4f88bfe82224090b
(1) 0x02c5c736031b4fa576cb911ddbb3e55eb741f134
(2) 0x905aef31f11b49923a6c377f83dec84567b90a25
(3) 0x3429059ea3d218c43266998cfed14e7f85f8d372
(4) 0x0ae76d77a96b34aff16f8910078ae2cf4e001768
(5) 0x148ffe781de6676fde20c2c6ff5bb022e80a9748
(6) 0x776be00b63a9d8692c1a4ce4d6baec1101fc913e
(7) 0x977eded9ac70b067c7cb840779ea9f46e8a89afb
(8) 0x01452fe9da1362ab8ace90d87056a4e5cfc22766
(9) 0xb422c3fafc0514cb733f83294d1681d6e3e8fc03

Private Keys
==================
(0) 26b3f59a6fec532ffc45f121bd2ba3c088666a34136669df03b315e938330d58
(1) 57adbbb3ba0aa454b3b85a87a6fc73e5ded11748e9323d0ba3866dca40e2e301
(2) db4093821ee1f362897847ac59851adea4a4aeeba22404321978111a02236174
(3) 3e953bb74802810739dec2df4cf2eb0e36d6ac665836a3c5658e35e4ef64814b
(4) 3ad55f2e89c7fe489a0cc04dd3574df69466656a98e92be78c9599a442370333
(5) ccc5585cbcbb0405fa5ea2aa48775e6ce7f75b819477c51396084b0b5ec3120d
(6) 77cab936b6001ad056cace50db94bacbec066fdf8842dbe598d7f656a18ec620
(7) 718159194d52ca85fad5dcaac0e9a9269994b6e3301cb233e107b4a71403d172
(8) debf1a6e65da5b3314de06dcefb6fec85065c9c74a42a8660a61d6786b322ba7
(9) 4c97648a754569ef7cfbfafeeb3df4213eea2fb8f1ced9ff38ade25e1ad688c5

hãy lưu lại private key đầu tiên để chút nữa ta sẽ sử dụng account tương ứng trong metamask.

Compile và Migration

Ta sẽ tiến hành compile contract để đảm bảo rằng contract của chúng ta không có lỗi:

truffle compile

kết quả

> Compiling ./contracts/Migrations.sol...
> Compiling ./contracts/SimpleStore.sol...
> Writing artifacts to ./../game/assets/Contracts

Để migration contract của chúng ta lên local chain, ta sẽ phải tạo thêm một file migration cho nó.

Trong thư mục migrations tạo thêm file 2_simple_store_migration.js với nội dung như sau:

const SimpleStore = artifacts.require('SimpleStore');

module.exports = function(deployer) {
  deployer.deploy(SimpleStore);
};

Và tiến hành migrate

truffle migrate --network development

kết quả


Starting migrations...
======================
> Network name:    'development'
> Network id:      123456789
> Block gas limit: 6721975


1_initial_migration.js
======================

   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0xaa2af826be86a1561c79bbff42c1c8646921e327609995b486c0b71eb920edc4
   > Blocks: 0            Seconds: 0
   > contract address:    0x6E896215aD8E6c9d335AAc6642747Cfa96b88511
   > account:             0x7c28281Fb4FA65B40E3f47af4F88BFe82224090b
   > balance:             99.99430184
   > gas used:            284908
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00569816 ETH

   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00569816 ETH


2_simple_store_migration.js
===========================

   Deploying 'SimpleStore'
   -----------------------
   > transaction hash:    0x70d5bfd2b6f35bced155e93422f7f73a04f475f604e158b766421690ee730882
   > Blocks: 0            Seconds: 0
   > contract address:    0xaC78cEE4872Dd65945a22C66DFCB2d55a9cc65DC
   > account:             0x7c28281Fb4FA65B40E3f47af4F88BFe82224090b
   > balance:             99.99139726
   > gas used:            145229
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00290458 ETH

   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00290458 ETH


Summary
=======
> Total deployments:   2
> Final cost:          0.00860274 ETH

OK vậy là xong phần contract.

Game Development

Hãy mở Cocos Creator và chọn project game mà chúng ta vừa tạo.

Scene

Mở đầu chúng ta sẽ có một scene helloworld với chỉ có một background, một logo và một label.

Ta sẽ tiến hành xóa logo, thêm một label cho Address, một label cho Balance, một inputBox cho giá trị value trong contract, và một button để set giá trị cho value.

Scene sẽ trở thành như sau:

Script

Để kết nối với blockchain từ game, ta cần phải sử dụng thư viện Web3js, trong bài viết này chúng ta sử dụng phiên bản 1.0.0-beta36.

Download tại đây https://cdn.jsdelivr.net/gh/ethereum/[email protected]/dist/web3.min.js.

Tạo một thư mục Library trong thư mục game và đưa web3.min.js vào đây. Thư mục game của chúng ta sẽ trông như sau:

click vào web3.min.js và đảm bảo rằng checkbox import as plugin không được check.

OK, giờ ta sẽ chỉnh sửa script Hello World như sau:

const Web3 = require('web3.min');
const GAS_PRICE_DEFAULT = '20000000000';

cc.Class({
  extends: cc.Component,

  properties: {
    currentValueLabel: {
      default: null,
      type: cc.Label
    },
    addressLabel: {
      default: null,
      type: cc.Label
    },
    balanceLabel: {
      default: null,
      type: cc.Label
    },
    inputBox: cc.EditBox,
    contractABI: cc.JsonAsset
  },

  onLoad: function() {
    this.web3 = null;
    this.web3Provider = null;
    this.web3ProviderName = 'metamask';
    this.contract = null;
    this.currentValue = 0;
    this.address = '0x';
    this.balance = 0;
    this.addressLabel.string = 'Address: ' + this.address;
    this.balanceLabel.string = 'Balance: ' + this.currentValue;

    this.updateCurrentValue(0);

    this.initWeb3();
  },

  initWeb3() {
    const isWeb3Enabled = () => !!window.web3;
    if (isWeb3Enabled()) {
      this.web3 = new Web3();

      //Request account access for modern dapp browsers
      if (window.ethereum) {
        this.web3Provider = window.ethereum;
        this.web3.setProvider(this.web3Provider);
        window.ethereum
          .enable()
          .then(accounts => {
            this.initAccount();
          })
          .catch(error => {
            console.error(error);
          });
      }
      //Request account access for legacy dapp browsers
      else if (window.web3) {
        this.web3Provider = window.web3.currentProvider;
        this.web3.setProvider(this.web3Provider);

        this.initAccount();
      }
    } else {
      console.error('You must enable and login into your Wallet or MetaMask accounts!');
    }
  },
  initAccount() {
    this.web3.eth.getAccounts().then(accounts => {
      if (accounts.length > 0) {
        this.address = accounts[0].toLowerCase();
        this.addressLabel.string = 'Address: ' + this.address;
        this.initContract();
        this.updateBalance();
      } else console.error('You must enable and login into your Wallet or MetaMask accounts!');
    });
  },

  initContract() {
    let networks = { main: '1', ropsten: '3', kovan: '42', rinkeby: '4' };
    this.web3.eth.net.getNetworkType().then(netId => {
      this.contract = new this.web3.eth.Contract(
        this.contractABI.json.abi,
        // this.contractABI.json.networks[networks[netId]].address
        this.contractABI.json.networks['123456789'].address
      );
    });
  },

  updateBalance() {
    this.web3.eth.getBalance(this.address, (err, balance) => {
      if (err) {
        console.error(err);
        return;
      }
      this.balanceLabel.string = cc.js.formatStr(
        'Balance: %d ETH',
        parseInt(this.web3.utils.fromWei(balance))
      );
    });
  },

  getValue() {
    this.contract.methods
      .get()
      .call({
        from: this.address
      })
      .then(val => {
        this.updateCurrentValue(val);
        // console.log('get current value: ', val);
      });
  },

  setValue() {
    this.contract.methods
      .set(this.inputBox.string)
      .send({
        from: this.address,
        gas: 250000,
        gasPrice: GAS_PRICE_DEFAULT
      })
      .on('transactionHash', hash => {
        // console.log('transactionHash: ', hash);
      })
      .on('receipt', receipt => {
        this.getValue();
      })
      .on('error', error => {
        console.error('endgame error: ', error);
      });
  },

  updateCurrentValue(value) {
    this.currentValueLabel.string = 'Current Value: ' + value;
  }
});

Giải thích:

  • khai báo các label và input box ta vừa định nghĩa bên trên, ở đây đáng chú ý là ta sẽ có một biến contractABI, đây chính là contract mà ta đã build bằng lệnh truffle migrate --network development ở bên trên.
contractABI: cc.JsonAsset;
  • tại onLoad, ta sẽ tiến hành khởi tạo các thuộc tính cần thiết, cũng như web3
onLoad: function() {
  this.web3 = null;
  this.web3Provider = null;
  this.web3ProviderName = 'metamask';
  this.contract = null;
  this.currentValue = 0;
  this.address = '0x';
  this.balance = 0;
  this.addressLabel.string = 'Address: ' + this.address;
  this.balanceLabel.string = 'Balance: ' + this.currentValue;
  this.updateCurrentValue(0);
  this.initWeb3();
},

  • Tại initWeb3, ta sẽ detect network ta đang dùng hiện tại và tạo ra một web3 instance
if (window.ethereum) {
  this.web3Provider = window.ethereum;
  this.web3.setProvider(this.web3Provider);
  window.ethereum
    .enable()
    .then(accounts => {
      this.initAccount();
    })
    .catch(error => {
      console.error(error);
    });
} else if (window.web3) {
  this.web3Provider = window.web3.currentProvider;
  this.web3.setProvider(this.web3Provider);

  this.initAccount();
}
  • ta sẽ lấy account đầu tiên trong số các account của ganache-cli và update balance cho nó, đồng thời tạo một instance của contract SimpleStorage chúng ta đã code trên kia.
initAccount() {
  this.web3.eth.getAccounts().then(accounts => {
    if (accounts.length > 0) {
      this.address = accounts[0].toLowerCase();
      this.addressLabel.string = 'Address: ' + this.address;
      this.initContract();
      this.updateBalance();
    } else console.error('You must enable and login into your Wallet or MetaMask accounts!');
  });
},

initContract() {
  let networks = { main: '1', ropsten: '3', kovan: '42', rinkeby: '4' };
  this.web3.eth.net.getNetworkType().then(netId => {
    this.contract = new this.web3.eth.Contract(
      this.contractABI.json.abi,
      // this.contractABI.json.networks[networks[netId]].address
      this.contractABI.json.networks['123456789'].address
    );
  });
},

updateBalance() {
  this.web3.eth.getBalance(this.address, (err, balance) => {
    if (err) {
      console.error(err);
      return;
    }
    this.balanceLabel.string = cc.js.formatStr(
      'Balance: %d ETH',
      parseInt(this.web3.utils.fromWei(balance))
    );
  });
},
  • Cuối cùng ta sẽ cài đặt 2 hàm setValuegetValue tương ứng với hai hàm setget trong SimpleStorage contract. Lưu ý rằng Cocos Creator chưa support async/await nên buộc ta phải xử lý các bất đồng bộ bằng cách wrap và chain promise.
getValue() {
  this.contract.methods
    .get()
    .call({
      from: this.address
    })
    .then(val => {
      this.updateCurrentValue(val);
      // console.log('get current value: ', val);
    });
},

setValue() {
  this.contract.methods
    .set(this.inputBox.string)
    .send({
      from: this.address,
      gas: 250000,
      gasPrice: GAS_PRICE_DEFAULT
    })
    .on('transactionHash', hash => {
      // console.log('transactionHash: ', hash);
    })
    .on('receipt', receipt => {
      this.getValue();
    })
    .on('error', error => {
      console.error('endgame error: ', error);
    });
},

ok script đã sẵn sàng, kết nối đã sẵn sàng, quay trở lại scene, kéo các biến cần thiết từ scene vào trong script HelloWorld:

Test game

Giờ là lúc play, nhớ chọn Browser và nhấn Play, ta sẽ tới địa chỉ http://localhost:7456/.

Khi này ta cần check lại network đã setup ở Metamask xem đã đúng local chưa. Nếu chưa đúng ta sẽ phải thêm một Custom RPC như sau:

Sau đó tiến hành import account:

Sau đó ta sẽ nhập vào private key đầu tiên ở bên trên khi ta chạy ganache-cli lúc nãy

Ok, mọi thứ đã hoàn tất, khi này scene của chúng ta sẽ trông như sau:

set thử một giá trị xem sao, ví dụ 123, khi này metamask sẽ hiện ra popup như sau:

Confirm và chờ một chút, ta sẽ thấy transaction hoàn thành, và giá trị của chúng ta đã được update thành 123:

All done!

Kết luận

Vậy là chúng ta đã hoàn thành một ứng dụng vô cùng đơn giản với Cocos Creator tích hợp với Ethereum Blockchain rồi.

Dù chức năng chỉ là minimum nhưng ta hoàn toàn có rất nhiều hướng để phát triển. Cocos vốn là một engine nổi tiếng để làm game, do đó ta có thể xây dựng được các game trên đó rất dễ dàng.

Đồng thời ngoài localchain, bằng cách tương tự, ta cũng có thể deploy và kết nối đến các chain khác như Main chain, Ropsten, Rinkeby, Kovan, Loom, Tomo...

Hi vọng chúng ta đã có những trải nghiệm tích cực 😄

Enjoy coding!