79solidity语言学习——2020年07月22日12:49:06
Posted oneapple
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了79solidity语言学习——2020年07月22日12:49:06相关的知识,希望对你有一定的参考价值。
79、solidity
2020年07月21日16:26:53
一、Solidity概述
特点:
- 比较简单,没有多线程的概念
- 类似javascript
- 不成熟,有一写bug
1. 文档
2. 合约包含的基本元素
//指定编译器版本,版本标识符
pragma solidity ^0.4.17;
//关键字 contract 跟java的class一样 智能合约是Inbox
contract Inbox{
//string 是数据类型,message是成员变量,在整个智能合约生命周期都可以访问
//public 是访问修饰符,是storage类型的变量,成员变量和是全局变量
string public message
//函数以function开头,构造函数
function Inbox (string initMessage) public {
//本地变量
var tmp = initMessage;
message = tmp;
}
//view是修饰符,表示该函数仅读取成员变量,不做修改
function getMessage() public view returns(string) {
return message;
}
}
2. 合约的生命周期
//析构函数
function destroy() {
//销毁当前合约,并把它所有资金发送到给定的地址
selfdestruct(msg.sender);
}
调用之后,合约仍然存在于区块链之上,但是函数无法被调用,调用会抛出异常。//TODO
二、数据类型分类
- 值类型(值传递)
- 引用类型(指针传递), 没有*号操作符,而是使用两个关键字来表示
- memory(值类型)
- storage(引用类型)
1. 值类型
值类型
是指变量在传递过程中是将数值完整的拷贝一份,再赋值给新的变量,这种方式需要开辟新的内存空间,效率较低,两个变量完全独立,修改一个不会影响另外一个。
值类型
包含
- 布尔(Booleans)
- 整型(Integer)
- 地址(Address) 《- - 没见过
0xd5957914c31E1d785cCC58237d065Dd25C61c4D0
- 定长字节数组(fixed byte arrays) // var b1 [10]byte , bytes10
- 有理数和整型(Rational and Integer Literals,String literals)
- 枚举类型(Enums)
- 函数(Function Types)
2. 引用类型
举例: string storage str1
solidity没有指针,对于复杂的结构进行高效传递方式(相当于指针)是使用关键字storage
进行修饰。
复杂类型,占用空间较大的。在拷贝时占用空间较大。所以考虑通过引用传递。常见的引用类型有:
- 字符串(string)
- 不定长字节数组(bytes)
- 数组(Array)
- 结构体(Structs)
三、值类型介绍
0. 状态变量vs局部变量
定义在合约之内,但是在函数之外的变量,我们叫做状态变量
,这些变量是会上传到区块链上保存的。
下面这个合约中的message就是状态变量。
solidity语言没有main函数,只要合约部署到区块链上,就会永不停歇的执行。
pragma solidity ^0.4.24;
contract Inbox{
//状态变量
string public message;
function Inbox()payable {
}
function setMessage(string newMessage) public{
//局部变量
string memory tmp= "hello";
message = newMessage;
}
function getMessage() public constant returns(string){
return message;
}
}
1. 布尔
go语言: var flag bool , flag := true
bool flag1 ;
bool flag2 = false;
2. 整型
- int(有符号整型,有正有负)
- uint(无符号整型,无负数)
- 以8位为区间,支持int8,int16,int24 至 int256,uint同理。 int默认为int256,uint默认为uint256
创建:01.integer.sol
如果是int和uint进行相加如何处理?
pragma solidity ^0.4.24;
contract test1 {
int8 public i8 = 333; //成员变量就是状态变量
int i256 = 256;
function add() constant returns(int) {
return i8 + i256;
}
function isEqual(int a, int b) public pure returns(bool) {
return a == b;
}
}
3. 函数类型
函数类型也就是我们所说的函数,本身也是一个特殊的变量,它可以当做变量赋值
,当做函数参数传递
,当做返回值
。
- 函数声明
函数名,函数签名(返回值,参数类型,修饰符)
!- 几个非常非常非常重要的关键字
修饰符 | 说明 | |
---|---|---|
public | 公有,任何人(拥有以太坊账户的)都可以调用 | 牢记 |
private | 私有, 只有智能合约内部可以调用 | 牢记 |
external | 仅合约外部可以调用,合约内部需使用this调用 | 先忽略 |
internal | 仅合约内部和继承的合约可以调用 | 先忽略 |
view/constant | 函数会读取但是不会修改任何contract的状态变量 | 牢记 |
pure(纯净的) | 函数不使用任何智能合约的状态变量 | 牢记 |
payable | 调用函数需要付钱,钱付给了智能合约的账户 | 牢记 |
returns | 返回值函数声明中使用 | 牢记 |
- 访问可见性
- public、private
修饰为public的状态变量会默认生成一个同名的public函数
状态变量默认是internal的(先理解为private即可)
创建:02.publiPrivate可见性.sol
pragma solidity ^0.4.24;
contract Test {
//状态变量
//类型不匹配时需要显示转换类型
//返回值需要使用returns描述
//public/private 可以修饰状态变量
//状态变量默认是私有的
uint256 public ui256 = 100;
int8 private i10 = -10;
//private 修饰的函数为私有的,只有合约内部可以调用
function add() private view returns(uint256) {
return ui256 + uint256(i10);
}
function isEqueal() public view returns(bool) {
return ui256 == uint256(i10);
}
//Public修饰的函数为共有的,合约内外都可以调用
function Add() public view returns(uint256){
return add();
}
}
- 学员问题
-
contract 误写为 constant
-
忘记添加分号
-
修改代码后要重新create(重新部署合约),新版本(deploy)
-
compile一直是红色的,提示:Compiler not found
- view,constant,pure讲解
- 如果一个函数里面,访问了状态变量,但是没有修改,我们使用view或者constant修饰。
- 如果访问了状态变量,而且修改了,那么就不能constant和view,否则会报错,不修饰即可。
- 如果没有使用过状态变量,我们要修饰为pure。
- 如果你修饰为constant,但是你在函数中修改了,效果是:不会报错,正常执行,但是值不会改变。
创建:03.constantViewPure.sol
pragma solidity ^0.4.24;
contract test1 {
int8 public i8 = 100; //成员变量就是状态变量
int i256 = 256;
//表示不会修改函数内的状态变量
//为了明确语义,一般要加上constant(view两者完全相同)
function add() private constant returns(int) {
return i8 + i256;
}
//public 表示所有的人都可以看到的,而且可以调用
//private表示所有人都可以看到,但是无法调用
function mins() constant returns(uint256) {
return uint256(i256 - i8);
}
function isEqual(int a, int b) public pure returns(bool) {
return a == b;
}
function setValue(int8 num) {
i8 = num;
}
function setValue1(int8 num) constant {
i8 = num;
}
}
- payable
- 任何函数,只要修饰为payable,那么就可以在调用这个方法的时候,对value字段赋值,然后将价值value的钱转给合约。
- 若这个函数没有指定payable,但是对value赋值了,那么本次调用会报错。
创建:04.payable向合约转账.sol
pragma solidity ^0.4.24;
contract test1 {
uint128 public num;
//如果构造函数中未指定payable关键字,那么创建合约时不允许转账
//如果指定了payable,则可以转账
constructor() public {
}
//任何函数,只要指定了payable关键字,这个合约就可以接受转账,调用时,也可以转0
function giveMoney() public payable {
}
}
- 构造函数
仅在部署合约时调用一次,完成对合约的初始化。可以在创建合约时转钱到合约
相当于go里面的init函数
- 合约同名函数(已废弃)
- constructor关键字修饰(推荐)
学员问题:
注意,所有在合约内的东西对外部的观察者来说都是可见,将某些东西标记为private
仅仅阻止了其它合约来进行访问和修改,但并不能阻止其它人看到相关的信息。
- 匿名函数
- 用于转账
一个合约可以有且只有一个匿名函数,此函数不能有参数,也不能有任何返回值,当我们企图去执行一个合约上没有的函数时,那么合约就会执行这个匿名函数。
当合约在只收到以太币的时候,也会调用这个匿名函数,而且一般情况下会消耗很少的gas,所以当你接收到以太币后,想要执行一些操作的话,你尽可以把你想要的操作写到这个匿名函数里,因为这样做成本非常便宜。
//如果想向合约转账,在合约中添加如下函数即可
function() payable {
//函数体什么都不填
}
- 用于处理不存在的函数
- 合约之间调用,非js调用
contract Test {
function() { x = 1; }
uint x;
}
contract Caller {
function callTest(address testAddress) {
Test(testAddress).call(‘0xabcdefgh‘); // hash does not exist
// results in Test(testAddress).x becoming == 1.
}
}
4. 地址(Address)
- 概述
以太坊地址的长度,大小20个字节
,20 * 8 = 160位
,所以可以用一个uint160
编码。地址是所有合约的基础,所有的合约都会继承地址对象,通过合约的地址串,调用合约内的函数。
- 运算符
描述 | 符号 |
---|---|
比较运算符 | <=,<,==,!=,>=,> |
- 操作
属性/方法 | 含义 | 备注 |
---|---|---|
balance | 获取余额 | 属性,其余的都是方法 |
send | 转账 | 不建议使用 |
transfer | 转账 | 建议使用 |
call | 合约内部调用合约 | |
delegatecall | 调底层代码,别用 | |
callcode | 调底层代码,别用 |
- 余额(balance)
返回指定地址的余额
创建:05.address地址.sol
pragma solidity ^0.4.24;
contract Test {
address public addr1 = 0x0014723a09acff6d2a60dcdf7aa4aff308fddc160c;
//地址address类型本质上是一个160位的数字
//可以进行加减,需要强制转换
function add() public view returns(uint160) {
return uint160(addr1) + 10;
}
//1. 匿名函数:没有函数名,没有参数,没有返回值的函数,就是匿名函数
//2. 当调用一个不存在的方法时,合约会默认的去调用匿名函数
//3. 匿名函数一般用来给合约转账,因为费用低
function () public payable {
}
function getBalance() public view returns(uint256) {
return addr1.balance;
}
function getContractBalance() public view returns(uint256) {
//this代表当前合约本身
//balance方法,获取当前合约的余额
return address(this).balance;
}
}
- 合约地址(this)
如果只是想返回当前合约账户的余额,可以使用this
指针,this
表示合约自身的地址
pragma solidity ^0.4.0;
contract addressTest{
function getBalance() constant public returns (uint){
//return addr.balance;
return this.balance; // <<----此处使用this代替
}
}
- 转账(send,transfer)
send和transfer函数提供了由合约向其他地址转账的功能。
对比项 | send | transfer | 备注 |
---|---|---|---|
参数 | 转账金额 | 转账金额 | wei单位 |
返回值 | true/false | 无(出错抛异常) | transfer更安全 |
创建:06.transfer和send.sol
pragma solidity ^0.4.24;
contract Test {
address public addr0 = 0x00ca35b7d915458ef540ade6068dfe2f44e8fa733c;
address public addr1 = 0x0014723a09acff6d2a60dcdf7aa4aff308fddc160c;
//1. 匿名函数:没有函数名,没有参数,没有返回值的函数,就是匿名函数
//2. 当调用一个不存在的方法时,合约会默认的去调用匿名函数
//3. 匿名函数一般用来给合约转账,因为费用低
function () public payable {
}
function getBalance() public view returns(uint256) {
return addr1.balance;
}
function getContractBalance() public view returns(uint256) {
return address(this).balance;
}
//由合约向addr1 转账10以太币
function transfer() public {
//1. 转账的时候单位是wei
//2. 1 ether = 10 ^18 wei (10的18次方)
//3. 向谁转钱,就用谁调用tranfer函数
//4. 花费的是合约的钱
//5. 如果金额不足,transfer函数会抛出异常
addr1.transfer(10 * 10 **18);
}
//send转账与tranfer使用方式一致,但是如果转账金额不足,不会抛出异常,而是会返回false
function sendTest() public {
addr1.send(10 * 10 **18);
}
}
- call方法
略
5. 枚举类型(Enums)
- 枚举类型是在Solidity中的一种用户自定义类型。
- 枚举可以显示的转换与整数进行转换,但不能进行隐式转换。显示的转换会在运行时检查数值范围,如果不匹配,将会引起异常。
- 枚举类型应至少有一名成员,枚举元素默认为uint8,当元素数量足够多时,会自动变为uint16,第一个元素默认为0,使用超出范围的数值时会报错。
创建:07.enum枚举.sol
pragma solidity ^0.4.0;
contract test {
enum WeekDays {
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
WeekDays currentDay;
WeekDWys defaultday = WeekDays.Sunday;
function setDay(WeekDays _day) public {
currentDay = _day;
}
function getDay() public view returns(uint256) {
return uint256(currentDay);
}
function getDefaultDay() public view returns(uint256) {
return uint256(defaultday);
}
}
6. 字节数组
[]byte go -> bytes(后面讲)
- 定长的字节数组
solidity内置了一些数组的数据类型:(和go语言做一下对比, var b8 [8]byte),完全只读
bytes1
, ... ,bytes32
,允许值以步长1递增。- byte默认表示bytes1,byte是类型,bytes是类型,bytes1是内置数组
- bytes1只能存储1个字节,即8位的内容,bytes2最多只能存储2个字节,即16位的内容。以此类推...
- 长度可以读取 length(返回bytes5类型的长度,而不是赋值的长度)
- 长度不可以修改
- 可以通过下标访问
- 内容不可修改
支持运算:
描述 | 符号 | |
---|---|---|
比较运算 | <=,<,==,!=,>=,> | |
位运算符 | &, | ,^(异或),~非 |
下标访问 | [0,n),n表示长度 |
内置成员:length
,返回数组长度
存储方式:16进制ascii码
创建:08.内置定长数组byte1.sol
pragma solidity ^0.4.2;
//bytes1
contract fixedArray {
/*
1. 长度可以读取 length
2. 长度不可以修改
3. 可以通过下标访问
4. 内容不可修改
*/
//bytes1 ... bytes32
//bytes1 b1 = "xy";
bytes2 b2 = "xy";
bytes3 public b3 = "xy";
uint public len = b3.length;
//b3.length = 10;
bytes8 b8 = "12345678";
//b8_0返回0x31,即十进制的数字1的ascii值(3*16+1=49)
bytes1 public b8_0 = b8[0];
//b = "HELLO";ERROR,定义之后不可修改
//b8[1] = "0";
//b8= "4567";
}
- 动态大小的字节数组(见后面章节)
bytes
: 动态长度的字节数组(非值类型)
string
: 动态长度的UTF-8编码的字符类型(非值类型)
一个好的使用原则是: bytes用来存储任意长度的字节数据,string用来存储任意长度的UTF-8编码 的字符串数据。 如果长度可以确定,尽量使用定长的如byte1到byte32中的一个,因为这样更省空间。
四、 引用类型介绍
1. 不定长字节数组(bytes)
bytes -> []byte
- 动态字节数组
- 引用类型(表明可以使用
storage
来修饰,进行引用传递,指针的效果) - 支持
下标索引
- 支持
length
、push
方法(push会帮助分配空间的) - 可以修改
- 以十六进制格式赋值: ‘h‘ -> 0x68 -> 104
- 格外注意:对于bytes,如果不使用下标访问,那么可以不用先申请空间, 直接赋值即可,或者直接push
注意的坑:
旧版本的remix可以直接在remix中使用"helloworld"形式给bytes赋值,新版本不允许,必须使用0x格式
例如,如果函数类型为:byte b1, 那么赋值时需要输入的格式为: "h"(旧版本), 0x68(新版本)
创建:09.动态字节数组.sol
pragma solidity ^0.4.24;
contract Test {
bytes public name;
function getLen() public view returns(uint256) {
return name.length;
}
//1. 可以不分空间,直接进行字符串赋值,会自动分配空间
function setValue(bytes input) public {
name = input;
}
//2. 如果未分配过空间,使用下标访问会访问越界报错
function getByIndex(uint256 i) public view returns(byte) {
return name[i];
}
//3. 可以设置长度,自动分配对应空间,并且初始化为0
function setLen(uint256 len) public {
name.length = len;
}
//4.可以通过下标进行数据修改
function setValue2(uint256 i) public {
name[i] = "h";
}
//5. 支持push操作,在bytes最后面追加元素
function pushData() public {
name.push("h");
}
}
2. 字符串(string)
- 动态尺寸的UTF-8编码字符串,是特殊的可变字节数组
- 引用类型
- 不支持下标索引
- 不支持length、push方法
- 可以修改(需通过bytes转换)
创建:10.string字符串.sol
pragma solidity ^0.4.24;
contract Test {
string public name = "lily";
function setName() public {
bytes(name)[0] = "L";
}
function getLength() public view returns(uint256) {
return bytes(name).length;
}
function setLength(uint256 i) public {
bytes(name).length = i;
bytes(name)[i - 1] = "H";
}
}
设置name 长度10,name结果
{ "0": "string: Lilyu0000u0000u0000u0000u0000u0000" }
3. 数据位置(Data location)
复杂类型,不同于之前值类型
,占的空间更大,超过256字节,因为拷贝它们占用更多的空间,如数组(arrays)
和数据结构(struct)
,他们在Solidity中有一个额外的属性,即数据的存储位置:memory
和storage
。
- 内存(memory)
- 数据不是永久存在的,存放在内存中,越过作用域后无法访问,等待被回收。
- 被memory修饰的变量是直接拷贝,即与上述的值类型传递方式相同。
- 存储 (storage)
- 数据永久保存在。
- 被storage修饰的变量是引用传递,相当于只传地址,新旧两个变量指向同一片内存空间,效率较高,两个变量有关联,修改一个,另外一个同样被修改。
- 只有引用类型的变量才可以显示的声明为
storage
。
- 状态变量
状态变量总是stroage类型的,无法更改
- 局部变量
默认是storage类型(仅限数据结构或数组,string),但是可以声明为memory类型。
创建:11.storageVsMemory.sol
pragma solidity ^0.4.24;
contract Test {
string public name = "lily";
uint256 public num = 10;
function call1() public {
setName(name);
}
//对于引用类型数据,作为函数参数时,默认是memory类型(值传递)
//function setName(string input) private {
function setName(string memory input) private {
num = 20;
bytes(input)[0] = "L";
}
function call2() public {
setName2(name);
}
//2. 如果想引用传递,那么需要明确指定为stroage类型
function setName2(string storage input) private {
num = 30;
bytes(input)[0] = "L";
}
//如果局部变量是string,数组,结构体类型数据,默认情况下是storage类型
function localTest() public {
//string tmp = name;
string storage tmp = name;
num = 40;
bytes(tmp)[0] = "L";
}
function localTest1() public {
//也可以明确设置为memory类型
string memory tmp = name;
num = 50;
bytes(tmp)[0] = "L";
}
}
4. 转换(bytes1/bytes/string)
综合示例:
创建:12.bytesStringbyte1相互转化.sol
pragma solidity ^0.4.24;
contract Test {
bytes10 public b10 = 0x68656c6c6f776f726c64; //helloworld
bytes public bs10 = new bytes(b10.length);
//将固定长度数组的值赋值给不定长度数组
function fixedByteToBytes() public {
//bs10 = b10;
for (uint256 i = 0; i < b10.length; i++) {
bs10[i] = b10[i];
}
}
//将bytes转成string
string public str1;
function bytesToString() public {
fixedByteToBytes();
str1 = string(bs10);
}
//将string转成bytes
bytes public bs20;
function stringToBytes() public {
bytesToString();
bs20 = bytes(str1);
}
}
5. 数组
- 内置数组
已介绍
- string(不定长)
- bytes(不定长)
- bytes1...bytes32(定长)
- 自定义数组
定长数组
go numbers [10] uint
- 类型T,长度K的数组定义为T[K],例如:uint [5] numbers, byte [10] names;
- 内容可变
- 长度不可变,不支持push
- 支持length方法
创建:13.自定义定长数组.sol
pragma solidity ^0.4.24;
contract Test {
//Type[Len] name
uint256[10] public numbers = [1,2,3,4,5,6,7,8,9, 10];
uint256 public sum;
// - 类型T,长度K的数组定义为T[K],例如:uint [5] numbers, byte [10] names;
// - 内容可变
// - 长度不可变,不支持push
// - 支持length方法
function total() public returns(uint256) {
for (uint256 i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}
function setLen() public {
//numbers.length = 10;
}
function changeValue(uint256 i , uint256 value) public {
numbers[i] = value;
}
//++++++++++++++++++++++++++++++++++
bytes10 public helloworldFixed = 0x68656c6c6f776f726c64;
byte[10] public helloworldDynamic = [byte(0x68), 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64];
bytes public b10;
function setToBytes() public returns (string){
for (uint256 i=0; i< helloworldDynamic.length; i++) {
byte b1 = helloworldDynamic[i];
b10.push(b1);
}
return string(b10);
}
}
不定长数组
- 定义格式为T [ ],例如:string[ ] names, byte[ ] citys。
- 内容可以修改
- 可以改变长度(仅限storage类型) 支持
length
、push
方法 - memory类型的不定长数组不支持修改长度
- 即使没有手动分配空间,直接改变长度,那么也会自动分配空间
创建:14.自定义不定长数组.sol
pragma solidity ^0.4.24;
contract Test {
//第一种创建方式,直接赋值
uint8[] numbers = [1,2,3,4,5,6,7,8,9,10];
function pushData(uint8 num) public {
numbers.push(num);
}
function getNumbers() public view returns(uint8[]) {
return numbers;
}
//第二种:使用new关键字进行创建,赋值给storage变量数组
uint8[] numbers2;
function setNumbers2() public {
numbers2 = new uint8[](7);
numbers2.length = 20;
numbers2.push(10);
}
function getNumbers2() public view returns(uint8[]) {
return numbers2;
}
function setNumbers3() public {
//使用new创建的memory类型数组,无法改变长度
uint8[] memory numbers3 = new uint8[](7);
//uint8[] memory numbers3;
//numbers3.length = 100; //无法修改
//numbers3.push(x0);
}
}
二维数组
//TODO
15.二维数组.sol
6. 结构体
创建:16.struct结构体.sol
pragma solidity ^0.4.5;
contract Test {
//定义结构之后无分号,与枚举一致
struct Student {
string name;
uint age;
uint score;
string sex;
}
Student[] public students;
//两种赋值方式
Student public stu1 = Student("lily", 18, 90, "girl");
Student public stu2 = Student({name:"Jim", age:20, score:80, sex:"boy"});
function assign() public {
students.push(stu1);
students.push(stu2);
stu1.name = "Lily";
}
}
7. 字典/映射/hash表(mapping)
- 键key的类型允许除映射外的所有类型,如数组,合约,枚举,结构体,值的类型无限制。
- 无法判断一个mapping中是否包含某个key,因为它认为每一个都存在,不存在的返回0或false。
- 映射可以被视作为一个哈希表,在映射表中,不存储键的数据,仅仅存储它的
keccak256
哈希值,用来查找值时使用。 - 映射类型,仅能用来定义状态变量,或者是在内部函数中作为storage类型的引用。
- 不支持length
创建:17.mapping映射.sol
pragma solidity ^0.4.20;
contract test {
//id -> name
mapping(uint => string) id_names;
constructor() public{
id_names[0x01] = "lily";
id_names[0x02] = "Jim";
id_names[0x02] = "Lily";
}
function getNameById(uint id) public returns (string){
//加上storage如何赋值?
string memory name = id_names[id];
return name;
}
function setNameById(uint id) public returns (string){
// mapping(uint => string) memory id_name = id_names;
// var ids = id_names;
id_names[id] = "Hello";
}
// function getMapLength() public returns (uint){
// return id_names.length;
// }
}
五、高级语法
1. 自动推导var(忘了她吧)
为了方便,并不总是需要明确指定一个变量的类型,编译器会通过第一个向这个对象赋予的值的类型来进行推断
uint24 x = 0x123;
var y = x;
需要特别注意的是,由于类型推断是根据第一个变量进行的赋值。所以下面的代码将是一个无限循环,因为一个uint8的i的将小于2000。
for (var i = 0; i < 2000; i++)
{
//uint8 -> 255
//无限循环
}
pragma solidity ^0.4.24;
contract Test{
function a() view returns (uint, uint){
uint count = 0;
var i = 0;
for (; i < 257; i++) {
count++;
if(count >= 260){
break;
}
}
return (count, i);
}
}
结果:
0: uint256: 260
1: uint256: 3
分析:
i, count
255, 256
0, 257
1, 258
2, 259
3, 260
00
01
10
11111111111111111111111
1000000000000000
1000000000000001
2. 全局函数/变量
- 最重要的两个全局变量
-
msg.sender
每一次和以太坊交互时都会产生一笔交易,这笔交易的执行人就是msg.sender。简而言之:谁调用的,msg.sender就是谁,每笔交易的msg.sender都可以不同。举例:
- 部署合约的时候,msg.sender就是部署的账户。
- 调用setMessage时,msg.sender就是调用账户。
- 调用getMessage时,msg.sender就是调用账户。
demo:
pragma solidity ^0.4.24;
contract Test {
address public owner;
uint256 a;
address public caller;
constructor() public {
//在部署合约的时候,设置一个全局唯一的合约所有者,后面可以使用权限控制
owner = msg.sender;
}
//1. msg.sender是一个可以改变的值,并不一定是合约的创造者
//2. 任何人调用了合约的方法,那么这笔交易中的from就是当前msg.sender
function setValue(uint256 input) public {
a = input;
caller = msg.sender;
}
}
-
msg.value
我们在介绍payable关键字的时候说,如果函数修饰为payable,那么这个函数可以接收转账,这笔钱通过remix的value输入框传递进来。
在转账操作中,这笔钱是通过我们调用一个函数从而产生一笔交易而转入合约的,换句话说,是这笔交易附带了一笔钱。在合约中,每次转入的value是可以通过msg.value来获取到的。注意,
- 单位是wei
- 有msg.value,就必须有payable关键字
demo:
pragma solidity ^0.4.24;
contract Test {
//uint256 public money;
mapping(address=> uint256) public personToMoney;
//函数里面使用了msg.value,那么函数要修饰为payable
function paly() public payable {
// 如果转账不是100wei,那么参与失败
// 否则成功,并且添加到维护的mapping中
if (msg.value != 100) {
throw;
}
personToMoney[msg.sender] = msg.value;
}
function getBalance() public view returns(uint256) {
return address(this).balance;
}
}
注意,测试时,传入的地址要加上英文双引号!!!
- 区块和交易的属性
函数 | 含义 |
---|---|
blockhash(uint blockNumber) | 哈希值(byte32) |
block.coinbase | (address) 当前块矿工的地址。 |
block.difficulty | (uint)当前块的难度 |
block.gaslimit | (uint)当前块的gaslimit |
block.number | (uint)当前区块的块号 |
block.timestamp | (uint)当前块的时间戳 |
msg.data | (bytes)完整的调用数据(calldata) |
gasleft() | (uint)当前还剩的gas |
msg.sender | (address)当前调用发起人的地址 |
msg.sig | (bytes4)调用数据的前四个字节(函数标识符) |
msg.value | (uint)这个消息所附带的货币量,单位为wei |
now (uint)当前块的时间戳 | 等同于block.timestamp |
tx.gasprice | (uint) 交易的gas价格 |
tx.origin | (address)交易的发送者(完整的调用链) |
示例:(使用图形化ganache测试)
pragma solidity ^0.4.24;
contract Test {
bytes32 public blockhash1;
address public coinbase;
uint public difficulty;
uint public gaslimit;
uint public blockNum;
uint public timestamp;
bytes public calldata;
uint public gas;
address public sender;
bytes4 public sig;
uint public msgValue;
uint public now1;
uint public gasPrice;
address public txOrigin;
function tt () public payable {
//给定区块号的哈希值,只支持最近256个区块,且不包含当前区块
blockhash1 = blockhash(block.number - 1);
coinbase = block.coinbase ;//当前块矿工的地址。
difficulty = block.difficulty;//当前块的难度。
gaslimit = block.gaslimit;// (uint)当前块的gaslimit。
blockNum = block.number;// (uint)当前区块的块号。
timestamp = block.timestamp;// (uint)当前块的时间戳。
calldata = msg.data;// (bytes)完整的调用数据(calldata)。
gas = gasleft();// (uint)当前还剩的gas。
sender = msg.sender; // (address)当前调用发起人的地址。
sig = msg.sig;// (bytes4)调用数据的前四个字节(函数标识符)。
msgValue = msg.value;// (uint)这个消息所附带的货币量,单位为wei。
now1 = now;// (uint)当前块的时间戳,等同于block.timestamp
gasPrice = tx.gasprice;// (uint) 交易的gas价格。
txOrigin = tx.origin;// (address)交易的发送者(完整的调用链)
}
}
3. 错误处理
在创建合约时设置owner(合约的所有人)
pragma solidity ^0.4.24;
contract Test {
address public owner;
constructor() public {
owner = msg.sender;
}
}
传统方法:采用 throw 和 if ... throw 模式(已过时),例如合约中有一些功能,只能被授权为拥有者的地址才能调用
if(msg.sender != owner) {
throw;
}
等价于如下任意一种形式:
if(msg.sender != owner) {
revert();
}
//assert和require是推荐的方式,里面的参数要求值为true,即期望的结果
assert(msg.sender == owner);
require(msg.sender == owner);
示例:
//描述编译器版本
pragma solidity ^0.4.24;
contract Inbox{
//定义变量:类型 + 变量名
string public message; // var name string
address public manager; //合约的部署者(拥有者)
address public caller; //合约函数的调用者
function Inbox() payable {
manager = msg.sender;
}
function setMessage(string newMessage) public {
caller = msg.sender;
// if (manager != msg.sender) {
// throw; //如果函数调用者不是管理员,直接抛异常
// }
// 断言:
// 1. 一条语句,既包含了条件,又可以抛异常(推荐)
// 2. 条件是期望的结果,与普通的条件判断相反
// (条件为true,继续执行,条件为false,抛出异常)
// require(manager == msg.sender);
assert(manager == msg.sender);
message = newMessage;
}
//如果有返回值,一定要加上returns关键字,使用()包裹起来
function getMessage() public constant returns(string){
return message;
}
}
4. 修饰器(modifier)
修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写(override)。下面我们来看一段示例代码:
//描述编译器版本
pragma solidity ^0.4.24;
contract Inbox{
//定义变量:类型 + 变量名
string public message; // var name string
address public manager; //合约的部署者(拥有者)
address public caller; //合约函数的调用者
function Inbox() payable {
manager = msg.sender;
}
//一个函数可以使用多个修饰器
function setMessage(string newMessage) public onlyManager onlyManager2(msg.sender){
caller = msg.sender;
message = newMessage;
}
//如果有返回值,一定要加上returns关键字,使用()包裹起来
function getMessage() public constant returns(string){
return message;
}
modifier onlyManager {
require(manager == msg.sender);
_; //下划线代表修饰器所修饰的代码
}
//修饰器可以带有参数
modifier onlyManager2(address _caller) {
require(manager == _caller);
_; //下划线代表修饰器所修饰的代码
}
}
5. 两个常用单位
- 货币单位
- 一个字面量的数字,可以使用后缀
wei
,finney
,szabo
或ether
来在不同面额中转换。 - 不含任何后缀的默认单位是
wei
。如1ether
== 1000finney
的结果是true
。
pragma solidity ^0.4.24;
contract EthUnit{
uint a = 1 ether;
uint b = 10 ** 18 wei;
uint c = 1000 finney;
uint d = 1000000 szabo;
function f1() constant public returns (bool){
return a == b;
}
function f2() constant public returns (bool){
return a == c;
}
function f3() constant public returns (bool){
return a == d;
}
function f4() constant public returns (bool){
return 1 ether == 100 wei;
}
}
- 时间单位
- seconds,minutes,hours,days,weeks,years均可做为后缀,默认是seconds为单位。
- 1 = 1 seconds
- 1 minutes = 60 seconds
- 1 hours = 60 minutes
- 1 days = 24 hours
- 1 weeks = 7 days
- 1 years = 365 days
pragma solidity ^0.4.0;
contract TimeUnit{
function f1() pure public returns (bool) {
return 1 == 1 seconds;
}
function f2() pure public returns (bool) {
return 1 minutes == 60 seconds;
}
function f3() pure public returns (bool) {
return 1 hours == 60 minutes;
}
function f4() pure public returns (bool) {
return 1 days == 24 hours;
}
function f5() pure public returns (bool) {
return 1 weeks == 7 days;
}
function f6() pure public returns (bool) {
return 1 years == 365 days;
}
}
6. 事件(Event)
相当于打印log,但是需要在调用端才能看到,web3调用时演示
pragma solidity ^0.4.0;
contract ClientReceipt {
//定义,注意,需要加分号,相当于一句语句,与struct和enum不同。
//类似于定义函数原型
event Deposit(
address indexed _from,
uint indexed _id,
uint _value
);
function deposit(uint _id) {
//使用
Deposit(msg.sender, _id, msg.value);
//TODO
}
}
7. 访问函数(Getter Functions)
编译器为自动为所有的public的状态变量
创建访问函数。下面的合约例子中,编译器会生成一个名叫data的无参,返回值是uint的类型的值data。状态变量的初始化可以在定义时完成。
pragma solidity ^0.4.24;
contract Test {
// 加了public 的转态变量,solidity会自动的生成一个同名个访问函数。
// 在合约内部使用这个状态变量的时候,直接当初变量使用即可, 不能直接当成方法使用
// 如果在合约外面向访问这个public变量(data),就需要使用xx.data()形式
uint256 public data = 200;
function getData() public view returns(uint256) {
return data;
}
//This代表合约本身,如果在合约内部使用this自己的方法的话,相当于外部调用
function getData1() public view returns(uint256) {
//return this.data; //不能使用.data形式
return this.data();
}
}
contract Test1 {
function getValue() public view returns(uint256) {
Test t1 = new Test();
return t1.data();
}
}
8. 合约(重要)
- 创建合约和外部调用
- new关键字,返回值是一个address,
需要显示转化类型
后才能使用 - C c1形式,此时c1是空的,需要赋值地址才能使用,否则报错
demo:
pragma solidity ^0.4.24;
contract C1 {
uint256 public value ;
constructor(uint256 input) public {
value = input;
}
function getValue() public view returns(uint256) {
return value;
}
}
contract C2 {
C1 public c1; //0x0000000000000
C1 public c11; //0x0000000000000
C1 public c13;
function getValue1() public returns(uint256) {
//创建一个合约,返回地址
address addr1 = new C1(10); //balance , transfer方法
//return addr1.getValue();
//需要显示的转换为特定类型,才可以正常使用
c1 = C1(addr1);
return c1.getValue();
}
function getValue2() public returns(uint256) {
//定义合约的时候,同时完成类型转换
c11 = new C1(20);
return c11.getValue();
}
function getValue3(address addr) public returns(uint256) {
//传进来的地址必须是同类型的,如果是不是C1类型的,转换时报错
c13 = C1(addr);
return c13.getValue();
}
}
- 继承
- is关键字, 可以同时继承多个父合约。
- 当父合约存在同名函数时,默认为最远继承原则
- 可以指定某个父合约,调用它的方法
pragma solidity ^0.4.24;
contract baseA {
function getData() public pure returns(uint256) {
return 1;
}
}
contract baseB {
function getData() public pure returns(uint256) {
return 2;
}
}
contract sonA is baseA, baseB {
function getSonData() public pure returns(uint256){
return 3;
}
function getData3() public pure returns(uint256) {
return baseA.getData();
}
}
- 合约间如何转钱
这个是官方示例,介绍如何使用一个合约向另一个合约转账。
pragma solidity ^0.4.24;
contract TestA {
string public message;
function invest(string _input) payable public {
message = _input;
}
function getBalanceA() public view returns(uint256) {
return address(this).balance;
}
}
contract TestB {
TestA public a1;
constructor() public {
a1 = new TestA();
}
function() public payable {
}
function pay() public {
//TestB调用TestA的invest方法时,如何转账给TestA?
//把TestB的钱转给TestA, 并不是调用pay函数人的钱
a1.invest.value(5 ether).gas(21000)("hangtou!");
}
function getBalanceB() public view returns(uint256) {
return address(this).balance;
}
}
9. internal和external
访问函数有外部(external)可见性。如果通过内部(internal)的方式访问,比如直接访问,你可以直接把它当一个变量进行使用,但如果使用外部(external)的方式来访问,如通过this.,那么它必须通过函数的方式来调用。
pragma solidity ^0.4.24;
//private , intenal , external, public
//合约本身可以调用, 合约及子类可以调用, 只能在合约外部调用, 可以被任意的合约调用
contract C1{
uint public c = 10;
function accessPrivate() private returns(uint) {
return c;
}
function accessInternal() internal returns (uint){
return c;
}
function accessExternal() external returns(uint){
return c;
}
function call1() public returns(uint) {
// accessExternal(); //无法在内部调用external修饰的函数
accessInternal();
}
function call2() public {
this.accessExternal(); //this调用函数,相当于外部调用
// this.c; // ok
// uint a = this.c; // error
uint b = this.c(); // ok
// c();
}
function call3() public returns(uint) {
}
}
contract C2{
function callExternal() public returns(uint){
C1 c1 = new C1();
// external修饰的只能在外部调用
return c1.accessExternal();
//internal修饰的只能在内部调用
// return c1.accessInternal();
}
}
contract C3 is C1 {
function test() public returns(uint) {
// C1 c1 = new C1();
// c1.accessPrivate();
// this.accessInternal(); //error
// c1.accessInternal(); // error
return accessInternal();
}
}
10. 元组(tuple)
return(a, b, c)
solidity无法返回自定义的数据结构,所以若想返回一个自定义结构的数据,需要在函数中一次返回多个值,即元组。元组是一个数据集合,类似于字典但是无法修改数据,使用圆括号包括多种数据类型。
- 可以包含多个数据
- 类型可以不同
- 不可以修改
- 使用圆括号包裹
pragma solidity ^0.4.5;
contract Test {
struct Student {
string name;
uint age;
uint score;
string sex;
}
//两种赋值方式
Student public stu1 = Student("lily", 18, 90, "girl");
Student public stu2 = Student({name:"Jim", age:20, score:80, sex:"boy"});
Student[] public Students;
function assign() public {
Students.push(stu1);
Students.push(stu2);
stu1.name = "Lily";
}
//1. 返回一个Student结构
function getLily() public view returns(string, uint, uint, string) {
require(Students.length != 0);
Student memory lily = Students[0];
//使用圆括号包裹的多个类型不一致的数据集合:元组
return (lily.name, lily.age, lily.score, lily.sex);
}
}
11. 内置数学函数
keccak256(...) returns (bytes32)
哈希函数,代替sha3(废弃)
pragma solidity ^0.4.24;
contract Test {
function test() public pure returns(bytes32){
bytes memory v1 = abi.encodePacked("abc", "b", uint256(1), "hello");
return keccak256(v1);
}
}
12. 其他
for、break、continue
- new
创建对象,合约等
- delete
- delete操作符可以用于任何变量(map除外),将其设置成默认值
- 如果对动态数组使用delete,则删除所有元素,其长度变为0: uint[ ] array0 ; arry0 = new uint
- 如果对静态数组使用delete,则重置所有索引的值: uint[10] array1 = [1,2,3,4,5,6];
- 如果对map类型使用delete,什么都不会发生
- 但如果对map类型中的一个键使用delete,则会删除与该键相关的值
pragma solidity ^0.4.24;
contract Test {
//01. string
string public str1 = "hello";
function deleteStr() public {
delete str1;
}
function setStr(string input) public {
str1 = input;
}
//02. array 对于固定长度的数组,会删除每个元素的值,但是数组长度不变
uint256[10] public arry1 = [1,2,3,4,5];
function deleteFiexedArry() public {
delete arry1;
}
//03. array new
uint256[] arry2 ;
function setArray2() public {
arry2 = new uint256[](10);
for (uint256 i = 0; i< arry2.length; i++) {
arry2[i] = i;
}
}
function getArray2() public view returns(uint256[]) {
return arry2;
}
function deleteArray2() public {
delete arry2;
}
//04. mapping
mapping(uint256 => string) public m1;
function setMap() public {
m1[0] = "hello";
m1[1] = "world";
}
//Mapping不允许直接使用delete,但是可以对mapping的元素进行指定删除
// function deleteM1() public {
// delete m1;
// }
function deleteMapping(uint256 i) public {
delete m1[i];
}
}
13. 编码规范
- public放到最前面
- 函数参数加下划线
14.合约销毁
- selfdestruct(msg.sender); 可以销毁合约,并将合约内的金额转给指定的地址。
- 合约销毁后,不再工作,无法继续调用其方法。
- 合约销毁,并不是将合约删除,只是不工作。
- 合约销毁权限一定要控制好。
pragma solidity ^0.4.24;
contract Test {
string public name;
address manager;
constructor(string _input) public payable {
name = _input;
manager = msg.sender;
}
function getBalance() public view returns(uint256) {
return address(this).balance;
}
function kill() public {
require(manager == msg.sender) ;
selfdestruct(msg.sender);
}
}
六、智能合约案例
1. 查看代币合约
2. 代币源码
- ERC20标准
合约中实现这些标准接口函数
contract ERC20 {
function totalSupply() constant returns (uint totalSupply);
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
function transferFrom(address _from, address _to, uint _value) returns (bool success);
function approve(address _spender, uint _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns (uint remaining);
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approveal(address indexed _owner, address indexed _spender, uint _value);
string public constant name = "Token Name";
string public constant symbol = "SYM";
uint8 public constant decimals = 18; // 大部分都是18
}
- 参考链接
-
以太坊:什么是ERC20标准?
-
以太坊ERC20 Token标准完整说明
https://blog.csdn.net/diandianxiyu_geek/article/details/78082551?utm_source=gold_browser_extension
- 查看真实的代币情况
//演示
- 分析BNB代码
pragma solidity ^0.4.8;
/**
* Math operations with safety checks
*/
contract SafeMath {
//internal > private
//internal < public
//修饰的函数只能在合约的内部或者子合约中使用
//乘法
function safeMul(uint256 a, uint256 b) internal returns (uint256) {
uint256 c = a * b;
//assert断言函数,需要保证函数参数返回值是true,否则抛异常
assert(a == 0 || c / a == b);
return c;
}
//除法
function safeDiv(uint256 a, uint256 b) internal returns (uint256) {
assert(b > 0);
uint256 c = a / b;
// a = 11
// b = 10
// c = 1
//b*c = 10
//a %b = 1
//11
assert(a == b * c + a % b);
return c;
}
//减法
function safeSub(uint256 a, uint256 b) internal returns (uint256) {
assert(b <= a);
assert(b >=0);
return a - b;
}
function safeAdd(uint256 a, uint256 b) internal returns (uint256) {
uint256 c = a + b;
assert(c>=a && c>=b);
return c;
}
}
contract QiongB is SafeMath{
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
address public owner;
/* This creates an array with all balances */
mapping (address => uint256) public balanceOf;
mapping (address => mapping (address => uint256)) public allowance;
mapping (address => uint256) public freezeOf;
/* This generates a public event on the blockchain that will notify clients */
event Transfer(address indexed from, address indexed to, uint256 value);
/* This notifies clients about the amount burnt */
event Burn(address indexed from, uint256 value);
/* This notifies clients about the amount frozen */
event Freeze(address indexed from, uint256 value);
/* This notifies clients about the amount unfrozen */
event Unfreeze(address indexed from, uint256 value);
/* Initializes contract with initial supply tokens to the creator of the contract */
//1000000, "QiongB", 18, "QB"
function QiongB(
uint256 initialSupply, //发行数量
string tokenName, //token的名字 BinanceToken
uint8 decimalUnits, //最小分割,小数点后面的尾数 1ether = 10** 18wei
string tokenSymbol //QB
) {
decimals = decimalUnits; // Amount of decimals for display purposes
balanceOf[msg.sender] = initialSupply * 10 ** 18; // Give the creator all initial tokens
totalSupply = initialSupply * 10 ** 18; // Update total supply
name = tokenName; // Set the name for display purposes
symbol = tokenSymbol; // Set the symbol for display purposes
owner = msg.sender;
}
/* Send coins */
//某个人花费自己的币
function transfer(address _to, uint256 _value) {
if (_to == 0x0) throw; // Prevent transfer to 0x0 address. Use burn() instead
if (_value <= 0) throw;
if (balanceOf[msg.sender] < _value) throw; // Check if the sender has enough
if (balanceOf[_to] + _value < balanceOf[_to]) throw; // Check for overflows
balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value); // Subtract from the sender
balanceOf[_to] = SafeMath.safeAdd(balanceOf[_to], _value); // Add the same to the recipient
Transfer(msg.sender, _to, _value); // Notify anyone listening that this transfer took place
}
/* Allow another contract to spend some tokens in your behalf */
//找一个人A帮你花费token,这部分钱并不打A的账户,只是对A进行花费的授权
//A: 1万
function approve(address _spender, uint256 _value)
returns (bool success) {
if (_value <= 0) throw;
//allowance[管理员][A] = 1万
allowance[msg.sender][_spender] = _value;
return true;
}
//mapping (管理员 => mapping (A =>1万)) public allowance;
//mapping (管理员 => mapping (B =>2万)) public allowance;
/* A contract attempts to get the coins */
function transferFrom(address _from /*管理员*/, address _to, uint256 _value) returns (bool success) {
if (_to == 0x0) throw; // Prevent transfer to 0x0 address. Use burn() instead
if (_value <= 0) throw;
if (balanceOf[_from] < _value) throw; // Check if the sender has enough
if (balanceOf[_to] + _value < balanceOf[_to]) throw; // Check for overflows
if (_value > allowance[_from][msg.sender]) throw; // Check allowance
// mapping (address => mapping (address => uint256)) public allowance;
balanceOf[_from] = SafeMath.safeSub(balanceOf[_from], _value); // Subtract from the sender
balanceOf[_to] = SafeMath.safeAdd(balanceOf[_to], _value); // Add the same to the recipient
//allowance[管理员][A] = 1万-五千 = 五千
allowance[_from][msg.sender] = SafeMath.safeSub(allowance[_from][msg.sender], _value);
Transfer(_from, _to, _value);
return true;
}
function burn(uint256 _value) returns (bool success) {
if (balanceOf[msg.sender] < _value) throw; // Check if the sender has enough
if (_value <= 0) throw;
balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value); // Subtract from the sender
totalSupply = SafeMath.safeSub(totalSupply,_value); // Updates totalSupply
Burn(msg.sender, _value);
return true;
}
function freeze(uint256 _value) returns (bool success) {
if (balanceOf[msg.sender] < _value) throw; // Check if the sender has enough
if (_value <= 0) throw;
balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value); // Subtract from the sender
freezeOf[msg.sender] = SafeMath.safeAdd(freezeOf[msg.sender], _value); // Updates totalSupply
Freeze(msg.sender, _value);
return true;
}
function unfreeze(uint256 _value) returns (bool success) {
if (freezeOf[msg.sender] < _value) throw; // Check if the sender has enough
if (_value <= 0) throw;
freezeOf[msg.sender] = SafeMath.safeSub(freezeOf[msg.sender], _value); // Subtract from the sender
balanceOf[msg.sender] = SafeMath.safeAdd(balanceOf[msg.sender], _value);
Unfreeze(msg.sender, _value);
return true;
}
// transfer balance to owner
function withdrawEther(uint256 amount) {
if(msg.sender != owner)throw;
owner.transfer(amount);
}
// can accept ether
function() payable {
}
}
七、验证(verify)合约
Ropste Duke AD,已经Verify
https://ropsten.etherscan.io/address/0x8136d63e3c1e3e8e93560a965c635f2704ce7c22#code
1. 为什么要验证合约
部署了一个合约之后,如果想让大家参与进来,那么必须接受大家的审计(审计一定要部署完就做,这样可以保证完全匹配),以确保你的合约的功能确实如你所说,全世界的人都看到了合约的所有功能,那么就可以放心使用了
2. 验证方式
//操作 ,优化填否
3. 操作验证后的合约
- 浏览器上的数据是如何解析的?
注意:
- 在remix部署到ropsten,将合约的地址添加到metamask的AddToken中,只输入地址即可,其他两项(name,decimals会自动加载出来)
- metamask中只能查看token数量,但是无法进行token交易
- 可以使用Myetherwallet页面网站来进行转账
- 可以使用MyEtherWallet客户端进行转账
- 可以在验证合约之后,在浏览器上直接完成转账。
4. 以太坊上的数据为什么能被看到?
所有的数据都是通过交易产生的,无论是上传数据,亦或是调用方法,那么传入的数据都是通过inputdata传递进来的,而这个字段是公开的,所有所有的数据都是透明的。
END
2020年07月22日12:48:08
以上是关于79solidity语言学习——2020年07月22日12:49:06的主要内容,如果未能解决你的问题,请参考以下文章
54go mod使用——2020年07月11日15:46:14