solidity语言教程:http://www.tryblockchain.org/
官方文档翻译:https://solidity-cn.readthedocs.io/zh/develop/
说明:通过本文的工厂模式、合约注册表、合约调用外部合约几种模式,可以实现合约的解耦,合约调用,合约升级,可以开发类似java中的大型dapp
工程模式:一个合约可以创建并管理多个合约。原理是工厂合约中记录并保存子合约的地址,通过工程合约调用子合约中的方法。
举个例子。
创建一个简单的计数器合约,每个调用者都有自己独有的计数器,记录访问次数。假如有1W个用户,需要创建1W个合约,管理1W个地址,我们这里不需要人工的管理这1W个地址,通过一个工厂来创建并且管理。
contract Counter {
address owner;
address factory;
uint count = 0;
function Counter(address _owner) { // 构造方法
owner = _owner;
factory = msg.sender;
}
modifier isOwner(address _caller) { //判断是否是自己的计数器合约
require(msg.sender == factory);
require(_caller == owner);
_;
}
function increment(address caller) public isOwner(caller) { // 通过继承的方式,实现每次调用都先进行权限判断
count = SafeMath.add(count, 1); //这里使用类型安全的计算
}
function getCount() constant returns (uint) { // 获取自己的count数量
return count;
}
}
Counter 是每个用户独有的计数器合约,一个没用一个。用于统计调用次数。
工厂模式首先要使用mapping来记录用户与合约地址的关系
mapping(address => address) counters;
创建合约使用new 关键字即可:
counters[msg.sender] = new Counter(msg.sender);
将地址转换为合约并且调用合约:
Counter(counters[msg.sender]).increment(msg.sender);
完整代码如下:
pragma solidity ^0.4.10;
/**
* @title SafeMath
* @dev Unsigned math operations with safety checks that revert on error
*/
library SafeMath {
/**
* @dev Multiplies two unsigned integers, reverts on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b);
return c;
}
/**
* @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0);
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;
return c;
}
/**
* @dev Adds two unsigned integers, reverts on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
/**
* @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}
contract Counter {
address owner;
address factory;
uint count = 0;
function Counter(address _owner) { // 构造方法
owner = _owner;
factory = msg.sender;
}
modifier isOwner(address _caller) { //判断是否是自己的计数器合约
require(msg.sender == factory);
require(_caller == owner);
_;
}
function increment(address caller) public isOwner(caller) { // 通过继承的方式,实现每次调用都先进行权限判断
count = SafeMath.add(count, 1); //这里使用类型安全的计算
}
function getCount() constant returns (uint) { // 获取自己的count数量
return count;
}
}
contract CounterFactory {
mapping(address => address) counters; //记录用户地址与合约地址的map <用户地址,合约地址>
function createCounter() public {
if (counters[msg.sender] == 0) {//首先判断是否已经创建了合约,没有创建Counter合约才创建
counters[msg.sender] = new Counter(msg.sender);
}
}
function increment() public {
require (counters[msg.sender] != 0); //校验调用方是否已经创建了Counter合约
Counter(counters[msg.sender]).increment(msg.sender);//将地址转换为合约并且调用合约
}
function getCount(address account) public constant returns (uint) {
if (counters[account] != 0) {
return (Counter(counters[account]).getCount());//返回Counter合约的count次数
}
}
}
部署合约时主要要部署CounterFactory:
部署完合约后,通过切换不同的测试账号来进行测试即可。
很多时候,我们编写合约时并不是只有一个合约,通常都需要多个合约。如果其中一个合约升级,其他合约可以在没有任何变化没有任何改动的情况下调用最新的合约。这时就需要使用合约注册表了。
合约注册表是合约名称 => 合约地址的映射表。这样就可以获取合约并且调用相关合约内的方法了。
类似金链盟的CNS域名服务
完整合约实例
pragma solidity ^0.4.10;
contract NameRegistry {
//合约信息结构体
struct ContractDetails {
address owner;
address contractAddress;
uint16 version;
}
mapping(string => ContractDetails) registry;//合约名称与合约信息的map: <合约名称, 合约信息ContractDetails>
function registerName(string name, address addr, uint16 ver) constant returns (bool) { //注册
require(ver >= 1);//这里表示合约的version必须>=1
ContractDetails memory info = registry[name];
if (info.contractAddress == address(0)) {//没有则新建
info = ContractDetails({
owner: msg.sender,
contractAddress: addr,
version: ver
});
} else {// 如果已经存在,则更新为最新的合约
require(info.owner == msg.sender);
info.version = ver;
info.contractAddress = addr;
}
registry[name] = info;//记录map
return true;
}
function getContractDetails(string name) constant returns(address, uint16) { //根据名称获取最新的合约,返回合约地址及version
return (registry[name].contractAddress, registry[name].version);
}
}
这里演示的是合约分开部署,通过合约地址进行调用。
第一个合约:
pragma solidity ^0.4.0;
contract MyContract1 {
function f(uint data) constant returns (uint){
return data + 2;
}
}
将第一个合约部署,获得合约地址0x6bc7ea1c744185d0ded0141d08a4dbc2978b5991
编写第二个合约MyContract2
,其中根据上面的MyContract1的合约地址对MyContract1进行初始化。这样就可以调用第一个合约的方法了。
pragma solidity ^0.4.0;
contract MyContract1{
function f(uint data) constant returns (uint);
}
contract MyContract2{
MyContract1 myContract1 = MyContract1(0x6bc7ea1c744185d0ded0141d08a4dbc2978b5991);
function AddData(uint value) constant returns (uint){
return myContract1.f(value);
}
}
部署第二个合约。
编写测试 demoMyContract2.js 代码(只列出核心代码):
name=instance.AddData(31);
console.log("=== " + name.toString());
执行代码:输出结果是31+2=33
如此一来,配合上面的合约注册表,就可以实现类似于java中的分布式编程了:
部署MyContract1 ,获取address,将该address注册到合约注册表NameRegistry 中,在合约MyContract2中调用合约MyContract1时,每次都去合约注册表NameRegistry中查询最新的MyContract1的地址,就可以实现合约升级。
合约之间解耦,灵活升级,这种模式可以开发大型DAPP。
完整操作流程:
1、首先部署NameRegistry合约,得到合约地址:0x2ad1ecffff78caa5f5a61398d077ec1856b54567
2、然后修改MyContract2合约并部署:
pragma solidity ^0.4.0;
contract MyContract1{
function f(uint data) constant returns (uint);
}
contract NameRegistry {
function getContractDetails(string name) constant returns(address);
}
contract MyContract2{
//根据地质初始化注册表(这个address是NameRegistry部署的地址)
NameRegistry nameRegistry = NameRegistry(0x2ad1ecffff78caa5f5a61398d077ec1856b54567);
function AddData(uint value) constant returns (uint){
//通过注册表获取最新的MyContract1地址并初始化MyContract1
MyContract1 myContract1 = MyContract1(nameRegistry.getContractDetails("myContract1"));
return myContract1.f(value);
}
}
3、部署MyContract1,得到合约地址0xe7a58975b465b96658f5db80f057f2e25c80f594:
4、使用nodejs将其注册到注册表中:
var func = "registerName(string,address,uint16)";
var params = ["myContract1", "0xe7a58975b465b96658f5db80f057f2e25c80f594", 2];
var receipt = await web3sync.sendRawTransaction(config.account, config.privKey, address, func, params);
name=instance.getContractDetails("myContract1");
console.log("=== " + name.toString());
5、调用MyContract2,可以看到输出33.
6、测试单独修改升级合约MyContract1这里2改为了8,MyContract2不需要改动。
pragma solidity ^0.4.0;
contract MyContract1 {
function f(uint data) constant returns (uint){
return data + 8; //这里由2修改为8
}
}
7、部署MyContract1并将其注册到注册表。
8、调用合约 MyContract2,可以看到结果变成了 31+8 = 39
。
solidity提供了mapping,但是没有提供友好的访问方式。
这里自己通过mapping外记录额外元数据信息的方式实现迭代。
实例(仅是个demo,作为演示用,安全性不够):
pragma solidity ^0.4.10;
contract MappingIterator {
mapping(string => string) elements;//map,用于存储数据
string[] keys;//记录map的key
//在mapping插入数据的同时也记录keys
function put(string key, string addr) returns (bool) {
keys.push(key);
elements[key] = addr;
return true;
}
//通过keys获取总数,keys的数量与mapping一致
function getKeyCount() constant returns (uint) {
return keys.length;
}
function getElementAtIndex(uint index) constant returns (string) {
return elements[keys[index]];//数组是可以通过下标取值的,故keys通过下标就可以获取到mapping的key了。然后通过这个key取mapping的值,就可以实现mapping的迭代了
}
function getElement(string name) constant returns (string) {
return elements[name];
}
}
通常在调用方法时都会进行校验,包括权限或者参数。如果都写在方法中,则会增加很多代码量,且不易读。
function increment() public {
if (owner == msg.sender) {
count = count + 1;
}
}
使用modifier定义修饰符来提高程序可读性及可维护性:
modifier onlyBy(address _account) {
require(msg.sender == _account);
_;
}
function increment() public onlyBy(owner) {
count = count + 1;
}
也可以将修饰符函数进行抽象,使其支持任何条件判断:
modifier onlyIf(bool _condition) {
require(_condition);
_;
}
function increment() public onlyIf(msg.sender == owner) {
count = count + 1;
}
上面onlyIf接收任意的判断条件,具体的条件在调用方提供。
通过在空格分隔的列表中指定多个修饰符,将多个修饰符应用于函数,并按所显示的顺序进行评估。
pragma solidity ^0.4.10;
contract MappingIterator {
address owner;
uint _count;
function MappingIterator() {//构造函数
owner = msg.sender;
}
modifier onlyIf(bool _condition) {//高度抽象的修饰符
require(_condition);
_;
}
function increment(uint count) public onlyIf(msg.sender == owner) onlyIf(count < 20) {//多个条件
_count = count + 1;
}
function get() constant returns (uint) {
return _count;
}
}
使用修饰符不仅可以提高程序可读性,还可以验证权限,限制数据写入,数据校验等功能。
合约自毁模式用于终止一个合约,这意味着将从区块链上永久删除这个合约。 一旦被销毁,就不可能 调用合约的功能,也不会在账本中记录交易。
在处理一个被销毁的合约时,有一些需要注意的问题:
pragma solidity ^0.4.10;
contract SelfDesctructionContract {
address owner;
string someValue;
modifier ownerRestricted {
require(owner == msg.sender);
_;
}
// 构造函数
function SelfDesctructionContract() {
owner = msg.sender;
}
function setSomeValue(string value){
someValue = value;
}
//销魂当前合约
function destroyContract() ownerRestricted {
suicide(owner);
}
}
部署合约后,可以调用合约的方法,一旦调用了suicide() 进行合约的销毁,那么在此调用合约方法将失败,而且如果其内部有资产,资产将永远消失。除非非常了解其含义,否则慎用。
就像java应用一样,合约的编写也照样需要模块化,不同的功能编写不同的合约。合约之间通过接口相互调用。
升级合约时,也不需要全部合约重新部署,仅仅部署有修改的合约即可。
正是有了模块化与合约升级,所以才需要数据存储合约与数据操作合约进行分离。如果存储合约重新部署,老合约储存的数据将无法使用。
本编码规范请参考:https://www.jianshu.com/p/d616f387d811