TRON Logo

本篇主要分享在 TRON (波場) 部署智能合約 (Smart Contract) 並與之交互的經驗分享。

為讓初學者快速瞭解與跟著操作,所有的環境都在本機 (自建私鏈) 完成,操作完成後,也可以方便快速地刪除,回到本機乾淨的環境。

1. 快速搭建 TRON 私鏈

本機需要先行建置好 Docker 環境,相關安裝教學請上網查詢。

安裝完成後,按下列步驟,即可快速啟動 TRON 私鏈在本機上。容器名稱將設定為 tron,你可以按需求在下列指令中更換。若自行更換,之後所有的操作請自行對應修改,不會再多述。

$ docker pull trontools/quickstart
$ docker run -it -p 9090:9090 --name tron trontools/quickstart
or
$ docker run -it -p 9090:9090 --name tron -e "preapprove=multiSignFee:1,allowMultiSign:1" trontools/quickstart

需要注意的是,預設會自動建立 10 個錢包,並持有 10000 TRX 可供使用,這 10 個錢包會在執行完成時,顯示在畫面上,如圖:

TRON Wallets

之後若忘了這些錢包地址也沒關係,只要知道此容器的 IP 地址。我們隨時都可以在本機依下列指令再次顯示,範例的 IP 是 172.17.0.2。

$ curl "http://172.17.0.2:9090/admin/accounts?format=all"

於是 TRON 私鏈就成功在本機啟動。

$ docker ps
CONTAINER ID        IMAGE                  COMMAND                 CREATED             STATUS              PORTS                    NAMES
18006b7cff41        trontools/quickstart   "./quickstart v2.1.1"   9 hours ago         Up 9 hours          0.0.0.0:9090->9090/tcp   tron

我們也可以查詢容器內 TRON 的 Log。

$ docker exec -it tron tail -f /tron/FullNode/logs/tron.log

2. 快速搭建開發 TRON 智能合約的環境

為了把重點放在如何與智能合約交互,本篇僅會以簡單的兩個函式為範例 (set, get) 交互,而非比較複雜的 Token。其中的差異僅在於,之後只要在網路上或自行編寫 Token 合約,按此篇的操作亦可完成。

後續所有開發皆採用與本機環境隔開的 Ubuntu 容器。

$ docker pull ubuntu:18.04
$ docker run -d -it --name ubuntu ubuntu:18.04
$ docker exec -it ubuntu bash

智能合約 (Smart Contract) 的開發方式有很多種,這裡將介紹兩種。因為都需要 node 程式語言,所以請按下列操作安裝,或使用自有習慣的方式。

注意,接下來的操作步驟是在上述 Ubuntu 的容器內。之後若見到以 “docker$” 起頭為標示的指令操作,即代表為在 Ubuntu 容器內之操作。

docker$ apt update
docker$ apt install nodejs npm git-core vim-tiny
docker$ node -v
v8.10.0
docker$ npm -v
3.5.2
docker$ git --version
git version 2.17.1

Tronbox

Tronbox 是 TRON 官方提出的開發框架。以下為安裝步驟。

docker$ npm install -g tronbox

接著初始化智能合約。

docker$ mkdir -p $HOME/tron-contract-tronbox
docker$ cd $HOME/tron-contract-tronbox
docker$ tronbox init

3. 開始撰寫 TRON 智能合約

之後可能因為版本更新的因素,請各位再自行調整。

我們需要建立、更新下述兩個指定的檔案。

docker$ cd $HOME/tron-contract-tronbox
docker$ cat contracts/MyContract.sol

pragma solidity >=0.4.23 <0.6.0;
contract MyContract {

    string message = "hello";

    function postMessage(string memory value) public returns (string memory) {
        message = value;
        return message;
    }

    function getMessage() public view returns (string memory) {
        return message;
    }
}
docker$ cd $HOME/tron-contract-tronbox
docker$ cat migrations/2_deploy_contracts.js

var MyContract = artifacts.require("./MyContract.sol");
var Migrations = artifacts.require("./Migrations.sol");

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

接著將 tronbox.js 檔案中 development 區段的 fullHost,指到我們的私鏈位置。

至於如何得知之前運行的 TRON 私鏈的 IP 位址,可以再自行上網查詢。範例是 172.17.0.2。

docker$ cat tronbox.js

