Tạo đồng tiền ảo bằng Z.com Cloud Blockchain
Trong bài viết này tôi sẽ giải thích cách sử dụng Z.com Cloud BlockChain hay là Conoha BlockChain. Mặc dù có hướng dẫn công khai được cũng cấp bởi Z.com nhưng chỉ là những ví dụ làm sao để tạo Contract . Còn để làm 1 ứng dụng thực tế thì phải làm thế nào thì có lẽ thật khó để tưởng tượng ra được. Chính vì vậy trong bài viết này sẽ giải thích code làm thế nào để tạo ra 1 ứng dựng thực tiễn.
Trong bài viết này mình không giải thích tổng quan về Smart Contract, và cách deploy Smart Contract lên Zcom BlockChain, vì nó đã được mô tả trong hướng dẫn của Z.com ở link bên duới, các bạn có thể tham khảo . Bài viết dành cho những ai đang sử dụng dịch vụ của Z.com BlockChain, có thể sẽ khó hiểu cho những ai không sử dụng dịch vụ này !!!
https://guide.blockchain.z.com/en/
Tóm lược về ứng dụng :
Ví dụ về các ứng dụng được phát triển dựa trên nền tảng BlockChain thì đồng tiền ảo là một đại diện tiêu biểu cho các ứng dụng đó. Trong bài biết này tối trình bày cách tạo ra đồng tiền ảo . Các chức năng của ứng dụng gồm có kiểm tra số tiền sở hữu và chức năng chuyển tiền . Ta dùng ngôn ngữ Solidity để tạo SmartContract và HTML + JavaScript để tạo ứng dụng web .
Smart Contract :
Smart Contract mà sử dụng Z.com Cloud Blockchain thì sẽ được cấu thành từ 4 Contract :
- Contract interface sẽ là MainContract .
- Business logic sẽ là LogicContract .
- Việc ghi log sẽ là EventContract .
- Ghi và lưu dữ liệu sẽ là TableContract .
Lần này ứng dụng tạo đồng tiền ảo sẽ có tên là MyCoin .
Mỗi quan hệ của các Contract sẽ giống như sơ đồ bên dưới.
Main Contract
Main Contract sẽ là MyCoinContract_v1.sol ở phía dưới .
pragma solidity ^0.4.2;
import '../../gmo/contracts/VersionContract.sol';
import './MyCoinLogic_v1.sol';
contract MyCoinContract_v1 is VersionContract {
uint constant TIMESTAMP_RANGE = 60;
MyCoinLogic_v1 public logic_v1;
function MyCoinContract_v1(ContractNameService _cns, MyCoinLogic_v1 _logic) VersionContract(_cns, 'MyCoin') {
logic_v1 = _logic;
}
function setMyCoinLogic_v1(MyCoinLogic_v1 _logic) onlyByProvider {
logic_v1 = _logic;
}
function checkTimestamp(uint _timestamp) private constant returns(bool) {
return (now - TIMESTAMP_RANGE < _timestamp && _timestamp < now + TIMESTAMP_RANGE);
}
function exist(address _account) constant returns (bool) {
return logic_v1.exist(_account);
}
function send(bytes _sign, uint _timestamp, address _to, uint _value) {
if (!checkTimestamp(_timestamp)) throw;
bytes32 hash = calcEnvHash('send');
hash = sha3(hash, _timestamp);
hash = sha3(hash, _to);
hash = sha3(hash, _value);
address sender = recoverAddress(hash, _sign);
logic_v1.send(sender, _to, _value);
}
function getAmount(address _account) constant returns (uint) {
return logic_v1.getAmount(_account);
}
}
Kế thừa VersionContract :
Main Contract thì cần phải kế thừa VersionContract . Ngoài ra ta cũng cần phải truyền CNS và tên Contract như là tham số cho Contructor của VersionContract.
Thiết lập Logic Contract Interface :
MainContract là list các Contract Interface , vì có nhiệm vụ gửi request đến Business Logic nên cần phải có Logic Contract Interface . Vì thế ta thiết lập Logic Contract trong Contructor . Ngoài ra, để thay đổi Logic Contract do fix bug chẳng hạn , ta sử dụng setter của Logic Contract . Lúc này, bằng cách sử dụng onlyProvider thì chỉ có tài khoản mà đã deloy Contract này thì mới có thể thay đổi được .
Lấy địa chỉ của end user :
Khi sử dụng Z.com Cloud BlockChain cho chức năng thanh toán thì ta không thể sử dụng msg.sender để lấy địa chỉ của end user . Chính vì vậy để lấy được địa chỉ của end user thì cần phải đưa vào trong tham số của Transaction cái sign mà end user đã ký kết . Tham số đầu tiên của Transaction chính là sign của end user. Sử dụng sign này ta có thể xây dựng lại được địa chỉ của end user . Để có thế xây dựng lại thìta cần phải có giá trị băm (hash) của tham số , và ta dùng calcEncHash để làm điều đó .
Kiểm tra Timestamp :
Nếu trong sign của end user không có timestamp , thì có thể xảy ra việc nhiều lần gửi cùng 1 request với những tham số giống nhau . Để phòng ngừa điều đó , ta đưa timestamp vào trong tham số của request và so sánh thời gian chạy Transaction để loại trừ được vấn để trên .
Logic Contract
Logic Contract sẽ là MyCoinLogic_v1.sol ở phía dưới .
pragma solidity ^0.4.2;
import '../../gmo/contracts/VersionLogic.sol';
import '../../gmo/contracts/AddressGroup_v1.sol';
import './MyCoinAccountTable_v1.sol';
import './MyCoinEvent_v1.sol';
contract MyCoinLogic_v1 is VersionLogic {
uint constant TOTAL_AMOUNT = 1000000000;
bool public initDone = true;
MyCoinAccountTable_v1 public myCoinTable_v1;
MyCoinEvent_v1 public event_v1;
modifier onlyFirst() {
if (!initDone) throw;
_;
}
modifier sendable(address _sender, uint _value) {
uint amount = getAmount(_sender);
if (amount < _value) throw;
_;
}
function MyCoinLogic_v1(ContractNameService _cns, MyCoinAccountTable_v1 _myCoinTable, MyCoinEvent_v1 _event) VersionLogic(_cns, 'MyCoin') {
myCoinTable_v1 = _myCoinTable;
event_v1 = _event;
}
function init(address _initAccount) onlyByProvider onlyFirst {
myCoinTable_v1.create(_initAccount);
myCoinTable_v1.setAmount(_initAccount, TOTAL_AMOUNT);
initDone = false;
}
function setMyCoinTable_v1(MyCoinAccountTable_v1 _myCoinTable) onlyByProvider {
myCoinTable_v1 = _myCoinTable;
}
function setMyCoinEvent_v1(MyCoinEvent_v1 _event) onlyByProvider {
event_v1 = _event;
}
function exist(address _account) constant returns (bool) {
return myCoinTable_v1.exist(bytes32(_account));
}
function send(address _from, address _to, uint _value) onlyByVersionContractOrLogic sendable(_from, _value) {
if (!exist(_to)) myCoinTable_v1.create(_to);
myCoinTable_v1.setAmount(_from, getAmount(_from) - _value);
myCoinTable_v1.setAmount(_to, getAmount(_to) + _value);
event_v1.send(_from, _to, _value);
}
function getAmount(address _account) constant returns (uint) {
return myCoinTable_v1.getAmount(_account);
}
}
Kế thừa VersionLogic :
Logic Contract thì cần phải kế thừa VersionLogic . Ngoài ra ta cũng cần phải truyền CNS và tên Contract như là tham số cho Contructor của VersionLogic.
Phát hành tiền tệ :
Trong ứng dụng lần này , chức năng phát hành tiền tệ không được thực hiên , nên đầu tiên ta cần phải phát hành toàn bộ tiền tệ . Sử dụng init ta phát hành tiền tệ cho tài khoản đang có với chỉ tham số TOTAL_AMOUNT . Lúc này , với onlyProvider thì chỉ có tài khoản mà đã deloy Contract này và với onlyFirst thì chỉ có lần chạy đầu tiên mới có thể thực hiện được.
Thiết lập Contract Interface cần thiết :
MyCoinLogic_v1 dùng để quản lý EventContract và dữ liệu sở hữu tiền ảo . MyCoinAccountTable_v1 dùng để truy cập vào TableContract .Do đó ta cần phải thiết lập những Contract Instance này trong Contructor và setter .
Hạn chế quyền truy cập của Transaction :
Về LogicContract , việc chấp nhận các transaction từ các Contract khác ngoài MainContract nên ta có quan ngại về vấn đề an ninh. Do đó, bằng cách gắn onlyByVersionContractOrLogic vào các hàm Transaction , ta có thể hạn chế quyền truy cập ngòai trừ MainContract và một phiên bản riêng của LogicContract.
Xử lý gửi tiền :
Khi gửi tiền , thì bên gửi tiền phải có sở hữu số tiền nhiều hơn số tiền muốn gửi . Dùng sendable (thật ra thì Z.com muốn dùng từ payable để diễn tả việc kiểm tra này , nhưng đây là từ khóa của ETH nên không thể dùng lại ) để kiểm tra , nếu số tiền sở hữu ít hơn số tiền muốn gửi thì lỗi sẽ xảy ra .
Table Contract
Table contract dùng để quản lý dữ liệu về tiền sở hữu của end user sẽ được trình bày trong MyCoinAccountTable_v1.sol bên dưới .
pragma solidity ^0.4.2;
import '../../gmo/contracts/VersionField.sol';
contract MyCoinAccountTable_v1 is VersionField {
struct Account {
bool isCreated;
uint amount;
}
mapping(bytes32 => Account) public accounts;
function MyCoinAccountTable_v1(ContractNameService _cns) VersionField(_cns, 'MyCoin') {}
/** OVERRIDE */
function setDefault(bytes32 _id) private {
accounts[_id] = Account({ isCreated: true, amount: 0 });
}
/** OVERRIDE */
function existIdAtCurrentVersion(bytes32 _id) constant returns (bool) {
return accounts[_id].isCreated;
}
function create(address _account) onlyByNextVersionOrVersionLogic {
bytes32 id = bytes32(_account);
if (exist(id)) throw;
accounts[id] = Account({ isCreated: true, amount: 0 });
}
function setAmount(address _account, uint _amount) onlyByNextVersionOrVersionLogic {
bytes32 id = bytes32(_account);
prepare(id);
accounts[id].amount = _amount;
}
function getAmount(address _account) constant returns (uint) {
bytes32 id = bytes32(_account);
if (shouldReturnDefault(id)) return 0;
return accounts[id].amount;
}
}
Kế thừa VersionField
Table Contract cần phải kế thừa VersionFiled . Và Contructor của VersionField cần phải được truyền tham số CNS và Contract Name .
Cấu trúc Data
accounts là một mảng kết hợp, có key là địa chỉ của người dùng, value có chứa Account để quản lý dữ liệu tiền. Tuy nhiên , hạn chế của VersionField là key chỉ bytes32 , vì vậy ta phải cast address thành bytes32 để sử dụng .
Tạo hàm abstract
Trong VersionField có 2 hàm abstract là setDefault và existIdAtCurrentVersion , vì vậy ta cần phải tạo 2 hàm đó . setDefault dùng để thiết lập giá trị mặc định của data của version này . existIdAtCurrentVersion thì nếu trong version này có tồn tại data thì trả về true.
Các column setter
Table Contract thì không bao gồm logic , chỉ dùng cho setter và getter của các cột data. Trong setter bằng việc gắn onlyByNextVersionOrVersionLogic thì có thể hạn chế việc truy cập ngoài những version tiếp theo của Table Contract và Logic Contract . Ngoài ra , trường hợp nhiều version đang triển khai , ta có thê gọi prepare để tổng hợp thống nhất dữ liệu .
Các column getter
Trường hợp nhiều version đang triển khai , để tổng hợp thống nhất dữ liệu ta gọi shouldReturnDefault , nếu mà là true thì trả về giá trị mặc định . Nếu không phải true thì trả về giá trị của version này .
Event Contract
Event Contract là MyCoinEvent_v1.sol như bên dưới .
pragma solidity ^0.4.2;
import '../../gmo/contracts/VersionEvent.sol';
contract MyCoinEvent_v1 is VersionEvent {
function MyCoinEvent_v1(ContractNameService _cns) VersionEvent(_cns, 'MyCoin') {}
event SendEvent(address indexed _from, address indexed _to, uint _value);
function send(address _from, address _to, uint _value) onlyByVersionLogic {
SendEvent(_from, _to, _value);
}
}
Kế thùa VersionEvent
Event Contract thì cần phải kế thừa VersionEvent . Ngoài ra , ta cần phải truyền CNS và Contract Name vào contructor của VersionEvent như là tham số .
Event chuyển tiền
Trong ứng dụng lần này cái sử lý cần ghi lại log thì chỉ có sử lý chuyển tiền . Để ghi lại sự kiện chuyển tiền ta định nghĩa SendEvent . Tham số ghi lại là nới chuyển tiền , nới nhận tiền , và số tiền gửi . Lúc này gắn thêm indexed vào ta có thể search trong event log .
Hàm event chuyển tiền
Để có thể sinh ra event chuyển tiền từ trong LogicContract , thì ta cần hàm để gọi event chuyển tiền . Thêm onlyByVersionLogic để hạn chế quyền truy cập ngoài trừ từ LogicContract .
Web Application
Mình sẽ giải thích làm sao để sử dụng MyCoin đã tạo ở trên trong ứng dụng Web . Để đến đước bước này bạn phải deploy contract của bạn và đăng ký CNS và ABI trong màn hình quản lý như trong hướng dẫn của Z.com
Tạo key
Z.com Cloud BlockChain việc chứng minh là có đúng là chính là người dùng đang thao tác hay không thì ta cần phải đăng ký khóa bí mật . Việc tạo khóa bị mật đó được trình bày phía dưới .
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="http://beta.blockchain.z.com/static/client/lib/eth-client.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript">
var account;
function create() {
ethClient.Account.create('https://beta.blockchain.z.com', $('#password').val(), function(err, instance) {
if (err) {
console.log(err);
} else {
account = instance;
localStorage.setItem('account', account.serialize());
$('#address').html(account.serialize());
}
});
};
function getAddress() {
var serializedAccount = localStorage.getItem('account');
account = ethClient.Account.deserialize(serializedAccount);
$('#address').html(account.getAddress());
};
</script>
</head>
<body>
<table>
<tr>
<td>password:</td>
<td>
<input id="password" value="password" />
</td>
<td>
<button onclick="create()">create</button>
<button onclick="getAddress()">getAddress</button>
</td>
<td><span id="address" /></td>
</tr>
</table>
</body>
</html>
Library
Tham khảo link bên dưới
Account · BlockChain Service API
Tạo account
Dùng ethClient.Account.create để tạo account người dùng. Tham số đầu tiền là URL , tham số thứ 2 là password của account . Giá trị trả về sẽ là 1 account instance . Lưu nó trong localStorage .
Lấy address
Sử dụng getAddress ta có thể lấy address của account .
Ứng dụng chuyển tiền
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="http://beta.blockchain.z.com/static/client/lib/eth-client.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript">
const CNS_ADDRESS = '0xf4eaf03dff63deafdfb68f202734ce15ec53eead';
const MY_COIN_ABI = [{"constant":true,"inputs":[],"name":"provider","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"isVersionContract","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_sign","type":"bytes"},{"name":"_timestamp","type":"uint256"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"send","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"exist","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"isVersionLogic","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"contractName","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_logic","type":"address"}],"name":"setMyCoinLogic_v1","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"logic_v1","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"isVersionContractOrLogic","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"cns","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"getAmount","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getContractName","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getCns","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"_cns","type":"address"},{"name":"_logic","type":"address"}],"payable":false,"type":"constructor"}];
const serializedAccount = localStorage.getItem('account');
const account = ethClient.Account.deserialize(serializedAccount);
const contract = new ethClient.AltExecCnsContract(account, CNS_ADDRESS);
function send() {
contract.sendTransaction('password', 'MyCoin', 'send', [Math.floor(new Date().getTime() / 1000), $("#to-address").val(), $("#value").val()], MY_COIN_ABI, function(err, res) {
if (err) {
alert('error');
console.error(err);
} else {
console.log(res);
};
});
}
function getAmount() {
contract.call('password', 'MyCoin', 'getAmount', [account.getAddress()], MY_COIN_ABI, function(err, amount) {
if (err) {
alert('err');
console.error(err);
} else {
$('#amount').text(amount);
}
});
}
$(document).ready(function() {
$('#my-address').text(account.getAddress());
getAmount();
});
</script>
</head>
<body>
<h3>アカウント情報</h3>
<div>
<div>アドレス</div>
<div id="my-address"></div>
</div>
<div>
<div>所持金</div>
<div id="amount"></div>
</div>
<h3>送金</h3>
<div>
<div>送金先</div>
<input id="to-address" />
</div>
<div>
<div>金額</div>
<input id="value" />
</div>
<div>
<button onclick="send()">決定</button>
</div>
</body>
</html>
Thiết lập truy cập đến Contract
Bạn cần phải đăng ký CNS và ABI của Contract để có thế truy cập đến Contract thông qua API . Ngoài ra bằng việc sử dụng ethClient.AltExecCnsContract , bạn có thể thực hiện sendTransaction và call đối với Contract .
Hiện thị số tiền sở hữu
Thực hiện call đôi với getAmount của MyCoin Contract với tham số là địa chỉ của chính mình .
Sử lý gửi tiền
Thực hiện sendTransaction đối với send của MyCoin Contract . Với tham số là timestamp , địa chỉ gửi tiền , số tiền gửi .
Demo
Bấy giờ ta sẽ tiến hành demo ứng dụng . Nội dung sẽ là hiện thị số tiền sở hữu và sử lý gửi tiền .
Hiện thị số tiền sở hữu
Đâu tiên địa chỉ của Account All currencies là 0xfc865f061f183129985e9224a52293a95bb1f916. Địa chỉ của Account bình thường là 0xadd1c664db02f7fcb898ed6cbb1fc63635c11a1d .
Số tiền của Account All currencies là 1000000000 , số tiền của tài khoản bình thưởng là 0 .
Sử lý gửi tiền
Sẽ tiến hành gửi tiền từ Account All currenciesn tới Account bình thường . Nếu thành công , số tiền của tài khoản bình thường sẽ tăng lên .
Khi sử lý gửi tiền nếu mà số tiền gửi nhiều hơn số tiền sở hữu sẽ sinh ra lỗi .
Sủ lý gửi tiền nếu thành công , sẽ sinh ra event .Từ Truffle Console ta có lấy được log chuyển tiền từ Account All currenciesn bằng tay như là ở phía dưới .
var event = MyCoinEvent_v1.deployed().SendEvent({from:'0xfc865f061f183129985e9224a52293a95bb1f916'}, {fromBlock:0,toBlock:'latest'});
event.get(function(err, res) {
console.log(res);
});
Log lấy được sẽ như bên dưới . Tham số của event được định nghĩa bằng MyCoinEvent_v1.sol sẽ được ghi trong args .
[
{
address: '0xcba5b1c1a823505e6003d59019572dd50d80aca8',
blockHash: '0xc42d0a275227258d7eee1d3ce84564e7bc35f746c610988989d90eafc0a59905',
blockNumber: 1051987,
logIndex: 0,
transactionHash: '0xc4c2e25a8f2b9e3d1e727ecdd79f9af3bee8ff4a1f126e9bf9bcf7cfefb19107',
transactionIndex: 0,
transactionLogIndex: '0x0',
type: 'mined',
event: 'SendEvent',
args:
{
_from: '0xfc865f061f183129985e9224a52293a95bb1f916',
_to: '0xadd1c664db02f7fcb898ed6cbb1fc63635c11a1d',
_value: [Object]
}
}
]
Kết luận
Ta đã thực hiện xong ứng dụng tiền ảo đơn giản bẳng cách sử dụng Z.com clound BlockChain . Dựa vào ví dụ này có thể giúp ta hiểu được cái code Solidity của Smart Contract của dịch vụ này. Giúp ta cảm thấy thật đơn giản khi phát triển dịch vụ của BlockChain .
Tham khảo :
https://recruit.gmo.jp/engineer/jisedai/blog/blockchain_coin_v1/