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

Last updated