web3的技术栈
- 首先需要明确一个概念:
Web3是支持完全去中心化应用程序的协议栈。
web3技术栈如下图。
以下是一个不完整的区块链样本,它结合了技术、效用、社区、势头和未来可行性:
- 以太坊——原始智能合约平台
- ZK汇总:ZKSync, Starknet, Hermez -高吞吐量以太坊 Layer2(以太坊解决方案的统称),但不兼容EVM
- Optimistic汇总:Arbitrum & Optimism -以太坊 Layer2,与EVM兼容(在这里了解更多关于Optimistic和ZK之间的差异)
- Polygon-以太坊侧链
- Solana -高吞吐量,便宜的手续费,快速的阻塞时间,但比EVM (Rust)更难学习
- NEAR - Layer 1 区块链,可以在Rust或Assemblyscript中编写智能合约
- Cosmos - 一个互操作区块链的生态系统
- Polkadot -基于区块链的计算平台,使构建在其上的区块链能够在它们之间执行交易,创建一个互联的区块链网络
- Fantom - EVM兼容 Layer2
- Avalanche- EVM兼容 Layer2
- Celo - EVM兼容 Layer2,旨在使任何拥有智能手机的人都可以轻松地发送、接收和存储加密
- Tezos -非EVM兼容 Layer2,很多NFT项目都在使用它
当与网络交互时,需要使用RPC端点。
有几种方法可以做到这一点:
- 访问公共RPC端点
- 运行自己的节点
- 将节点提供者作为服务访问
- 将分散节点提供者作为服务访问
公共RPC端点通常由网络提供,但是对于大多数生产dapp,您希望利用自己的端点,因为它们不是稳定的,也不推荐用于生产。
有一些RPC服务提供商在那里,这里有一些:
Layer1和Layer2
像我们所熟悉的比特币网络、以太坊主网等主流公链都属于Layer 1的范畴。
通俗来说,在以太坊网络中,Layer 1的主要作用就是确保网络安全、去中心化及最终状态确认,做到状态共识,并作为一条公链网络中可信的“加密法院”,通过智能合约设计的规则进行仲裁,以经济激励的形式将信任传递到 Layer2 上。
而Layer2 则以追求更高效的性能为终极目标,从上面区块链技术逻辑架构示意图中,我们可以看到,作为第二层网络,可以替 Layer1 承担大部分计算工作,近年来,不少项目都是基于Layer2搭建的,从而将交易行为从主链上分离出来,降低一层网络的负担,提高业务处理效率,从而实现扩容。在这个过程中,Layer2 虽然只做到了局部共识,但是基本可以满足各类场景的需求。
目前行业内比较贴切的是将Layer1和Layer2的关系和中央银行与商业银行的关系来类比:把Layer1承担着中央银行的角色,而layer2则是各大商业银行。 在现行主流的金融系统中,所有的资产都必须在中央银行结算,而具体的流通过程可以同时发生在中央银行和商业银行。因为如果所有人都去央行结算的话,势必会发生业务拥堵的情况,更好的解决办法当然是由商业银行来先处理大量交易业务,然后由各个商业银行和中央银行结算一次整体业务,这样才能使得整个金融系统更加高效有序的运转起来。所以从中我们能够得到的启示就是,对于在以太坊网络中存在的交易拥堵、手续费居高不下的问题,一个可行的解决方案就出炉了——将以太坊的资产存入Layer2,之后的资产流动交易环节都在Layer2上进行,只把最终结算过程放到Layer1上就可以了。
以太坊的JavaScript API —— web3.js
web3.js 是以太坊的JavaScript API集合。
web3.js可以:
- 直接以太从一个帐户到另一个帐户
- 从智能合约中交付和写入数据
- 制定智能合约
- 更多其他工作
• web3.js 通过
RPC调用与本地节点通信,它可以用于任何暴露了RPC层的以太坊节点
• web3 包含 eth 对象 - web3.eth(专门与以太坊区块链交互)和 shh 对象 - web3.shh(用于与 Whisper 交互)
常用 API —— 基本信息查询 查看 web3 版本
- v0.2x.x:web3.version.api
- v1.0.0:web3.version
查看 web3 连接到的节点版本(clientVersion)
- 同步:web3.version.node
- 异步:web3.version.getNode((error,result)=>{console.log(result)})
- v1.0.0:web3.eth.getNodeInfo().then(console.log)
基本信息查询 获取 network id
- 同步:web3.version.network
- 异步:web3.version.getNetwork((err, res)=>{console.log(res)})
- v1.0.0:web3.eth.net.getId().then(console.log)
获取节点的以太坊协议版本
- 同步:web3.version.ethereum
- 异步:web3.version.getEthereum((err, res)=>{console.log(res)})
- v1.0.0:web3.eth.getProtocolVersion().then(console.log)
网络状态查询 是否有节点连接/监听,返回true/false
- 同步:web3.isConnect() 或者 web3.net.listening
- 异步:web3.net.getListening((err,res)=>console.log(res))
- v1.0.0:web3.eth.net.isListening().then(console.log)
查看当前连接的 peer 节点
- 同步:web3.net.peerCount
- 异步:web3.net.getPeerCount((err,res)=>console.log(res))
- v1.0.0:web3.eth.net.getPeerCount().then(console.log)
Provider
查看当前设置的 web3 provider
- web3.currentProvider
查看浏览器环境设置的 web3 provider(v1.0.0) • web3.givenProvider 设置 provider
- web3.setProvider(provider)
- web3.setProvider(new web3.providers.HttpProvider(‘http://localhost:8545’))
web3 通用工具方法
以太单位转换
- web3.fromWei web3.toWei
数据类型转换
- web3.toString web3.toDecimal web3.toBigNumber
字符编码转换
- web3.toHex web3.toAscii web3.toUtf8 web3.fromUtf8
地址相关
- web3.isAddress web3.toChecksumAddress
web3.eth – 账户相关
coinbase 查询
- 同步:web3.eth.coinbase
- 异步:web3.eth.getCoinbase( (err, res)=>console.log(res) )
- v1.0.0:web3.eth.getCoinbase().then(console.log)
账户查询
- 同步:web3.eth.accounts
- 异步:web3.eth.getAccounts( (err, res)=>console.log(res) )
- v1.0.0:web3.eth.getAccounts().then(console.log)
区块相关
区块高度查询
- 同步:web3.eth. blockNumber
- 异步:web3.eth.getBlockNumber( callback )
gasPrice 查询
- 同步:web3.eth.gasPrice
- 异步:web3.eth.getGasPrice( callback )
区块查询
- 同步:web3.eth.getBlockNumber( hashStringOrBlockNumber [ ,returnTransactionObjects] )
- 异步:web3.eth.getBlockNumber( hashStringOrBlockNumber, callback ) 块中交易数量查询
- 同步:
web3.eth.getBlockTransactionCount( hashStringOrBlockNumber ) - 异步:
web3.eth.getBlockTransactionCount( hashStringOrBlockNumber , callback )
交易相关
余额查询:
- 同步:web3.eth.getBalance(addressHexString [, defaultBlock])
- 异步:web3.eth.getBalance(addressHexString [, defaultBlock][, callback])
交易查询:
- 同步:web3.eth.getTransaction(transactionHash)
- 异步:web3.eth.getTransaction(transactionHash [, callback])
交易执行相关:
- 交易收据查询(已进块)
- 同步:web3.eth.getTransactionReceipt(hashString)
- 异步:web3.eth.getTransactionReceipt(hashString [, callback])
- 估计 gas 消耗量
- 同步:web3.eth.estimateGas(callObject)
- 异步:web3.eth.estimateGas(callObject [, callback])
发送交易:
- web3.eth.sendTransaction(transactionObject [, callback])
- 交易对象:
- from:发送地址
- to:接收地址,如果是创建合约交易,可不填
- value:交易金额,以wei为单位,可选
- gas:交易消耗 gas 上限,可选
- gasPrice:交易 gas 单价,可选
- data:交易携带的字串数据,可选
- nonce:整数 nonce 值,可选
消息调用
- web3.eth.call(callObject [, defaultBlock] [, callback])
- 参数:
- 调用对象:与交易对象相同,只是from也是可选的
- 默认区块:默认“latest”,可以传入指定的区块高度
- 回调函数,如果没有则为同步调用
例如:
var result = web3.eth.call({ to:
"0xc4abd0339eb8d57087278718986382264244252f",
data:
"0xc6888fa100000000000000000000000000000000000000000000000000
0 0000000000003" });
console.log(result);
日志过滤(事件监听)
web3.eth.filter( filterOptions [ , callback ] )
// filterString 可以是 'latest' or 'pending'
var filter = web3.eth.filter(filterString);
// 或者可以填入一个日志过滤 options
var filter = web3.eth.filter(options);
// 监听日志变化
filter.watch(function(error, result){ if (!error) console.log(result); });
// 还可以用传入回调函数的方法,立刻开始监听日志
web3.eth.filter(options, function(error, result){
if (!error) console.log(result);
});
合约相关 —— 创建合约
web3.eth.contract
var MyContract = web3.eth.contract(abiArray);
// 通过地址初始化合约实例
var contractInstance = MyContract.at(address);
// 或者部署一个新合约
var contractInstance = MyContract.new([constructorParam1]
[, constructorParam2], {data: '0x12345...', from:
myAccount, gas: 1000000});
调用合约函数
• 可以通过已创建的合约实例,直接调用合约函数
// 直接调用,自动按函数类型决定用 sendTransaction 还是 call
myContractInstance.myMethod(param1 [, param2, ...] [,
transactionObject] [, defaultBlock] [, callback]);
// 显式以消息调用形式 call 该函数
myContractInstance.myMethod.call(param1 [, param2, ...] [,
transactionObject] [, defaultBlock] [, callback]);
// 显式以发送交易形式调用该函数
myContractInstance.myMethod.sendTransaction(param1 [,
param2, ...] [, transactionObject] [, callback]);
监听合约事件
• 合约的 event 类似于 filter,可以设置过滤选项来监听
var event = myContractInstance.MyEvent({valueA: 23}
[, additionalFilterObject])
// 监听事件
event.watch(function(error, result){
if (!error) console.log(result);
});
//还可以用传入回调函数的方法,立刻开始监听事件
var event = myContractInstance.MyEvent(
[{valueA: 23}]
[, additionalFilterObject] ,
function(error, result){
if (!error) console.log(result);
});
如何使用 Solidity 和 JavaScript 测试智能合约
软件测试有两种常规类型:单元测试和集成测试。
- 单元测试地关注每个独立的功能。
- 集成测试重点在于确保代码的多个部分按预期在一起工作。
区块链软件也不例外。 而且由于不可变性,区块链应用程序需要更多地强调测试。
用Solidity编写智能合约的测试用例让我们可以在区块链层级进行测试。这种测试用例可以调用合约方法,就像用例部署在区块链里一样。为了测试智能合约的内部行为,我们可以:
- 编写Solidity单元测试来检查智能合约函数的返回值以及状态变量的值。
- 编写Solidity集成测试来检查智能合约之间的交互。这些集成测试可以确保像继承或者依赖注入这样的机制的运行符合预期
实例测试:github...
我们有两个合约: Background and EntryPoint 需要测试.
Background 是一个内部合约,DApp前端不会直接和它交互。EntryPoint 则是设计作为供DApp交互的智能合约,在EntryPoint合约会引用Background合约。
合约
Background合约代码如下:
pragma solidity >=0.5.0;
contract Background {
uint[] private values;
function storeValue(uint value) public {
values.push(value);
}
function getValue(uint initial) public view returns(uint) {
return values[initial];
}
function getNumberOfValues() public view returns(uint) {
return values.length;
}
}
在上面,我们看到Background合约提供了三个函数:
storeValue(uint):存值getValue(uint):读取值getNumberOfValues():获取值的
这三个合约函数都很简单,因此也很容易进行单元测试。
EntryPoint.sol 合约代码如下:
pragma solidity >=0.5.0;
import "./Background.sol";
contract EntryPoint {
address public backgroundAddress;
constructor(address _background) public{
backgroundAddress = _background;
}
function getBackgroundAddress() public view returns (address) {
return backgroundAddress;
}
function storeTwoValues(uint first, uint second) public {
Background(backgroundAddress).storeValue(first);
Background(backgroundAddress).storeValue(second);
}
function getNumberOfValues() public view returns (uint) {
return Background(backgroundAddress).getNumberOfValues();
}
}
在EntryPoint合约的构造函数中,使用了 Background 合约的部署地址,并将其存入一个状态变量backgroundAddress。EntryPoint合约暴露出三个函数:
getBackgroundAddress():返回Background合约的部署地址storeTwoValues(uint, uint):保存两个值getNumberOfValues():返回值的数量
storeTwoValues(uint, uint) 函数调用两次Background合约中的函数,因此对这个函数进行独立单元测试比较困难。getNumberOfValues()也有同样的问题,因此这两个函数更适合进行集成测试。
Solidity 测试用例
在 Solidity 测试用例中,我们将为智能合约编写Solidity单元测试用例和集成测试用例。 让我们先从简单一点的单元测试开始。
TestBackground 测试用例如下:
pragma solidity >=0.5.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
contract TestBackground {
Background public background;
// 在每个测试函数之前运行
function beforeEach() public {
background = new Background();
}
// Test that it stores a value correctly
function testItStoresAValue() public {
uint value = 5;
background.storeValue(value);
uint result = background.getValue(0);
Assert.equal(result, value, "It should store the correct value");
}
// Test that it gets the correct number of values
function testItGetsCorrectNumberOfValues() public {
background.storeValue(99);
uint newSize = background.getNumberOfValues();
Assert.equal(newSize, 1, "It should increase the size");
}
// Test that it stores multiple values correctly
function testItStoresMultipleValues() public {
for (uint8 i = 0; i < 10; i++) {
uint value = i;
background.storeValue(value);
uint result = background.getValue(i);
Assert.equal(result, value, "It should store the correct value for multiple values");
}
}
}
它测试了 Background 合约,确保它:
- 在
values数组中保存新的值 - 按索引返回
values - 在
values数组中保存多个值 - 返回
values数组的大小
下面是 TestEntryPoint , 包含了一个单元测试testItHasCorrectBackground() 用于验证EntryPoint合约的功能符合预期:
pragma solidity >=0.5.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
import "../../../contracts/EntryPoint.sol";
contract TestEntryPoint {
// Ensure that dependency injection working correctly
function testItHasCorrectBackground() public {
Background backgroundTest = new Background();
EntryPoint entryPoint = new EntryPoint(address(backgroundTest));
address expected = address(backgroundTest);
address target = entryPoint.getBackgroundAddress();
Assert.equal(target, expected, "It should set the correct background");
}
}
这个函数测试了注入的依赖。如前所述,EntryPoint合约中的其他函数需要与Background合约交互,因此我们没有办法单独测试这些函数,需要在集成测试中进行验证。下面是集成测试的代码:
pragma solidity >=0.5.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../../../contracts/Background.sol";
import "../../../contracts/EntryPoint.sol";
contract TestIntegrationEntryPoint {
BackgroundTest public backgroundTest;
EntryPoint public entryPoint;
// 在测试用例之前运行
function beforeEach() public {
backgroundTest = new BackgroundTest();
entryPoint = new EntryPoint(address(backgroundTest));
}
// Check that storeTwoValues() works correctly.
// EntryPoint contract should call background.storeValue()
// so we use our mock extension BackgroundTest contract to
// check that the integration workds
function testItStoresTwoValues() public {
uint value1 = 5;
uint value2 = 20;
entryPoint.storeTwoValues(value1, value2);
uint result1 = backgroundTest.values(0);
uint result2 = backgroundTest.values(1);
Assert.equal(result1, value1, "Value 1 should be correct");
Assert.equal(result2, value2, "Value 2 should be correct");
}
// Check that entry point calls our mock extension correctly
// indicating that the integration between contracts is working
function testItCallsGetNumberOfValuesFromBackground() public {
uint result = entryPoint.getNumberOfValues();
Assert.equal(result, 999, "It should call getNumberOfValues");
}
}
// Extended from Background because values is private in actual Background
// but we're not testing background in this unit test
contract BackgroundTest is Background {
uint[] public values;
function storeValue(uint value) public {
values.push(value);
}
function getNumberOfValues() public view returns(uint) {
return 999;
}
}
我们可以看到TestIntegrationEntryPoint使用了一个Background的扩展,即定义在第43行的 BackgroundTest,以其作为我们的模拟合约,这可以让我们的测试用例检查EntryPoint 函数是否调用了部署在backgroundAddress地址处的合约。
Javascript 测试文件
用JavaScript编写集成测试来确保合约的外部行为满足预期要求,这样我们就可以基于这些智能合约开发DApp了。
下面是我们的JavaScript测试文件 entryPoint.test.js:
const EntryPoint = artifacts.require("./EntryPoint.sol");
require('chai')
.use(require('chai-as-promised'))
.should();
contract("EntryPoint", accounts => {
describe("Storing Values", () => {
it("Stores correctly", async () => {
const entryPoint = await EntryPoint.deployed();
let numberOfValues = await entryPoint.getNumberOfValues();
numberOfValues.toString().should.equal("0");
await entryPoint.storeTwoValues(2,4);
numberOfValues = await entryPoint.getNumberOfValues();
numberOfValues.toString().should.equal("2");
});
});
});
使用EntryPoint合约中的函数,JavaScript测试用例可以将区块链外部的值通过交易传入智能合约,这是通过调用合约的storeTwoValues(uint,uint) 函数(第15行)实现的。
通过在测试的第12行和第16行调用getNumberOfValues()来检索存储在区块链中的值的数量,以确保和存储的值一致。
结论
在测试智能合约时,越多越好。 应该不遗余力确保所有可能的执行路径返回预期结果。 将链级Solidity测试用于单元测试和集成测试,并将Javascript测试用于DApp级别的集成测试。