Blockchain cơ bản – Học lập trình Ethereum (P2)
Bài trước chúng ta đã nói khá nhiều về lập trình Ethereum dùng Truffle và command line Ganache. Bài này chúng ta sẽ dùng environment này để nói về 2 concept khác của Solidity: Interface và các Function Modifier. Cài đặt sẵn sàng Sau khi bạn đã cài Truffle và ganache-cli, tạo một ...
Bài trước chúng ta đã nói khá nhiều về lập trình Ethereum dùng Truffle và command line Ganache. Bài này chúng ta sẽ dùng environment này để nói về 2 concept khác của Solidity: Interface và các Function Modifier.
Cài đặt sẵn sàng
Sau khi bạn đã cài Truffle và ganache-cli, tạo một folder mới và chạy truffle init. Chúng ta sẽ tạo 2 contract trong folder truffle mới tạo.
- Answers.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
pragma solidity ^0.4.18; contract Answers { uint answerUniverse; function setAnswerUniverse(uint _answer) external { answerUniverse = _answer; } function getAnswerUniverse() public view returns (uint){ return answerUniverse; } } |
- Questions.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
pragma solidity ^0.4.18; contract AnswersInterface { function getAnswerUniverse() public view returns (uint); } contract Questions { AnswersInterface answersContract; function setAnswersContractAddress(address _address) external{ answersContract = AnswersInterface(_address); } function whatIsTheAnswerUniverse() public view returns (uint){ uint answer = answersContract.getAnswerUniverse(); return answer; } } |
Contract Answers.sol rất đơn giản. Một variable answerUniverse có cả getter và setter. Lưu ý rằng, bởi vì nó quan trọng rằng setter là nhân tố ngoài, nghĩa là nó phải được call từ ngoài contract, còn getter thì public nghĩa là ai call nó cũng được.
Contract Questions.sol gồm 2 thứ: contract Questions, và interface AnswersInterface. Vậy, interface là gì? Một interface cho phép chúng ta nói về một contract khác trên blockchain. Như bạn có thể thấy, để xác định một interface, bạn phải bắt đầu từ một contract bình thường, với keyword contract. Trong contract này, bạn chỉ xác định các function bạn muốn tương tác không cần phần thân. Các function này cần là public hoặc từ bên ngoài để có thể call chúng từ ngoài contract gốc. Bạn không thể tương tác với các function riêng hoặc nội bộ course được.
Bên trong contract Questions, chúng ta sẽ tạo một interface mẫu trong answersContract. Function setAnswersContractAddress sẽ cho contract biết chỗ tìm được contract Answer gốc. Function whatIsTheAnswerUniverse thu về variable answerUniverse trong contract Answers.
Note: Dĩ nhiên chúng ta cần phải set địa chỉ contract trước khi lấy variable. Nếu không chúng ta sẽ chả biết nó ở đâu trên blockchain!
- Tiếp đến, trong file truffle-config.js:
1 2 3 4 5 6 7 8 9 10 11 |
module.exports = { networks: { development: { host: '127.0.0.1', port: 7545, network_id: '*' } } }; |
Với người dùng Windows, bạn sẽ phải remove file truffle.js để tránh mâu thuẫn. Với hệ khác, bạn có thể giữ lại cả hau và bỏ vào code trong truffle.js, hoặc lằm giống như user Windows chẳng sao cả.
- Trong folder migrations, tạo 2 file: 2_deploy_questions.js và 3_deploy_answers.js.
- 2_deploy_questions.js
1 2 3 4 5 6 7 |
var Questions = artifacts.require("Questions") module.exports = function(deployer) { deployer.deploy(Questions) }; |
1 2 3 4 5 6 7 |
var Questions = artifacts.require("Questions") module.exports = function(deployer) { deployer.deploy(Questions) }; |
- 3_deploy_answers.js
1 2 3 4 5 6 7 |
var Answers = artifacts.require("./Answers.sol") module.exports = function(deployer) { deployer.deploy(Answers) } |
Triển khai thôi
Mở một cửa sổ terminal mới và chạy ganache-cli -p 7545. Quay lại folder của project và chạy:
- truffle compile
- truffle migrate --network development
- truffle console --network development
Bây giờ chúng ta có thể chơi với các contract và interface của nó. Đầu tiên, tạo một instance cho mỗi contract.
1 2 3 4 |
truffle(development)> Questions.deployed().then(inst => Questions = inst) truffle(development)> Answers.deployed().then(inst => Answers = inst) |
Sau khi launch Answers instance, bạn sẽ thấy vài thứ mới xuất hiện trong console. Sẽ có cả field address. Đây là address của contract Answers. Đây có thể là những gì ta cần để call setAnswersContractAddress function:
truffle(development)> Questions.setAnswersContractAddress('0x2e91a07090cfbbc0839e0d76d8110e2518bae18c')
Note: Hãy thế address bằng bất cứ address nào bạn thấy trong field.
Hãy xem answersUniverse variable trong Answers contract:
truffle(development)> Answers.setAnswerUniverse(76)
Và lấy nó từ contract Questions:
1 2 3 4 |
truffle(development)> Questions.whatIsTheAnswerUniverse().then(answer => answer.toNumber()) 76 |
Hãy chỉnh lại answer và retrieve lần nữa:
1 2 3 4 5 |
truffle(development)> Answers.setAnswerUniverse(42) truffle(development)> Questions.whatIsTheAnswerUniverse().then(answer => answer.toNumber()) 42 |
Và bạn đã có một interface rồi.
Các function modifier
Có thể dễ thấy một vấn đề về bảo mật trong project của chúng ta: function setAnswersContractAddress là yếu tố từ bên ngoài, nghĩa là bất kì ai từ ngoài contract cũng gọi nó được, cũng có nghĩa là ai cũng có thể call function và đổi address được cả. Để giải quyết được việc này, chúng ta phải add một function modifier, sẽ được call khi function được thực hiện. Căn bản là, nó sẽ chạy một vài đợt check để đảm bảo rằng mọi thứ vẫn ổn. Trong trường hợp này, chúng ta phải đảm bảo rằng chỉ có người sở hữu mới gọi được function. Để vậy chúng ta sẽ dùng contract từ thư viện OpenZeppelin Solidity gọi là Ownable. Copy và dán nó vào.
Đừng hoảng lên nếu bạn không hiểu gì trong contract. Chỉ cần biết rằng nó sẽ đảm bảo cho function chỉ có thể kích hoạt được bởi chủ.
- Tạo một file Ownable.sol trong các contract
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/** * @title Ownable * @dev The Ownable contract has an owner address, and provides basic authorization control * functions, this simplifies the implementation of "user permissions". */ contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev The Ownable constructor sets the original `owner` of the contract to the sender * account. */ function Ownable() public { owner = msg.sender; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(msg.sender == owner); _; } } |
- Import contract Ownable vào Questions và xác định function modifier cho setAnswersContractAddress:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
pragma solidity ^0.4.18; import "./Ownable.sol"; contract AnswersInterface { function getAnswerUniverse() public view returns (uint); } contract Questions is Ownable{ AnswersInterface answersContract; function setAnswersContractAddress(address _address) external onlyOwner{ answersContract = AnswersInterface(_address); } function whatIsTheAnswerUniverse() public view returns (uint){ uint answer = answersContract.getAnswerUniverse(); return answer; } } |
Relaunch command ganache-cli, và các command truffle. Deploy các contract Answers và Questions. Mặc định chủ contract sẽ là account đầu tiên được tạo bởi ganache-cli. Sau đó lấy một account khác:
truffle(development)> account = web3.eth.accounts[3]
Bây giờ, nếu chúng ta set interface address từ account này, bạn sẽ bị lỗi:
1 2 3 4 |
truffle(development)> Questions.setAnswersContractAddress('0x37eba1bb7d4c779474a4955437e524fbdbac0dc2', {from: account}) Error: VM Exception while processing transaction: revert |
Nếu bạn remove cái object argument, hoặc dùng accounts[0] address trong key, bạn vẫn có thể set address ổn thoả.
TopDev via dev.to