solidity学习-cryptoPunks为实例
在这里使用cryptoPunks为实例来进行solidity的介绍,一般这些内容理解了就能够进行相对简单的智能合约的编写了,同时会添加一些我认为也十分重要的内容
学习文档为http://solidity-cn.readthedocs.io/zh/develop/layout-of-source-files.html
()pragma solidity ^0.4.0;
这样,意味着源文件将既不允许低于 0.4.0 版本的编译器编译, 也不允许高于(包含) 0.5.0 版本的编译器编译(第二个条件因使用 ^ 被添加)
还有pragma solidity >0.4.23 <0.5.0;
()声明状态变量
string public name;
一般我都会显示地将他们定义为public,虽然你不这么定义,调用合约时也能调用,但是用remix-ide的就知道区别,当你在run中deploy后,出现的变量按钮都是你声明为public
()变量声明后将有默认初始值,其初始值字节表示全部为零。任何类型变量的“默认值”是其对应类型的典型“零状态”,所以并不需要自己赋值,声明即可
例如, bool 类型的默认值是 false 。 uint 或 int 类型的默认值是 0 。对于静态大小的数组和 bytes1 到 bytes32 ,每个单独的元素将被初始化为与其类型相对应的默认值。 最后,对于动态大小的数组, bytes 和 string 类型,其默认缺省值是一个空数组或字符串。
所以其实代码中mapping (uint => Bid) public punkBids的punkBids被声明为{false,0,0x0,0}
()映射
mapping (address => uint256) public balanceOf;
当你想要将两个变量设定一定关系时,如该例子,想要通过账户的地址来查出账户拥有的token数量
也可以写得复杂一点,比如:
mapping (address => mapping (address => uint256)) public allowance;
访问就是allowance[‘’][‘’]
()结构类型¶
结构是可以将几个变量分组的自定义类型
1》structure结构体
struct Bid {
bool hasBid;//是否正在竞标
uint punkIndex;
address bidder;
uint value;
}
这样你可以将一组相关的信息写在一起,然后再结合语句:
mapping (uint => Bid) public punkBids;
这样你就可以通过punkBids[5]的映射方式去获取结构体Bid的信息了
2》enum枚举类型
枚举可用来创建由一定数量的“常量值”构成的自定义类型
举例:enum Gender {Male,Female}
Male = 0 , Female = 1
访问枚举方式 Gender.Male 实际等于数字 0
性别枚举的值就有男和女,所以当你声明一个枚举类型的变量的时候,它的值要么是男,要么是女这样,枚举下标定义从左至右从零开始
()event
用于事件监听,比如
event PunkTransfer(address indexed from, address indexed to, uint256 punkIndex);//声明
function transferPunk(address to, uint punkIndex) {
…
emit PunkTransfer(msg.sender, to, punkIndex); //监听事件
…
}
该event的作用就是你可以使用instance.PunkTransfer(filter,callback)函数,通过设置过滤条件filter,如{from:web3.eth.accounts[0]}来在调用transferPunk的同时监听该函数的特定条件的交易是否成功及其返回内容。
监听时的emit要加上,没有回警告
() if (msg.sender != owner) throw;
这是在合约中我们常见到的条件判断语句,但是现在用这个的时候会有警告,现在都不用这种写法了,可以换成
• if(msg.sender != owner) { revert(); }
• assert(msg.sender == owner);
• require(msg.sender == owner);
assert和require正确才可往下走
revert() 和 require() 都会返还剩余 gas,而且允许返回一个message,比如:
revert(‘Something bad happened’);
或者
require(condition, ‘Something bad happened’);
就是如果当判断的条件不成立的时候就会输出的错误信息
三者使用情况:
• 一般地,尽量使用 require 函数
• 一般地,require 应该在函数最开始的地方使用
如果有复杂的 if/else 逻辑流,那么应该考虑使用 revert() 函数而不是require()。
• 一般地,尽量少使用 assert 调用
• 一般地,assert 应该在函数结尾处使用
基本上,require() 应该被用于函数中检查条件,assert() 用于预防不应该发生的情况,但不应该使条件错误。
另外,“除非认为之前的检查(用 if 或 require )会导致无法验证 overflow,否则不应该盲目使用 assert 来检查 overflow”——来自于@chriseth
举例,在下例中,你可以看到如何轻松使用``require``检查输入条件以及如何使用``assert``检查内部错误,注意,你可以给 require 提供一个消息字符串,而 assert 不行:
contract Sharer {
function sendHalf(address addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
//由于转移函数在失败时抛出异常并且不能在这里回调,因此我们应该没有办法仍然有一半的钱。
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
所以我一般就简单地记为,当想要在调用函数之前进行条件的限制时,就使用require或者if-revert,而且可以写message来输出错误信息以确定是哪里的判断限制了交易的进行。在remix-ide中,message会在触发后在控制台显示错误信息,但若是使用自己配置的私有链,就需要去自己查看了(没试过,之后有空试试)
()函数
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]
1》参数类型<parameter types>可以为空();返回类型<return types>与参数类型相反,返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个 returns (<return types>) 部分。
solidity与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输出,但solidity不是,它返回多少个变量是定义死的,returns (uint a, uint b)。
但是参数的名称是可以省略的。在函数中未使用参数的名称(特别是返回参数)可以省略。比如一个函数return(false,2),那它的返回参数就可以写为returns(bool,uint)。这些参数仍然存在于堆栈中,但它们无法访问。
举例说明:
contract C {
// 省略参数名称
function func(uint k, uint) public pure returns(uint) {
return k;
}
2》被声明为internal|external,是内部函数和外部函数的区别
有两种方法可以访问当前合约中的函数:一种是直接使用它的名字,f ,另一种是使用 this.f 。 前者适用于声明为内部函数internal,后者适用于声明为外部函数external的函数
请注意,当前合约函数被声明为 public 函数(默认也为public),则该函数既可以被当作内部函数也可以被当作外部函数使用。 如果想将一个函数当作内部函数使用,就用 f 调用,如果想将其当作外部函数,使用 this.f
3》pure|constant|view|payable
1)带上payable关键字,说明这个函数需要接受msg.value,比如
function buyPunk(uint punkIndex) payable {}
那么在你部署好合约后,想要调用这个函数,你的调用形式一定是:
contract.buyPunk(punkIndex,{from:web3.eth.accounts[0],value:30,gas:30000});
这里的value传入的值就是在合约中使用的msg.value,from传入的值就是合约中的msg.sender
这里的gas的传入也是一个十分重要的点,详细分析请看
2)view是不能修改状态(相当于constant)
1. 写入状态变量;
2. (emitting??)发送事件;
3. 创建其他合约;
4. 使用selfdestruct;
5. 通过调用发送Ether;
6. 调用没有被声明为view和pure的函数
7. 使用低级调用;
8. 使用包含特定操作码的内联程序集。
3)pure的限制更多一点,它是不能修改状态也不能读取状态
1. 读取状态变量;
2. 访问this.balance 或者<address>.balance;
3. 访问block,tx,msg(除了msg.sig和msg.data);
4. 调用没有标记为pure的函数;
5. 使用包含特定操作码的内联程序集。
()cryptoPunks函数withdraw函数中有一句msg.sender.transfer(amount)
这句话的意思是,当我们在合同中有写payable的函数(如buyPunk)的时候,当调用这个函数时,msg.sender就需要付一定的msg.value,在这里,这个msg.value的去向to是合约的地址。
但是一般在另一个函数(withdraw)中,我们收取这个value是希望它最终是会流向某一个账户的地址的,所以在这里我们一般都会定义一个数组,即一个临时账户pendingWithdrawals来记录你付的value的值, uint amount = pendingWithdrawals[msg.sender]。
当最后想要把这个值给一个账户地址的时候,我们就会使用语句msg.sender.transfer(amount)来将合约地址上的这笔钱转到msg.sender这个账户地址中去
()地址类型address
address:地址类型存储一个 20 字节的值(以太坊地址的大小)
可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个地址发送 以太币(以 wei 为单位),即上文的msg.sender.transfer(amount)
()在remix-ide中编译合约的时候有出现过下面的警告:
Warning: Variable is declared as a storage pointer. Use an explicit "storage" keyword to silence this warning.
解决办法是: 加一个显式声明storage(搞清楚其与memory的区别)
Bid storage exist = bidForPicture[pictureId];
() 数据位置——详细说明:
有三种类型,memory,storage和calldata,一般只有外部函数的参数(不包括返回参数)被强制指定为calldata。这种数据位置是只读的,不会持久化到区块链
storage存储或memory内存
memory存储位置同我们普通程序的内存类似,即分配,即使用,动态分配,越过作用域即不可被访问,等待被回收。
而对于storage的变量,数据将永远存在于区块链上。
总结¶
强制指定的数据位置:
• 外部函数的参数(不包括返回参数): calldata,效果跟 memory 差不多
• 状态变量: storage
默认数据位置:
• 函数参数(包括返回参数): memory
• 所有其它局部变量: storage
举例说明:
contract C { uint[] x; // x 的数据存储位置是 storage,状态变量 // memoryArray 的数据存储位置是 memory,函数参数 function f(uint[] memoryArray) public { x = memoryArray; // 将整个数组拷贝到 状态变量storage 中,可行 var y = x; // 分配一个指针(其中 y 的数据存储位置是 storage),可行,其他局部变量 y[7]; // 返回第 8 个元素,可行 y.length = 2; // 通过 y 修改 x,可行 delete x; // 清除数组,同时修改 y,可行 // 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组 // 但 storage 是“静态”分配的: // y = memoryArray; // 下面这一行也不可行,因为这会“重置”指针, // 但并没有可以让它指向的合适的存储位置。 // delete y; } }
三种数据之间的相互赋值行为:
(1)当我们把一个storage类型的变量赋值给另一个storage时,我们只是修改了它的指针,一个变,另一个也变
struct S{string a;uint b;}
S s;
function convertStorage(S storage s) internal{
S tmp = s;
tmp.a = “Test”;//s的a的值也改变了
}
(2)memory转为memory,memory之间是引用传递,并不会拷贝数据,一个变,另一个也跟着变
(3)memory转换为storage
因为局部变量和状态变量的类型都可能是storage。所以我们要分开来说这两种情况:
1. memory赋值给状态变量
将一个memory类型的变量赋值给一个状态变量时,实际是将内存变量拷贝到存储中,后续两者不会有任何关系
S s;
function memoryToState(S memory tmp) internal{
s = tmp;//从内存中复制到状态变量中。
//修改旧memory中的值,并不会影响状态变量
tmp.a = “Test”;//s的就不会变了
}
2.memory赋值给局部变量是不可以的
由于在区块链中,storage必须是静态分配存储空间的。局部变量虽然是一个storage的,但它仅仅是一个storage类型的指针。如果进行这样的赋值,实际会产生一个错误。
function assign(S s) internal{ //默认的变量是storage的指针
//error:Type struct MemoryToLocalVar.S memory is not implicitly convertible to expected type struct MemoryToLocalVar.S storage pointer.
S tmp = s; //修改变量为memory类型S memory tmp = s;即可
}
(4)storage转为memory,会创建一份独立的拷贝,两两互不影响
()在合约中有很多地方即有uint8/uint256/uint,区别是:
声明一个类型为 uint (256位无符号整数)
所以在solidity中,如果你声明了一个变量类型是uint的,其实就是声明了一个uint256的
int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8 到 uint256 (无符号,从 8 位到 256 位)以及 int8 到 int256,以 8 位为步长递增。 uint 和 int 分别是 uint256 和 int256 的别名
()判断语句
JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了 switch 和 goto。 因此 Solidity 中有 if,else,while,do,for,break,continue,return,``? :``这些与在 C 或者 JavaScript 中表达相同语义的关键词。
用于表示条件的括号 不可以 被省略,单语句体两边的花括号可以被省略。
注意,与 C 和 JavaScript 不同, Solidity 中非布尔类型数值不能转换为布尔类型,因此 if (1) { ... } 的写法在 Solidity 中 无效 。
()
Gas requirement of function CryptoPicturesMarket.imageHash() high: infinite. If the gas requirement of a function is higher than the block gas limit, it cannot be executed. Please avoid loops in your functions or actions that modify large areas of storage (this includes clearing or copying arrays in storage)
但是我查了一下资料,发现这个好像不是什么问题,希望以后能够看到可以解决它的方法吧
以上是在cryptoPunks中需要知道的内容,下面是一些添加内容:
其他内容:
()一个合约如何调用另一个合约
contract MappingExample { mapping(address => uint) public balances; function update(uint newBalance) public { balances[msg.sender] = newBalance; } } contract MappingUser { function f() public returns (uint) { MappingExample m = new MappingExample(); m.update(100); return m.balances(this);//this是该合约的地址 } }
通过 new 创建合约
使用关键字 new 可以创建一个新合约。待创建合约的完整代码必须事先知道,所以在MappingUser合约中创建合约MappingExample,MappingExample合约必须在MappingUser合约之前声明。因此递归的创建依赖是不可能的。
即一个合约中创建另一个合约,另一个合约一定要在该合约之前就已经声明了
()合约相关¶
this (current contract's type):
当前合约,可以明确转换为 地址类型。
selfdestruct(address recipient):
销毁合约,并把余额发送到指定 地址类型。
suicide(address recipient):
与 selfdestruct 等价,但已不推荐使用。
()导入
import * as symbolName from “filename”;//或“.sol”文件
等同于import "filename" as symbolName;
()注释
单行(//)、多行注释(/*…*/)和(/** ... */)在函数开头或内部做的注释
/** @title 形状计算器。 */
contract shapeCalculator {
/** @dev 求矩形表明面积与周长。
* @param w 矩形宽度。
* @param h 矩形高度。
* @return s 求得表面积。
* @return p 求得周长。
*/
function rectangle(uint w, uint h) returns (uint s, uint p) {
s = w * h;
p = 2 * (w + h);
}
}
()定长字节数组¶
关键字有:bytes1, bytes2, bytes3, ..., bytes32。byte 是 bytes1 的别名。
.length 表示这个字节数组的长度(只读)
注解
可以将 byte[] 当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时,每个元素会浪费 31 字节。 更好地做法是使用 bytes。
()变长字节数组¶
bytes:
变长字节数组,参见 数组。它并不是值类型。
string:
变长 UTF-8 编码字符串类型,参见 数组。并不是值类型。
()字面常数-需要好好看看
Solidity 中是没有八进制的,因此前置 0 是无效的
注解
数值字面常数表达式只要在非字面常数表达式中使用就会转换成非字面常数类型。 在下面的例子中,尽管我们知道 b 的值是一个整数,但 2.5 + a 这部分表达式并不进行类型检查,因此编译不能通过。
//在remix中编译的时候的确是会报错
browser/test.sol:4:17: TypeError: Operator + not compatible with types rational_const 5 / 2 and uint128
uint128 b = 2.5 + a + 0.5;
^-----^
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;
意思应该就是说因为在字面上我们可以看见不进行类型检查时a是非字面常数类型
十六进制字面常数以关键字 hex 打头,后面紧跟着用单引号或双引号引起来的字符串(例如,hex”001122FF")
()library ArrayUtils {
// internal内部函数可以在内部库函数中使用,
// 因为它们会成为同一代码上下文的一部分
…}
contract Pyramid {
using ArrayUtils for *;//使用,然后通过ArrayUtils.函数名()调用
}
()数组:
可以在声明时指定长度,也可以动态调整大小。
对于 存储storage的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或者结构体)。
对于 内存memory的数组来说,元素类型不能是映射类型,如果作为 public 函数的参数,它只能是 ABI 类型。
(1)在memory中:一经创建,内存memory数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数
1.内存中的数组是不允许创建变长数组的
uint[] memory a;//wrong
2.数组字面常数是一种定长的 内存memory数组类型
在memory中,声明定长数组:
uint[] memory a = new uint[](2);
在memory中,声明定长数组并初始化:
uint[2] memory a = [uint(1),2];
uint[] memory a = [uint(1),2];
//wrong,定长的 内存memory数组并不能赋值给变长的 内存memory数组
为什么要在第一个元素一前面加上uint的类型,这是因为我们前面声明的是uint的数组,而在solidity中有uint8,uint248等多种不同位数的无符号int型常量,在不声明的情况下,默认使用uint256,而我们初始化的数组元素是1和2,系统会判定为uint8,所以需要我们在第一个元素1前面强制转化为uint型
当然,我们也可以如下的声明方式:
uint8[2] memory a = [1,2];
但要注意,那么a数组里面就不可以出现2进制里面大于8位的数了
定长数组是无法通过length和push方法更改长度或插入的
(2)在storage中
1.在storage中,声明定长数组,并初始化变量:
uint[2] a = [1,2];
2.在storage中,声明变长数组,并初始化变量:
uint[] a = [1,2];
3.二维数组初始化,行列是反过来定义的:
uint[2][3] a = [[1,1],[2,2],[3,3]];
但访问是相同的:
访问第三个动态数组的第二个元素,你应该使用 a[2][1]
(3)
bytes 和 string 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 string 与 bytes 相同,但(暂时)不允许用长度或索引来访问。
(4)变长的 存储storage数组以及 bytes 类型(而不是 string 类型)都有一个叫做 push 的成员函数
()delete的作用:
delete a 的结果是将 a 的类型在初始化时的值赋值给 a。即对于整型变量来说,相当于 a = 0, 但 delete 也适用于数组,对于动态数组来说,是将数组的长度设为 0,而对于静态数组来说,是将数组中的所有元素重置。 如果对象是结构体,则将结构体中的所有属性重置。
delete 对整个映射是无效的(因为映射的键可以是任意的,通常也是未知的)。 因此在你删除一个结构体时,结果将重置所有的非映射属性,这个过程是递归进行的,除非它们是映射。 然而,单个的键及其映射的值是可以被删除的。
contract DeleteExample { uint data; uint[] dataArray; function f() public { uint x = data; delete x; // 将 x 设为 0,并不影响数据 delete data; // 将 data 设为 0,并不影响 x,因为它仍然有个副本 uint[] storage y = dataArray; //可以通过改变dataArray来改变y,也可以通过改变y来改变dataArray delete dataArray; // 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,y 也将受到影响, // 因为它是一个存储位置是 storage 的对象的别名。 // 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。 } }
()基本类型之间的转换
(1)隐式转换
int8 不能转换成 uint256(因为 uint256 不能涵盖某些值,例如,-1)。 更进一步来说,无符号整型可以转换成跟它大小相等或更大的字节类型,但反之不能。 任何可以转换成 uint160 的类型都可以转换成 address 类型。
(2)显式转换
但有些时候会出问题
int8 y = -3;
uint x = uint(y);
这段代码的最后,x 的值将是 0xfffff..fd (64 个 16 进制字符),因为这是 -3 的 256 位补码形式。
如果一个类型显式转换成更小的类型,相应的高位将被舍弃
uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678
类型推断
为了方便起见,没有必要每次都精确指定一个变量的类型,编译器会根据分配该变量的第一个表达式的类型自动推断该变量的类型
uint24 x = 0x123;
var y = x;
这里 y 的类型将是 uint24。不能对函数参数或者返回参数使用 var。
()时间seconds、 minutes、 hours、 days、 weeks 和 years
years 后缀已经不推荐使用了,因为从 0.5.0 版本开始将不再支持。
这些后缀不能直接用在变量后边。如果想用时间单位(例如 days)来将输入变量换算为时间,你可以用如下方式来完成:
function f(uint start, uint daysAfter) public {
if (now >= start + daysAfter * 1 days) {
// ...
}
}
()赋值¶
解构赋值和返回多值¶
Solidity 内部允许元组 (tuple) 类型,也就是一个在编译时元素数量固定的对象列表,列表中的元素可以是不同类型的对象。这些元组可以用来同时返回多个数值,也可以用它们来同时给多个新声明的变量或者既存的变量(或通常的 LValues):
pragma solidity >0.4.23 <0.5.0; contract C { uint[] data; function f() public pure returns (uint, bool, uint) { return (7, true, 2); } function g() public { //基于返回的元组来声明变量并赋值 (uint x, bool b, uint y) = f(); //交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。 (x, y) = (y, x); //元组的末尾元素可以省略(这也适用于变量声明)。 (data.length,,) = f(); // 将长度设置为 7 //省略元组中末尾元素的写法,仅可以在赋值操作的左侧使用,除了这个例外: (x,) = (1,); //(1,) 是指定单元素元组的唯一方法,因为 (1) //相当于 1。 } }
()错误处理:
下列情况将会产生一个 assert 式异常:
1 如果你访问数组的索引太大或为负数(例如 x[i] 其中 i >= x.length 或 i < 0)。
2 如果你访问固定长度 bytesN 的索引太大或为负数。
3 如果你用零当除数做除法或模运算(例如 5 / 0 或 23 % 0 )。
4 如果你移位负数位。
5 如果你将一个太大或负数值转换为一个枚举类型。
6 如果你调用内部函数类型的零初始化变量。
7 如果你调用 assert 的参数(表达式)最终结算为 false。
下列情况将会产生一个 require 式异常:
1 调用 throw 。
2 如果你调用 require 的参数(表达式)最终结算为 false 。
3 如果你通过消息调用调用某个函数,但该函数没有正确结束(它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),上述函数不包括低级别的操作 call , send , delegatecall 或者 callcode 。低级操作不会抛出异常,而通过返回 false 来指示失败。
4 如果你使用 new 关键字创建合约,但合约没有正确创建(请参阅上条有关”未正确完成“的定义)。
5 如果你对不包含代码的合约执行外部函数调用。
6 如果你的合约通过一个没有 payable 修饰符的公有函数(包括构造函数和 fallback 函数)接收 Ether。
7 如果你的合约通过公有 getter 函数接收 Ether 。
8 如果 .transfer() 失败。
在内部, Solidity 对一个 require 式的异常执行回退操作(指令 0xfd )并执行一个无效操作(指令 0xfe )来引发 assert 式异常。 在这两种情况下,都会导致 EVM 回退对状态所做的所有更改。回退的原因是不能继续安全地执行,因为没有实现预期的效果。 因为我们想保留交易的原子性,所以最安全的做法是回退所有更改并使整个交易(或至少是调用)不产生效果。 请注意, assert 式异常消耗了所有可用的调用 gas ,而从 Metropolis 版本起 require 式的异常不会消耗任何 gas。
下边的例子展示了如何在 revert 和 require 中使用错误字符串:
pragma solidity ^0.4.22;
contract VendingMachine {
function buy(uint amount) payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 下边是等价的方法来做同样的检查:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// 执行购买操作
}
}
这里提供的字符串应该是经过 ABI 编码 之后的,因为它实际上是调用了 Error(string) 函数。在上边的例子里,revert("Not enough Ether provided."); 会产生如下的十六进制错误返回值:
0x08c379a0 // Error(string) 的函数选择器
0x0000000000000000000000000000000000000000000000000000000000000020 // 数据的偏移量(32)
0x000000000000000000000000000000000000000000000000000000000000001a // 字符串长度(26)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串数据("Not enough Ether provided.")
()可见性
contract C {
uint private data;
function f(uint a) private returns(uint b) { return a + 1; }//不可继承
function setData(uint a) public { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }//可继承
}
contract D {//这能访问public
function readData() public {
C c = new C();
uint local = c.f(7); // error: member `f` is not visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // error: member `compute` is not visible
}
}
contract E is C {//继承至C,所以可以访问internal,public
function g() public {
C c = new C();
uint val = compute(3, 5); // access to internal member (from derived to parent contract)
}
}
()getter函数
(1)就是在合约中声明的状态变量如data,其实都自动地生成了getter函数,就是可以像访问函数一样访问它的值,在合约外访问时就可以直接 合约名.data()
pragma solidity ^0.4.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public {
uint local = c.data();
}
}
(2)如果是在合约内部访问,它有两种访问的形式:internal和external
internal则是直接变量名访问即可
external则是使用this.data()
pragma solidity ^0.4.0;
contract C {
uint public data;
function x() public {
data = 3; // internal access
uint val = this.data(); // external access
}
}
()
contract Complex {
//Note that the mapping in the struct is omitted because there is no good way to provide the key for the mapping.
struct Data {
//如果在一个结构体中声明了一个映射,一般赋值时都先省略,然后在赋值mapping,因为它的key是不固定的
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public data;
//调用方法:data[arg1][arg2][arg3].a
}
()修饰器modifier
继承后可以直接使用被继承处的修饰器
使用修改器实现的一个防重复进入的例子。
pragma solidity ^0.4.0;
contract Mutex {
bool locked;
modifier noReentrancy() {
if (locked) throw;
locked = true;
_;//函数体f()return前的内容执行的区域
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within msg.sender.call cannot call f again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() noReentrancy returns (uint) {
if (!msg.sender.call()) throw;
return 7;
}
}
例子中,由于call()方法有可能会调回当前方法,修改器实现了防重入的检查。
如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。
需要注意的是,在Solidity的早期版本中,有修改器的函数,它的return语句的行为有些不同。
在修改器中和函数体内的显式的return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的”_"后继续执行。
()fallback function \ call()\send()
(1)fallback function
contract ExecuteFallback{ //回退事件,会把调用的数据打印出来 event FallbackCalled(bytes data); //fallback函数,注意是没有名字的,没有参数,没有返回值的 function(){
//msg.data其实就是你调用一个函数后,函数名,参数进行keccak256哈希后连接起来的data FallbackCalled(msg.data);//event } //调用已存在函数的事件,会把调用的原始数据,请求参数打印出来 event ExistFuncCalled(bytes data, uint256 para); //一个存在的函数 function existFunc(uint256 para){ ExistFuncCalled(msg.data, para); } // 模拟从外部对一个存在的函数发起一个调用,将直接调用函数 function callExistFunc(){ bytes4 funcIdentifier = bytes4(keccak256("existFunc(uint256)")); this.call(funcIdentifier, uint256(1)); } //模拟从外部对一个不存在的函数发起一个调用,由于匹配不到函数,将调用回退函数 function callNonExistFunc(){ bytes4 funcIdentifier = bytes4(keccak256("functionNotExist()")); this.call(funcIdentifier); } }
回退函数(fallback function):则是当调用一个合约里的某个函数时,如果该函数并不存在,那么就会去调用该回调函数,回调函数无参,无名,无返回值,你可以通过在里面返回个什么或者emit一个事件来显示调用了该回退函数,该函数还是很有用的
(2)Call()
Call():call()是一个底层的接口,用来向一个合约发送消息,进行合约之间的交互,但是很不安全,一般是不用的。这个函数是这样的,第一个参数是该访问的函数的名字,后面的参数则是该函数所需的参数,调用它的是一个合约的地址
(3)send()
send()函数发送ether
当我们使用address.send(ether to send)向某个合约直接转帐时,由于这个行为没有发送任何数据,所以接收合约总是会调用fallback函数
function() payable{fallbackTrigged(msg.data);}
function deposit() payable{//用来给合约存点钱
}
在上述的代码中,我们先要使用deposit()合约存入一些ether,否则将会由于余额不足,调用send()函数将报错。
查看事件fallbackTrigged得到:
fallbackTrigged[
"0x"
]
可以看到,我们成功使用send()发送了1wei到合约,触发了fallback函数,附带的数据是0x(bytes类型的默认空值),空数据。
这里需要特别注意的是:
1. 如果我们要在合约中通过send()函数接收,就必须定义fallback函数,否则会抛异常。
2. fallback函数必须增加payable关键字,否则send()执行结果将会始终为false。
fallback中的限制
send()函数总是会调用fallback,这个行为非常危险,著名的DAO被黑也与这有关。如果我们在分红时,对一系列帐户进行send()操作,其中某个做恶意帐户中的fallback函数实现了一个无限循环,将因为gas耗尽,导致所有send()失败。为解决这个问题,send()函数当前即便gas充足,也只会附带限定的2300gas,故而fallback函数内除了可以进行日志操作外,你几乎不能做任何操作。
下述行为消耗的gas都将超过fallback函数限定的gas值:
• 向区块链中写数据
• 创建一个合约
• 调用一个external的函数
• 发送ether
所以一般,我们只能在fallback函数中进行一些日志操作:
()底层的日志接口(Low-level Interface to Logs):可以代替event
通过函数log0,log1,log2,log3,log4,可以直接访问底层的日志组件。logi表示总共有带i + 1个参数(i表示的就是可带参数的数目,只是是从0开始计数的)。其中第一个参数会被用来做为日志的数据部分,其它的会做为主题(topics)。前面例子中的事件可改为如下:
log3(
msg.value,
0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20,
msg.sender,
_id
);
其中的长16进制串是事件的签名,计算方式是keccak256("Deposit(address,hash256,uint256)")
()继承
pragma solidity ^0.4.0; contract mortal is owned { function kill() { if (msg.sender == owner) selfdestruct(owner); } } contract Base1 is mortal { function kill() { /* do cleanup 1 */ mortal.kill(); } } contract Base2 is mortal { function kill() { /* do cleanup 2 */ mortal.kill(); } } contract Final is Base1, Base2 { }
对Final.kill()的调用只会调用Base2.kill(),因为派生重写,会跳过Base1.kill,因为它根本就不知道有Base1。一个变通方法是使用super。
pragma solidity ^0.4.0; contract mortal is owned { function kill() { if (msg.sender == owner) selfdestruct(owner); } } contract Base1 is mortal { function kill() { /* do cleanup 1 */ super.kill(); } } contract Base2 is mortal { function kill() { /* do cleanup 2 */ super.kill(); } } contract Final is Base2, Base1 { }
如果Base1调用了函数super,它不会简单的调用基类的合约函数,它还会调用继承关系图谱上的下一个基类合约,所以会调用Base2.kill()。需要注意的最终的继承图谱将会是:Final,Base1,Base2,mortal,owned。使用super时会调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这类似于普通虚函数查找(ordinary virtual method lookup)
当基类的构造函数中如果需要传参,那么继承它时的方式是:
contract Base { uint x; function Base(uint _x) { x = _x; } } contract Derived is Base(7) { function Derived(uint _y) Base(_y * _y) { } }
继承写的顺序是很重要的,从继承少的到多的
()抽象(Abstract Contracts)
抽象函数是没有函数体的的函数。如下:
pragma solidity ^0.4.0; contract Feline { function utterance() returns (bytes32); }
这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。
pragma solidity ^0.4.0; contract Feline { function utterance() returns (bytes32); function getContractName() returns (string){ return "Feline"; } } contract Cat is Feline { function utterance() returns (bytes32) { return "miaow"; } }
如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。
()接口(interface)
接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:
1. 不能继承其它合约,或接口。
2. 不能定义构造器
3. 不能定义变量
4. 不能定义结构体
5. 不能定义枚举类
其中的一些限制可能在未来放开。
接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。
接口用自己的关键词表示:
interface Token {
function transfer(address recipient, uint amount);
}
合约可以继承于接口,因为他们可以继承于其它的合约。
pragma solidity ^0.4.11; interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; } //使用则是在合同函数中直接使用: function approveAndCall(address _spender, uint256 _value, bytes _extraData) public returns (bool success) { tokenRecipient spender = tokenRecipient(_spender);
()library库
使用库合约的合约,可以将库合约视为隐式的父合约(base contracts),当然它们不会显式的出现在继承关系中。意思就是不用写is来继承,直接可以在合约中使用:
library Set {
struct Data { mapping(uint => bool) flags; }
}
contract C {
Set.Data knownValues;
}
但调用库函数的方式非常类似,如库L有函数f(),使用L.f()即可访问。此外,internal的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是internal的调用惯例,这意味着所有internal类型可以传进去,memory类型则通过引用传递,而不是拷贝的方式。
library Set { // We define a new struct datatype that will be used to // hold its data in the calling contract. struct Data { mapping(uint => bool) flags; } // Note that the first parameter is of type "storage // reference" and thus only its storage address and not // its contents is passed as part of the call. This is a // special feature of library functions. It is idiomatic // to call the first parameter 'self', if the function can // be seen as a method of that object. function insert(Data storage self, uint value) returns (bool) { if (self.flags[value]) return false; // already there self.flags[value] = true; return true; } function remove(Data storage self, uint value) returns (bool) { if (!self.flags[value]) return false; // not there self.flags[value] = false; return true; } function contains(Data storage self, uint value) returns (bool) { return self.flags[value]; } } contract C { Set.Data knownValues; function register(uint value) { // The library functions can be called without a // specific instance of the library, since the // "instance" will be the current contract. if (!Set.insert(knownValues, value)) throw; } // In this contract, we can also directly access knownValues.flags, if we want. }
上面的例子中:
• Library定义了一个数据结构体struct Data,用来在调用的合约中使用(库本身并未实际存储的数据)。如果函数需要操作数据,这个数据一般是通过库函数的第一个参数传入(Data storage self),按惯例会把参数名定为self。
• 另外一个需要留意的是上例中self的类型是storage,那么意味着传入的会是一个引用,而不是拷贝的值,那么修改它的值,会同步影响到其它地方,俗称引用传递,非值传递。
• 库函数的使用不需要实例化,c.register中可以看出是直接使用Set.insert。但实际上当前的这个合约本身就是它的一个实例。
• 这个例子中,c可以直接访问,knownValues。虽然这个值主要是被库函数使用的
对比普通合约来说,库的限制:
• 无状态变量(state variables)。
• 不能继承或被继承
• 不能接收ether。
附着库(Using for)
指令using A for B;用来附着库里定义的函数(从库A)到任意类型B。这些函数将会默认接收调用函数对象的实例作为第一个参数。语法类似,python中的self变量一样。
using A for *的效果是,库A中的函数被附着在做任意的类型上。
在这两种情形中,所有函数,即使那些第一个参数的类型与调用函数的对象类型不匹配的,也被附着上了。类型检查是在函数被真正调用时,函数重载检查也会执行。
using A for B;指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。
上面的例子就改成了:
contract C { using Set for Set.Data; // this is the crucial change Set.Data knownValues; function register(uint value) { // Here, all variables of type Set.Data have // corresponding member functions. // The following function call is identical to // Set.insert(knownValues, value) if (!knownValues.insert(value)) throw; } }
//其实就是本来要访问的话的语句是Set.insert(knownValues, value),现在是knownValues.insert(value),即将库中的函数都附着在了结构体struct Data上,因为之前库函数的第一个参数的声明其实也是这个结构体(self),附着后调用时就可以省略掉第一个参数了,就算没有结构体,那么附着的一定是函数的第一个参数声明的那个self的类型
如:
library Search { function indexOf(uint[] storage self, uint value) } contract C { using Search for uint[]; uint[] data; }
即 library 库,有两种作用
《1》定义 library,使用结构体,调用函数
pragma solidity ^0.4.20; // This is the same code as before, just without comments library Set { struct Data { mapping(uint => bool) flags; } function insert(Data storage self, uint value) public returns (bool) { if (self.flags[value]) return false; // already there self.flags[value] = true; return true; } function remove(Data storage self, uint value) public returns (bool) { if (!self.flags[value]) return false; // not there self.flags[value] = false; return true; } function contains(Data storage self, uint value) public view returns (bool) { return self.flags[value]; } } //然后合约中调用库中的函数 contract C { using Set for Set.Data; // this is the crucial change Set.Data knownValues; function register(uint value) public { // Here, all variables of type Set.Data have // corresponding member functions. // The following function call is identical to // `Set.insert(knownValues, value)` require(knownValues.insert(value)); } }
《2》 使用库来扩展数据类型
pragma solidity ^0.4.20; library Search { function indexOf(uint[] storage self, uint value) public view returns (uint) { for (uint i = 0; i < self.length; i++) if (self[i] == value) return i; return uint(-1); } } contract C { using Search for uint[];//使得类型为uint[]的数值可以有函数indexOf进行使用 uint[] data; function append(uint value) public { data.push(value); } function replace(uint _old, uint _new) public { // This performs the library function call uint index = data.indexOf(_old); if (index == uint(-1)) data.push(_new); else data[index] = _new; } }
()如果你想要把数据写到交易中:
参考:https://blog.csdn.net/koastal/article/details/78794275
let Web3 = require("web3"); let fs = require("fs"); let web3 = new Web3(); web3.setProvider(new Web3.providers.HttpProvider("http://192.168.1.10:8545")); let log = { time:(new Date).getTime(), type:"error", msg:"数据库连接失败" }; let str = JSON.stringify(log);//将json转换为string let data = Buffer.from(str).toString('hex'); data = '0x'+data; console.log(data); //将数据写入到交易中 let coinbase = "0x8a1C505f1ff14045c03622E9ab82EB19c730cef3"; let user1 = "0xb4B6338977078f1c355ee394D67D6DFE5C24dad8"; web3.personal.unlockAccount(coinbase, "coinbase"); let address = web3.eth.sendTransaction({ from:coinbase, to:user1, value:'0x00', data:data }); //从交易地址获取数据 let transaction = web3.eth.getTransaction(address); let inputData = transaction.input; let res_str = Buffer.from(inputData.replace('0x',''),'hex').toString(); let res_json = JSON.parse(res_str);//将string转换为json console.log(transaction); console.log(res_json);
web3.eth.sendTransaction(transactionObject [, callback])
参数:
transactionObject
: Object - 要发送的交易对象。
- from: String - 指定的发送者的地址。如果不指定,使用web3.eth.defaultAccount。
- to: String - (可选)交易消息的目标地址,如果是合约创建,则不填.
- value: Number|String|BigNumber - (可选)交易携带的货币量,以wei为单位。如果合约创建交易,则为初始的基金。
- gas: Number|String|BigNumber - (可选)默认是自动,交易可使用的gas,未使用的gas会退回。
- gasPrice: Number|String|BigNumber - (可选)默认是自动确定,交易的gas价格,默认是网络gas价格的平均值 。
- data: String - (可选)或者包含相关数据的字节字符串,如果是合约创建,则是初始化要用到的代码。
- nonce: Number - (可选)整数,使用此值,可以允许你覆盖你自己的相同nonce的,正在pending中的交易11。
- Function - 回调函数,用于支持异步的方式执行7。
返回值:
String
- 32字节的交易哈希串。用16进制表示。
web3.eth.getTransaction(transactionHash [, callback])
参数:
transactionHash
: String - 交易的哈希值。callback
: Function - 回调函数,用于支持异步的方式执行7。
返回值:
Object
- 一个交易对象- hash: String - 32字节,交易的哈希值。
- nonce: Number - 交易的发起者在之前进行过的交易数量。
- blockHash: String - 32字节。交易所在区块的哈希值。当这个区块处于pending将会返回null。
- blockNumber: Number - 交易所在区块的块号。当这个区块处于pending将会返回null。
- transactionIndex: Number - 整数。交易在区块中的序号。当这个区块处于pending将会返回null。
- from: String - 20字节,交易发起者的地址。
- to: String - 20字节,交易接收者的地址。当这个区块处于pending将会返回null。
- value: BigNumber - 交易附带的货币量,单位为Wei。
- gasPrice: BigNumber - 交易发起者配置的gas价格,单位是wei。
- gas: Number - 交易发起者提供的gas。.
- input: String - 交易附带的数据。
结果为:
0x7b2274696d65223a313531333135363433363137392c2274797065223a226572726f72222c226d7367223a22e695b0e68daee5ba93e8bf9ee68ea5e5a4b1e8b4a5227d { blockHash: '0x0000000000000000000000000000000000000000000000000000000000000000', blockNumber: null, from: '0x8a1c505f1ff14045c03622e9ab82eb19c730cef3', gas: 90000, gasPrice: { [String: '18000000000'] s: 1, e: 10, c: [ 18000000000 ] }, hash: '0x3149578fbb8cf75f264fc87426b7bfa2a89256763fe5194ccad8b724a0325470', input: '0x7b2274696d65223a313531333135363433363137392c2274797065223a226572726f72222c226d7367223a22e695b0e68daee5ba93e8bf9ee68ea5e5a4b1e8b4a5227d', nonce: 57, to: '0xb4b6338977078f1c355ee394d67d6dfe5c24dad8', transactionIndex: 0, value: { [String: '0'] s: 1, e: 0, c: [ 0 ] }, v: '0x26', r: '0x742335f7b649c554a35baf624fe90c8e3fa3c9cfc116cfe858af420dc893359a', s: '0x20a63d760ab574736aaed4799f2a15bc727a988b4d41ee279e3b753b1c1f3913' } { time: 1513156436179, type: 'error', msg: '数据库连接失败' }