Web3与Solidity基础
本教程是技术大牛 luofei614 的DApp开发学习笔记,编成教程分享给开发者。

1、web3的基础使用

web3.js 是以太坊官方发布的与以太坊交互的js库。
上面示例中已经有初步接触, 我们可以在VUE中使用web3 也可以在truffle控制台使用web3进行代码调试。
下面说下web3的常用方法:
  • 获得账户
web3.eth.getAccounts()
  • 查看余额
web3.eth.getBalance("accountAddress")
  • 账户转账
web3.eth.sendTransaction({from:"fromaddress",to:"toaddress",value:1000})
还有跟交易相关的gas , nonce等参数, 都会自动生成或者也可以指定覆盖。
  • 单位转换
web3.utils.toWei("1","ether"); //将1ETH转换为wei单位
web3.utils.fromWei("20000000000","ether");//将wei单位转换为ETH
web3.utils.toBN("123");
  • 获得协议版本
web3.eth.getProtocolVersion()
  • 获得当前gas价格
web3.eth.getGasPrice()
  • 获得交易数量
web3.eth.getTransactionCount("address")
  • 预估执行合约要的gas数量
instance.functionName.estimateGas(args)
这里instance是truffle的合约对象。传参args 是functionName的传参。

2、solidity的基础使用

solidity是编写智能合约的编程语言,solidity代码通过编译器编译为EVM字节码, 以太坊区块上有个EVM虚拟机机制可以执行EVM的字节码。
上面在部署水龙头合约时已经初步接触了solidity,这一小节详细讲解一下solidity。

2.1合约结构

定义一个简单的合约
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.0 <0.8.0;
contract Mytest {
uint storednumber=0;
address payable owner;
event LogNumber(uint storednumber);
constructor(){
owner=msg.sender;
}
function test(uint number) public payable {
storednumber=helper(number);
emit LogNumber(storednumber);
}
function destory() public {
require(msg.sender == owner);
selfdestruct(owner);
}
}
function helper(uint x) pure returns (uint) {
return x * 2;
}
第一行是声明开源协议
第二行声明代码适合的编译器版本
第三行contract关键词定义合约
合约中可以包括:状态变量, 函数, 事件。

2.2状态变量

状态变量是永久地存储在合约存储中的值, 我们可以把合约理解成为一个单例的程序,constructor 构造函数只会在第一次合约创建时调用,所以合约中定义的变量就像单例对象的属于一样,值会全局一直保存。一般在构造函数中存储合约的创建者地址,方便合约后期做一些只能创建者操作的权限判断。
变量的数据类型有:
  • 布尔值bool,值为true或false
  • 整数型int, uint(无符合),声明长度,以8比特为单位, int8到uint256 ,如果没有定义长度,默认是256
  • 浮点数fixed ,ufixed , 定义方式(ufixedMxN), M是整数的比特位数,N是小数位数
  • 地址address, address有成员函数balance,transfer等 , 地址类型要能支付需要在定义时用payable修饰或者在使用时用payable函数转换
  • 字节数组(固定), bytes1 到 bytes32, 一个英文字母会占一个byte
  • 字节数组(动态) bytes, string , 字符串没有直接支持中文。 中文要utf8编码后才能赋值给变量。
    • 字符串拼接, 没有直接拼接的方法, 需要封装一个函数:
function strConcat(string memory _a, string memory _b) internal pure returns (string memory){
bytes memory _ba = bytes(_a);
bytes memory _bb = bytes(_b);
string memory ret = new string(_ba.length + _bb.length);
bytes memory bret = bytes(ret);
uint k = 0;
for (uint i = 0; i < _ba.length; i++) bret[k++] = _ba[i];
for (uint i = 0; i < _bb.length; i++) bret[k++] = _bb[i];
return string(ret);
}
  • 枚举类型 enum Name {Labe1,labe2}
  • 数组 ,如unit32[][5]
    • 数组的添加
      • arrayName.push() ; 添加数组
    • 数组的删除
      • delete arrayName[1]; ,这个是清空值: 数字为变为0,地址会变为0地址,位置还存在,需要手动移动位置,如:
uint[] array = [1,2,3,4,5];
uint index=3;
delete array[index];
for (uint i = index; i<array.length-1; i++){
array[i] = array[i+1];
}
array.pop();//删除最后一个函数
网上有文章讲array.length--改变长度的,0.6后不支持array.length-- 改变长度,需要array.pop 删除最后一个元素。
注意和js语言的pop函数不同的是,这里不会返回最后一个元素,也不能传参制定索引。
    • 数组的遍历
for(uint i=0;i<arr.length;i++){
arr[i]; //获得数组的值
}
arr.length是属于uint类型的。
数组中的值只能是标量,不能为复杂的struct结构, 可以结合映射实现复杂的例子,这个例子可以学习一下: https://ethereum.stackexchange.com/questions/12611/solidity-filling-a-struct-array-containing-itself-an-array/12614
  • 结构:struct NAME{TYPE1 NAME1; TYPE2 NAME2}
  • 映射: mapping(KEYTYPE => VALUETYPE) NAME;
  • 变量修饰符 memory、storage、calldata。
状态变量会强制为storage类型, 外部传参的参数默认会为calldata类型。 calldata类型只能只读。 memory类型是内存临时变量,执行完后就会被释放, 一般函数类定义的变量会为memory类型的。

2.3函数

形式: function FunctionName([parameters]) {public|external|private|internal} [pure|constant|view|playable] [modifiers] [returns (return types)]
下面对各部分说明一下。
  • 可见性说明