...
    development: {
      // For trontools/quickstart docker image
      privateKey: 'da146374a75310b9666e834ee4ad0866d6f4035967bfc76217c5a495fff9f0d0',
      userFeePercentage: 0,
      feeLimit: 1e8,
      fullHost: 'http://172.17.0.2:' + port,
      network_id: '9'
    },
...

另外,Solidity 的 version 也需要指定。

docker$ cat tronbox.js

...
    compilers: {
      solc: {
        version: '0.5.4'
      }
    }

...

開始編譯智能合約。

docker$ cd $HOME/tron-contract-tronbox
docker$ tronbox compile --compile-all
docker$ tronbox migrate --reset

成功時,會輸出智能合約的地址,請務必記得其中我們編寫的 MyContract 地址。

TRON MyContract Address

4. 試著與 TRON 智能合約交互前,我們需要知其 ABI

在撰寫智能合約交互之前,我們需要取得先前部署在 TRON 私鏈上該智能合約的 ABI。

方法有兩種。

Method 1 : Solidity Compiler (solc)

安裝步驟。

docker$ npm install -g solc

接著初始化智能合約目錄結構。

docker$ mkdir -p $HOME/tron-contract-solc
docker$ mkdir -p $HOME/tron-contract-solc/build

把先前智能合約的程式碼再寫一次。

docker$ cd $HOME/tron-contract-solc
docker$ cat MyContract.sol

pragma solidity >=0.4.23 <0.6.0;
contract MyContract {
    string message = "hello";
    function postMessage(string memory value) public returns (string memory) {
        message = value;
        return message;
    }
    function getMessage() public view returns (string memory) {
        return message;
    }
}

編譯及部署智能合約。

docker$ solcjs MyContract.sol -o build --bin --abi --opcodes

取得編譯出來的 ABI。

docker$ cat build/MyContract_sol_MyContract.abi
[{"inputs":[],"name":"getMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"postMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"nonpayable","type":"function"}]

Method 2 : TronWeb

利用 TronWeb 從私鏈上獲取智能合約的 ABI。

$ docker exec -it tron ./tronWeb

然後將之前智能合約的地址,取代下述指令中的 OOOOO。

> tronWeb.contract().at("OOOOO").then(function(v){console.log(JSON.stringify(v.abi));});

> .exit

例如我先前圖中的範例,MyContract 智能合約的 base58 地址是 “TARcTNcPPw5yrKCYZZryfdwuTGZoSEcAno”。

> tronWeb.contract().at("TARcTNcPPw5yrKCYZZryfdwuTGZoSEcAno").then(function(v){console.log(JSON.stringify(v.abi));});
[{"outputs":[{"type":"string"}],"inputs":[{"name":"value","type":"string"}],"name":"postMessage","stateMutability":"Nonpayable","type":"Function"},{"outputs":[{"type":"string"}],"constant":true,"name":"getMessage","stateMutability":"View","type":"Function"}]

> .exit

此時輸出的就是 MyContract 的 ABI。

5. 試著與 TRON 智能合約交互

這裡採用的函式庫是 TRON-API,是非官方社群維護的 PHP 函式庫。目前最後的修改日是 2019-11-15,已歷時半年以上未更新,參照文件或範例運作時,會有幾處需要調整,而我已經踩過幾個坑,只要按照我的操作即可。

首先,安裝 PHP 及 Composer。

docker$ apt install php php-gmp php-bcmath composer

初始化智能合約目錄結構。

docker$ mkdir -p $HOME/tron-api-php
docker$ cd $HOME/tron-api-php
docker$ composer require iexbase/tron-api

在正式與智能合約交互之前,我們需要以下資料。

  1. MyContract 智能合約的地址。
  2. MyContract 智能合約的 ABI。
  3. TRON (私)鏈的服務器地址。
  4. TRON (私)鏈的指定錢包地址。
  5. TRON (私)鏈的指定錢包私鑰。

我們在前述的操作中已知 1, 2, 3,而 4 及 5,可利用先前提到的指令來從我們自建的 TRON 私鏈取得。

$ curl "http://172.17.0.2:9090/admin/accounts?format=all"

TRON Logo

我們將使用其中的第一個錢包。即,

address = 414ee854e425347aa6c4a319330e17d2b507cdce6c
privacy_key = c72092f1a156efbc46e48683bef4d64c1005f97d668248e3e7a1411237bb82c1

於是,把上述五項資料,填寫於設定檔中。

docker$ cd $HOME/tron-api-php
docker$ cat config.php
<?php

$tron_host            = 'http://172.17.0.2:9090';
$sun_limit            = 1000000000;
$main_address         = '414ee854e425347aa6c4a319330e17d2b507cdce6c';
$main_privacy_key     = 'c72092f1a156efbc46e48683bef4d64c1005f97d668248e3e7a1411237bb82c1';
$contract_hex_address = '4104fe64a68fc938555092d88d441e8b883f684004';
$contract_abi = '[{"outputs":[{"type":"string"}],"inputs":[{"name":"value","type":"string"}],"name":"postMessage","stateMutability":"Nonpayable","type":"Function"},{"outputs":[{"type":"string"}],"constant":true,"name":"getMessage","stateMutability":"View","type":"Function"}]';

$tron_host = 'http://172.17.0.2:9090';
$sun_limit = 1000000000;

注意,contract_hex_address 必須使用智能合約的 hex 地址而不是 base58。如下圖,

TRON MyContract Address

另外,本文前述提供兩種取得智能合約 ABI 的方法,若是使用 Method 1 : Solidity Compiler (solc) 方法取得的,通常沒有問題,只需直接複製到 $contract_abi 變數後即可,如:

docker$ cd $HOME/tron-api-php
docker$ cat config.php
<?php

$tron_host            = 'http://172.17.0.2:9090';
$sun_limit            = 1000000000;
$main_address         = '414ee854e425347aa6c4a319330e17d2b507cdce6c';
$main_privacy_key     = 'c72092f1a156efbc46e48683bef4d64c1005f97d668248e3e7a1411237bb82c1';
$contract_hex_address = '4104fe64a68fc938555092d88d441e8b883f684004';
$contract_abi = '[{"inputs":[],"name":"getMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"postMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"nonpayable","type":"function"}]';

但若是使用 Method 2 : TronWeb 方法取得時,在我們使用 TRON-API 時,會需要調整一下。

例如在本例中,TronWeb 方法會得到,

[{"outputs":[{"type":"string"}],"inputs":[{"name":"value","type":"string"}],"name":"postMessage","stateMutability":"Nonpayable","type":"Function"},{"outputs":[{"type":"string"}],"constant":true,"name":"getMessage","stateMutability":"View","type":"Function"}]

之後執行,會遇到如下述的錯誤。

TRON API Warning

PHP Notice:  Undefined index: inputs in /root/tron-api-php/vendor/iexbase/tron-api/src/TransactionBuilder.php on line 432
PHP Warning:  count(): Parameter must be an array or an object that implements Countable in /root/tron-api-php/vendor/iexbase/tron-api/src/TransactionBuilder.php on line 432
PHP Notice:  Undefined index: inputs in /root/tron-api-php/vendor/iexbase/tron-api/src/TransactionBuilder.php on line 439
PHP Warning:  array_map(): Argument #2 should be an array in /root/tron-api-php/vendor/iexbase/tron-api/src/TransactionBuilder.php on line 439
PHP Warning:  count(): Parameter must be an array or an object that implements Countable in /root/tron-api-php/vendor/iexbase/tron-api/src/TransactionBuilder.php on line 441
PHP Fatal error:  Uncaught InvalidArgumentException: encodeParameters number of types must equal to number of params. in /root/tron-api-php/vendor/sc0vu/web3.php/src/Contracts/Ethabi.php:154
Stack trace:
#0 /root/tron-api-php/vendor/iexbase/tron-api/src/TransactionBuilder.php(454): Web3\Contracts\Ethabi->encodeParameters(Array, Array)
#1 /root/tron-api-php/demo.php(31): IEXBase\TronAPI\TransactionBuilder->triggerSmartContract(Array, '4104fe64a68fc93...', 'getMessage', Array, 1000000000, '414ee854e425347...')
#2 {main}
  thrown in /root/tron-api-php/vendor/sc0vu/web3.php/src/Contracts/Ethabi.php on line 154

我們要要調整後才可使用,例如此例是需要把 ““inputs”:[]“,加到 “getMessage” 中。

由原本 TronWeb 取得的,

[{"outputs":[{"type":"string"}],"inputs":[{"name":"value","type":"string"}],"name":"postMessage","stateMutability":"Nonpayable","type":"Function"},{"outputs":[{"type":"string"}],"constant":true,"name":"getMessage","stateMutability":"View","type":"Function"}]

調整為,