solidity函数的可见性定义在函数名之后,且是必须定义的。
    • public 内部外部都可以调用
    • external 外部可调用,内部要用this调用 , external比public更省gas
    • internal 只能合约内部调用,子类也可以调用
    • private 只能合约内部调用,子类不能调用
  • 函数行为声明
    • view , 声明不会改变区块链状态(修改状态变量),只会读取状态变量,可以return 返回数据
    • prue, 声明不会改变区块链状态,也不读取状态变量,只处理计算就返回结果。
    • payable, 接受支付以太坊。
    • 不声明,表示non-payable 类型,表示会改变区块链状态但不支持支付。
注:view 和 prue 是不会产生gas费用的,因为他不用旷工确认,多声明view或prue,这是在省钱。
  • 函数修饰符modifier
contract Mytest {
event printString(string s);
modifier onlyOwner{
require(msg.sender == owner);
_;
emit printString("runing");
}
function testmodifer() public onlyOwner{
emit printString("body");
}
}
函数修饰符常用于验证条件的封装,下划线"_" 表示要替换代码的body部分。

2.4事件

用event声明,用emit触发,会记录到区块链的交易日志中,参数可以添加indexed关键词作为索引查询web3可以查询
event Withdrawal(adress indexed to,uint amount);
....
emit Withdrawal(msg.sender,123);
我们经常也会用事件来调试, 事件日志在合约调用也是会显示在remix控制台的。

2.5错误处理

  • require , 不符合条件就会抛出异常, 第二个参数可以写错误原因
require(msg.sender==owner,"Only the contract owner can call this function");
  • revert ,支持抛出异常和输出错误信息
if(msg.sender!=owner){
revert("Only the contract owner can call this function");
}

2.6全局变量

  • msg对象
    • msg.sender 调用者地址,可以是外部账户也可以是合约账户(合约之间可以互相调用)
    • msg.value 以太坊数量,单位wei
    • msg.gas 新版是用gasleft()函数获得要剩余的gas。
    • msg.data 传入的数据 bytes类型
    • msg.sig 签名, 类型是bytes4,其实是函数选择器的字符,EVM通过这个知道执行那个函数。
  • tx对象, 交易相关信息
    • tx.gasprice 交易价格。
    • tx.origin 交易发起的外部账户。
  • block区块对象
    • blockhash(blockNumber)函数,指定区块ID返回区块hash,类型是bytes32;
    • block.coinbase 矿工地址
    • block.difficulty 当前区块正明难度
    • block.gaslimit , 当前区块花费的最大gas
    • block.number , 当前区块编号
    • block.timestamp , 当前区块写入的时间戳
  • address,地址对象
    • address.balance 余额
    • address.transfter(number) 向这个地址转账
    • address.send(number) 与transfter 类似, 出错是不会抛出异常,会返回false
    • call/callcode/delegatecall 调用合约
这几个方法都是比较旧的方式, 新方式可以把合约对象作为传参,然后显性调用, 如:
contract Mybase{
function run() public pure returns(string memory){
return "test";
}
}
contract Mytest {
event printString(string s);
function testcall(Mybase my) public {
emit printString("run start");
string memory s = my.run();
emit printString(s);
emit printString("run end");
}
}
这my这个参数在调用的时候传递合约地址。 其实这个合约地址下只要有run方法就行。EVM是无法识别合约是不是属于Mybase的。

2.7接口和库

接口
interface InterfaceBase {
//抽象方法
function brand() external returns (bytes32);
}
contract MyContract is InterfaceBase{
function brand() external override pure returns (bytes32){
return '123';
}
}
接口定义的方法要用external不能用public
实现接口的方法要用override修饰
solidity 是用is实现继承的, is可以跟多个类来继承
库:
library Lib {
function test(string memory s) external pure returns(string memory){
return s;
}
}
contract MyContract {
function run() external pure{
Lib.test("xxx"); //调用库的方法
}
}

2.8导入文件

父类或库独立成单独的文件方便代码管理,然后可以用import导入。
比如独立 lib.sol 文件
导入:
import "./lib.sol";
这样导入文件中所有类会被导入, 为了防止有名称冲突可以:
import * as lib from "./lib.sol"
也可以自定导入文件中的几个对象
import {Lib,InterfaceBase} from "./lib.sol"
solidity的导入语法是遵循ES6规范的。

2.9让合约可以接受支付

合约中加入如下代码:
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}

2.10合约被调用到不存在的方法的处理

fallback() external {
//处理代码
}
可以接受calldata直接调用:

2.11合约摧毁

调用函数 selfdestrunct 可以摧毁合约, 摧毁时会把合约的余额打入到函数指定的地址,并奖励gas, 这个机制是激励开发回收不用的合约,虽然合约摧毁了,但是交易记录依然永久存在区块链上。
如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.0 <0.8.0;
contract Mytest {
address payable owner;
constructor(){
owner=msg.sender;
}
function destory() public {
require(msg.sender == owner);
selfdestruct(owner);
}
}
调用合约的destory方法就可以摧毁合约。 摧毁的是合约的代码的并不会摧毁合约地址。 所以看见的现象会是,合约还能调用,而且还能往合约地址打以太坊, 合约地址成为了黑户,如果往里面打以太坊将取不回。 所以摧毁合同是危险的,还是不要轻易摧毁。
更多学习,看文档: https://learnblockchain.cn/docs/solidity/index.html