[{"outputs":[{"type":"string"}],"inputs":[{"name":"value","type":"string"}],"name":"postMessage","stateMutability":"Nonpayable","type":"Function"},{"outputs":[{"type":"string"}],"inputs":[],"constant":true,"name":"getMessage","stateMutability":"View","type":"Function"}]

最後得到的 config.php 為,

docker$ cat config.php
<?php

$tron_host            = 'http://172.17.0.2:9090';
$sun_limit            = 1000000000;
$main_address         = '414ee854e425347aa6c4a319330e17d2b507cdce6c';
$main_privacy_key     = 'c72092f1a156efbc46e48683bef4d64c1005f97d668248e3e7a1411237bb82c1';
$contract_hex_address = '4104fe64a68fc938555092d88d441e8b883f684004';

# Get from Solidity Compiler (solc)
$contract_abi = '[{"inputs":[],"name":"getMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"postMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"nonpayable","type":"function"}]';

# Modified from TronWeb
// $contract_abi = '[{"outputs":[{"type":"string"}],"inputs":[{"name":"value","type":"string"}],"name":"postMessage","stateMutability":"Nonpayable","type":"Function"},{"outputs":[{"type":"string"}],"inputs":[],"constant":true,"name":"getMessage","stateMutability":"View","type":"Function"}]';

然後是主要交互的 PHP 程式碼。

TRON-API 官方提供的文件或範例之差異是,現在發送給 TRON Web API 的請求,必須包含 ‘Content-Type: application/json’ 的 Header,否則照官方網站的文件與範本是無法正常運行的。

docker$ cd $HOME/tron-api-php
docker$ cat demo.php
<?php
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/config.php';

use IEXBase\TronAPI\Tron;

$fullNode = new \IEXBase\TronAPI\Provider\HttpProvider($tron_host, 30000, false, false, ['Content-Type' => 'application/json']);
$solidityNode = new \IEXBase\TronAPI\Provider\HttpProvider($tron_host, 30000, false, false, ['Content-Type' => 'application/json']);
$eventServer = new \IEXBase\TronAPI\Provider\HttpProvider($tron_host, 30000, false, false, ['Content-Type' => 'application/json']);

try {
    $tron = new \IEXBase\TronAPI\Tron($fullNode, $solidityNode, $eventServer);
} catch (\IEXBase\TronAPI\Exception\TronException $e) {
    exit($e->getMessage());
}

$tron->setAddress($main_address);
$balance = $tron->getBalance(null, true);
$tron->setPrivateKey($main_privacy_key);
var_dump($balance);

$transaction_builder = $tron->getTransactionBuilder();

$abi = json_decode($contract_abi, true);

$response = $transaction_builder->triggerSmartContract($abi, $contract_hex_address, "getMessage", [], $sun_limit, $main_address);
var_dump($response);

$transaction = $transaction_builder->triggerSmartContract($abi, $contract_hex_address, "postMessage", ["ant_is_awesome"], $sun_limit, $main_address);
$signedTransaction = $tron->signTransaction($transaction);
$response = $tron->sendRawTransaction($signedTransaction);
var_dump($response);

$response = $transaction_builder->triggerSmartContract($abi, $contract_hex_address, "getMessage", [], $sun_limit, $main_address);
var_dump($response);

最後執行時,就會取得我們要的結果。

docker$ php demo.php
float(9918.07856)
array(1) {
  [0]=>
  string(5) "hello"
}
array(1) {
  ["result"]=>
  bool(true)
}
array(1) {
  [0]=>
  string(14) "ant_is_awesome"
}

另外,若需要取得某次交易的狀況,可以取代下述指令中的 OOOOO,換為 TRON (私)鏈上的 TxID。

// parameter , TxID (transaction id)
$response = $tron->getTransaction('OOOOO');
var_dump($response);

例如我想要取得 TxID 為 “b73e42ff5da9f9083fcff340678a1a1ce09533b56a6e795b5cdc1cfcf98e1356” 的紀錄時,

// parameter , TxID (transaction id)
$response = $tron->getTransaction('b73e42ff5da9f9083fcff340678a1a1ce09533b56a6e795b5cdc1cfcf98e1356');
var_dump($response);

6. 清理、銷毀 TRON (波場) 開發環境

因為開發都是採用容器化的方式,所以僅需要下列步驟即可恢復回原本乾淨的本機環境。

$ docker stop ubuntu
$ docker rm   ubuntu
$ docker rmi  ubuntu:18.04

$ docker stop tron
$ docker rm   tron
$ docker rmi  trontools/quickstart