Skip to main content

doc

  1. https://docs.soliditylang.org/zh/latest/introduction-to-smart-contracts.html
  2. https://ethereum.org/en/developers/
  3. https://docs.soliditylang.org/en/v0.8.25/metadata.html

call, delegatecall 和 staticcall​

在 Solidity 中,call,delegatecall,和staticcall是底层函数,用于与其他合约交互或在合约内部发送以太币。每个函数有不同的用途和特点:

  1. call

    • call用于向另一个合约发送以太币(ETH)和/或任意数据。它可以调用合约的函数,但不会改变调用者的存储。
    • call返回一个布尔值,表示操作成功与否,以及任何返回的数据。
    • 应用场景:当你想要调用另一个合约的函数,并且你可能不关心它的接口时。

    使用示例:

    (bool success, bytes memory data) = someContractAddress.call{value: etherAmount}(abi.encodeWithSignature("someFunction(uint256)", arg));
    require(success, "Call failed");
  2. delegatecall

    • delegatecall是一个低级别的函数,它调用另一个合约的函数,但是执行环境仍然是当前合约的,这意味着它使用当前合约的存储和 ETH 余额。
    • delegatecall返回一个布尔值和任何返回的数据。
    • 应用场景:在高级用例,如代理合约或库的函数调用,其中你希望在当前合约的上下文中执行代码,而不是在被调用合约的上下文中。

    使用示例:

    (bool success, bytes memory data) = someContractAddress.delegatecall(abi.encodeWithSignature("someFunction(uint256)", arg));
    require(success, "Delegatecall failed");
  3. staticcall

    • staticcall类似于call,但它保证不会改变状态,即它不会改变任何存储的数据。这是只读操作。
    • staticcall返回一个布尔值和任何返回的数据。
    • 应用场景:当你想要确保调用不会产生任何状态变化,例如查询另一个合约的状态而不引起副作用时。

    使用示例:

    (bool success, bytes memory data) = someContractAddress.staticcall(abi.encodeWithSignature("someFunction(uint256)", arg));
    require(success, "Staticcall failed");

安全注意事项:

  • 这些函数都跳过了 Solidity 的类型检查和函数签名检查,因此它们可能会增加智能合约的安全风险。使用它们时需要非常谨慎,确保调用的数据是正确的。
  • 由于它们允许与任意合约交互,因此存在重入攻击的风险。在执行这些调用后,合约应该小心处理状态变化。
  • 从 Solidity 0.5.0 版本开始,当发送 ETH 时,使用call的新语法(someAddress.call{value: amount}(""))比使用transfer或send更推荐,因为后两者在 gas 上有固定的限制,可能会因为 gas 不足而失败。

在实际应用中,除非有特定的底层需求,否则推荐使用 Solidity 的高级特性(如直接的函数调用和合约接口),因为它们更安全,也更容易理解和维护。

外部调用合约​

在 Solidity 中,"外部调用"(External Call)是指一个合约调用另一个合约的函数或者发送以太币到另一个地址的操作。这是合约与区块链上其他合约或账户交互的基本方式。

外部调用可以通过几种不同的方式进行:

  1. 直接调用: 如果你知道目标合约的地址和它的接口(ABI),你可以创建一个合约实例并直接调用它的方法。例如:

    // 假设已知外部合约的接口为SomeContract
    SomeContract externalContract = SomeContract(externalContractAddress);
    externalContract.someFunction();
  2. 通过接口调用: 你可以定义一个接口来表示外部合约的部分或全部函数,然后通过这个接口与合约通信。例如:

    interface IExternalContract {
    function someFunction() external;
    }

    IExternalContract externalContract = IExternalContract(externalContractAddress);
    externalContract.someFunction();
  3. 低级调用: 你也可以使用call、delegatecall、或staticcall等低级函数进行更灵活的调用,这些调用允许你指定调用的数据(例如函数选择器和参数)。例如:

    (bool success, bytes memory data) = externalContractAddress.call(abi.encodeWithSignature("someFunction(uint256)", 123));
    require(success, "Call failed");
  4. 发送以太币: 你可以使用transfer、send或call(推荐使用call以避免固定的 2300 gas 限制)来发送以太币。例如:

    // 推荐的方式来发送以太币
    (bool sent, ) = externalContractAddress.call{value: amount}("");
    require(sent, "Failed to send Ether");

外部调用是合约与外部世界交互的主要方式,但它们也增加了合约的复杂性和潜在的安全风险。例如,外部调用可能会失败,或者它们可能会被恶意合约利用来进行重入攻击。因此,编写合约时需要格外小心,确保正确处理外部调用的结果,并采取措施防范潜在的安全问题。

为什么可以通过接口调用?​

在 Solidity 中,接口(Interface)是一种特殊的合约结构,它定义了一组函数声明但不包含它们的实现。当你想与一个已经部署在以太坊网络上的合约交互时,接口允许你的合约知道如何正确地调用那个合约的函数,即使你没有那个合约的完整代码。

接口的工作原理是基于以太坊的智能合约和 EVM(以太坊虚拟机)如何处理函数调用。每个函数调用都是通过一种叫做 ABI 编码的方式来构造的,这种编码包括了函数的标识符(函数选择器)和它的参数。

这里是为什么可以通过接口调用的原因:

  1. 函数选择器:在 Solidity 中,每个函数都有一个对应的唯一函数选择器,这是一个由函数的名称和参数类型组成的签名通过 Keccak-256 哈希算法取前 4 个字节而来的。这个选择器用于在以太坊上唯一地标识函数。

  2. ABI 编码:当你通过接口调用一个函数时,Solidity 编译器会根据接口中定义的函数签名来生成这个函数选择器,并且将它与函数参数一起 ABI 编码,形成调用数据。

  3. 动态链接:即使你的合约并不包含外部合约的完整实现代码,只要你知道外部合约的接口,你就可以构造出正确的调用数据。因此,当你的合约发送这个调用数据到外部合约的地址时,EVM 就能够根据函数选择器找到并执行正确的函数。

通过这种方式,接口充当了合约间通信的“桥梁”,允许合约开发者在不需要知道外部合约的内部实现细节的情况下,与这些合约进行交互。

下面是一个简单的例子来展示如何使用接口:

// 定义一个接口,它声明了一个外部合约的函数
interface IExternalContract {
function someFunction(uint256 _value) external returns (bool);
}

contract MyContract {
// 使用接口与外部合约进行交互
function callExternalContract(address _externalContract, uint256 _value) public returns (bool) {
IExternalContract externalContract = IExternalContract(_externalContract);
return externalContract.someFunction(_value);
}
}

在这个例子中,MyContract合约通过IExternalContract接口与外部合约_externalContract进行交互。即使MyContract不知道someFunction函数的具体实现,它也能通过接口正确地调用该函数。

接口的参数为什么是合约地址?​

在 Solidity 中,使用接口与另一个合约进行交互时,需要提供目标合约的地址,因为:

  • 接口只定义函数签名:它不包含状态或者实现代码。
  • 合约地址指定目标实例:每个合约实例在区块链上有一个唯一的地址。
  • 通过地址创建实例:我们使用接口和地址来创建一个指向具体合约的引用,以便调用其函数。

示例代码:

interface IExternalContract {
function someFunction(uint256 _value) external returns (bool);
}

contract MyContract {
function callExternalContract(address contractAddress, uint256 _value) public returns (bool) {
IExternalContract external = IExternalContract(contractAddress);
return external.someFunction(_value);
}
}

在这段代码中,callExternalContract接收一个合约地址contractAddress,然后通过接口IExternalContract与该地址上的合约实例进行交互。

合约函数中的 this 指向​

在 Solidity 中,this 关键字是一个指向合约自身的指针。更具体地说,this 表示当前合约的实例,你可以通过它来访问合约的公共和外部函数以及状态变量。

以下是一些使用 this 的场景:

  1. 调用合约的当前实例的函数: 如果你想要从合约内部以外部调用(external call)的方式调用合约自己的一个公共(public)或外部(external)函数,你可以使用 this 关键字。

    pragma solidity ^0.8.0;

    contract MyContract {
    uint public value;

    function setValue(uint _value) public {
    value = _value;
    }

    function externalSetValue(uint _value) public {
    // 使用 this 来以外部调用的方式调用 setValue 函数
    this.setValue(_value);
    }
    }

    在上面的例子中,externalSetValue 函数通过 this.setValue(_value) 以外部调用的方式调用了 setValue 函数。

  2. 获取合约的地址: 你可以使用 this 来获取当前合约实例的地址。

    address public contractAddress;

    function setContractAddress() public {
    // 将合约地址设置为当前合约的地址
    contractAddress = address(this);
    }

    在上面的例子中,setContractAddress 函数设置 contractAddress 为当前合约的地址。

需要注意的是,当你通过 this 来调用合约的函数时,这些调用被视为外部调用,即使是在合约内部进行的。外部调用会使用消息调用(message call),这意味着它们会产生一个新的 EVM 调用栈,可能会有额外的 gas 成本,并且对状态变量的更改将不会在调用中反映出来,除非调用成功完成。

合约函数类型相互转化​

在 Solidity 中,函数可以被定义为几种不同的类型,主要包括:external, public, internal, 和 private。这些类型定义了函数的可见性和可调用性。然而,你可能想要在合约中改变一个函数的类型,或者更准确地说,你可能想要在不同的上下文中以不同方式调用函数。这涉及到函数选择器和低级调用。

函数选择器是函数签名的前四个字节,它是调用合约函数时必须提供的。在 Solidity 中,你可以通过 this.functionName 访问外部函数的选择器,即使该函数实际上是公共(public)的。这样,你就可以以外部调用的方式调用一个公共函数。

这里有一些关于函数类型转换的说明:

  • 从 Public 到 External: 你可以通过 this.functionName 使用公共函数的外部接口。这会导致稍微高一些的气体费用,因为它通过外部调用的方式而不是直接跳转来执行。

  • 从 External 到 Public/Internal: 一个外部函数不能直接转换为公共或内部函数。如果你想要在合约内部以内部方式调用一个外部函数,你需要写一个包装器函数,这个函数是公共或内部的,并且它内部调用外部函数。

  • 从 Internal 到 Public/External: 内部函数可以通过定义一个新的公共或外部函数来"暴露"。这个新函数将简单地调用内部函数。

  • 从 Private 到 Public/External/Internal: 和内部函数一样,私有函数可以通过定义一个新的公共、外部或内部函数来"暴露"。同样,这个新函数会调用私有函数。

在 Solidity 中,你不能直接改变一个已经定义的函数的类型,但你可以通过定义新的函数来间接地"转换"类型。请注意,这种转换实际上不是改变原有函数的类型,而是创建了一个新的函数,新函数按照你需要的那种方式调用原有函数。

这里有一个简单的示例:

pragma solidity ^0.8.0;

contract Example {
// Internal function
function internalFunction() internal pure returns (string memory) {
return "Called internalFunction";
}

// Public function that exposes the internal function
function callInternal() public pure returns (string memory) {
return internalFunction();
}

// External function
function externalFunction() external pure returns (string memory) {
return "Called externalFunction";
}

// Public function that exposes the external function
function callExternal() public pure returns (string memory) {
return this.externalFunction();
}
}

在这个例子中,callInternal 是一个公共函数,它调用了内部函数 internalFunction。而 callExternal 是一个公共函数,它以外部调用的方式调用了外部函数 externalFunction。

AJAX VS JSON-RPC​

前后端之间的 AJAX 通信和前端与智能合约之间的通信是两种不同的交互模式,它们在底层技术、安全性、交互方式和使用的协议等方面有所不同。以下是它们之间的一些主要异同点:

相同点​

  1. 异步性: AJAX 通信和前端与智能合约的通信通常都是异步的。这意味着在发送请求后,用户界面可以继续响应用户操作,而不需要等待响应返回。

  2. 基于 JavaScript: 在前端,无论是与后端服务器还是与区块链智能合约通信,通常都会使用 JavaScript 或者 JavaScript 框架来处理这些通信。

  3. 触发事件: 在两种通信方式中,都会有事件触发请求,比如用户点击按钮,或者满足特定条件。

  4. 处理响应: 两种通信方式都需要处理返回的数据,这可能涉及到更新前端的状态或界面。

不同点​

  1. 通信协议:

    • AJAX: 通常使用 HTTP/HTTPS 协议与服务器进行数据交换。
    • 合约通信: 与智能合约通信通常使用 JSON-RPC 协议通过以太坊客户端与区块链交互。
  2. 安全性:

    • AJAX: 通信安全性依赖于 HTTPS 和服务器的安全配置。
    • 合约通信: 通信安全性依赖于区块链的加密和共识机制。
  3. 数据存储:

    • AJAX: 数据存储在服务器上,服务器可以是中心化的,也可以是分布式的,但通常由单个实体控制。
    • 合约通信: 数据存储在区块链上,是分布式的,不可篡改的。
  4. 身份验证:

    • AJAX: 身份验证通常通过用户名和密码、Token 或其他身份验证机制实现。
    • 合约通信: 身份验证通过私钥签名交易实现,确保了交易的发送者拥有相应的地址权限。
  5. 交互方式:

    • AJAX: 请求可以是简单的读取操作,也可以是写入操作,后者可能会更改服务器上的数据。
    • 合约通信: 读取操作不需要进行交易,但写入操作需要创建并发送交易,可能需要支付一定的燃料费(Gas)。
  6. 工具和库:

    • AJAX: 使用诸如 XMLHttpRequest、Fetch API、jQuery、Axios 等 JavaScript 库。
    • 合约通信: 使用诸如 Web3.js、ethers.js 等专门为与区块链交互设计的库。
  7. 费用:

    • AJAX: 通常不需要支付费用来与服务器通信。
    • 合约通信: 执行写入操作(即发送交易)通常需要支付燃料费,这是因为区块链网络需要对交易进行验证和执行。
  8. 响应时间:

    • AJAX: 响应时间相对较快,取决于网络延迟和服务器处理能力。
    • 合约通信: 读取操作响应较快,但写入操作(交易)的确认时间取决于区块链网络的拥堵程度和提供的燃料费价格。

总而言之,AJAX 通信是建立在传统的客户端-服务器模型之上,而前端与智能合约的通信则是建立在去中心化的区块链技术之上。尽管在交互方式上有一些相似之处,但它们在实现细节、安全性、成本和基础架构上有很大的不同。

json-rpc 是数据协议,http 是传输协议​

是的,您的理解是正确的。JSON-RPC 和 HTTP 分别代表了数据协议和传输协议这两个不同的概念:

  1. JSON-RPC 是一个数据协议,它定义了客户端和服务器之间交换数据的格式和规则。JSON-RPC 使用 JSON(JavaScript Object Notation)作为数据格式,使得数据易于读写,同时也易于被人和机器解析。JSON-RPC 定义了一种简单的协议,允许在不同的系统之间进行远程过程调用(RPC),并不限定于特定的传输协议。

  2. HTTP(HyperText Transfer Protocol)是一个传输协议,它定义了客户端和服务器之间如何发送和接收数据。HTTP 是互联网上最常用的协议之一,通常用于网页浏览,但也可用于传输任何类型的数据。因为它是一个基础的网络协议,所以可以用来传输各种数据格式,包括 JSON-RPC 请求和响应。

简而言之,JSON-RPC 关注于“要发送什么数据”(即调用的方法和参数),而 HTTP 关注于“如何发送数据”(即通过请求和响应的结构)。在实际应用中,JSON-RPC 请求通常被封装在 HTTP 请求的 body 中发送给服务器,服务器处理后,再将 JSON-RPC 响应封装在 HTTP 响应的 body 中返回给客户端。这种结合使用的方式使得 JSON-RPC 可以利用 HTTP 的广泛支持和成熟的基础设施。

字符串类型​

在 Solidity 中,字符串是一种特殊的数组类型,它本质上是一个字节序列。字符串被表示为string类型,但它们并不像在许多其他编程语言中那样具有许多内置的方法。这是因为 Solidity 主要关注于低级别的合约操作和气体效率,而字符串操作通常是高级别的、成本较高的。

Solidity 中的string类型是动态大小的,且是 UTF-8 编码的字节序列。但是,与 JavaScript 或 Python 等语言不同,Solidity 的string类型并没有内置的方法来直接操作这些字符串。例如,你不能直接在 Solidity 合约中调用方法来获取字符串长度、连接两个字符串或者切分字符串。

要在 Solidity 中操作字符串,你通常需要自己写函数或者使用第三方库。例如,你可能需要使用bytes类型来处理字符串数据,因为bytes是一个低级数据类型,它有一些可以操作字节序列的方法。

以下是一些在 Solidity 中处理字符串时可能会用到的bytes方法:

  • length:获取字节序列的长度。
  • push:向动态字节数组的末尾添加一个新元素(仅限bytes,不适用于string)。
  • pop:移除动态字节数组末尾的元素(仅限bytes,不适用于string)。

如果你需要进行更复杂的字符串处理,你可能需要使用汇编语言或者引入外部库。例如,OpenZeppelin 是一个流行的智能合约开发库,它提供了一些字符串操作的函数。

这里是一个使用bytes来获取字符串长度的例子:

pragma solidity ^0.8.0;

contract StringHelpers {
function getLength(string memory str) public pure returns (uint) {
return bytes(str).length;
}
}

在 Solidity 中,处理字符串通常需要更多的注意和手动操作,因为每个操作都可能涉及到非常昂贵的气体成本。设计智能合约时,通常建议尽量避免复杂的字符串操作,或者尽可能将这些操作移到链下处理。

如何避免悬空引用​

在 Solidity 中,悬空引用(dangling reference)通常指的是指向一个已经被删除或者不再有效的存储位置的引用。在使用数组时,悬空引用可能发生在以下情况:

  1. 当引用指向一个已经被删除的动态数组元素时。
  2. 当引用指向一个已经被删除或者超出了数组长度的固定大小数组元素时。

要避免悬空引用,你应该确保在访问数组元素之前,该元素是存在的。在 Solidity 中,你可以通过检查数组的长度来做到这一点。如果你尝试访问一个超出数组长度的元素,Solidity 会抛出一个异常。

这里有一些避免悬空引用的方法:

  1. 检查数组长度:在访问数组的特定索引之前,先检查索引是否小于数组的长度。
if (index < myArray.length) {
// 安全地访问数组元素
uint value = myArray[index];
}
  1. 使用数组操作的返回值:对于 pop 这样会改变数组长度的操作,你可以直接使用其返回值(如果有的话),而不是通过索引访问。
uint removedValue = myArray.pop(); // pop 返回被移除的元素
  1. 避免在数组长度变化后使用旧索引:如果你在数组长度变化后保留了一个索引,确保在再次使用它之前重新检查数组长度。

  2. 使用映射(Mapping)来管理元素的存在性:如果你在删除元素时需要保持引用的有效性,可以考虑使用映射来跟踪哪些元素是有效的。

  3. 使用高级语言特性:Solidity 0.6.0 之后的版本支持 try/catch 语句,你可以用它来处理数组访问可能抛出的异常。

通过这些方法,你可以确保在访问数组时不会遇到悬空引用的问题。记住,智能合约的异常处理非常重要,因为错误的处理可能会导致合约被锁定或资金丢失。始终确保你的合约代码能够安全地处理各种边缘情况。

从 java 类的角度理解下合约​

在 Java 中,一个类是一个定义对象属性和方法的蓝图。一个 Java 类可以包含字段(属性)、方法(函数)、构造函数和更多的结构来定义一个对象的状态和行为。在智能合约和 Solidity 的上下文中,合约可以被看作是类的一个类似物。智能合约中的结构体(Structs)与 Java 中的类有一些相似之处。

让我们通过比较 Java 类和 Solidity 合约来理解这些概念:

Java 类​

一个 Java 类通常包含以下部分:

  • 字段(Field):定义对象状态的变量。
  • 方法(Method):定义对象可以执行的操作。
  • 构造函数(Constructor):用于创建类的实例。
  • 访问修饰符(Access Modifier):定义类、字段和方法的访问级别,如public、private等。
public class User {
private int id;
private String name;
private String email;

public User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}

public int getId() {
return id;
}

public String getName() {
return name;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

// 其他方法...
}

Solidity 合约​

一个 Solidity 合约通常包含以下部分:

  • 状态变量(State Variable):存储合约持久状态的变量。
  • 函数(Function):定义合约可以执行的操作。
  • 构造函数(Constructor):用于初始化合约状态。
  • 修饰符(Modifier):用于修改函数的行为。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract UserContract {
struct User {
uint id;
string name;
string email;
}

User[] public users;

function createUser(uint _id, string memory _name, string memory _email) public {
users.push(User(_id, _name, _email));
}

function getUser(uint _index) public view returns (User memory) {
require(_index < users.length, "Index out of bounds");
return users[_index];
}

// 其他函数...
}

在这个 Solidity 合约中,User结构体类似于 Java 中的类,它定义了用户的属性。合约中的函数可以看作是 Java 类中的方法。

比较​

  • 实例化:在 Java 中,你使用new关键字创建类的实例。在 Solidity 中,你可以直接创建结构体实例,或者部署另一个合约的实例。
  • 存储:Java 对象通常存储在堆内存中,而 Solidity 的状态变量存储在区块链上,这意味着它们是持久化的。
  • 访问控制:Java 使用访问修饰符来限制访问范围。Solidity 也有类似的概念(如public、private等),但它们的含义与区块链的上下文紧密相关。
  • 构造函数:Java 和 Solidity 都有构造函数,但 Solidity 的构造函数只在合约创建时执行一次。
  • 继承:Java 支持多重继承,而 Solidity 只支持单继承,但允许多个接口的实现。

在将 Java 类的概念映射到 Solidity 合约时,重要的是要记住它们的执行环境和目的是不同的。Java 类通常用于构建通用的软件应用程序,而 Solidity 合约是为了在以太坊这样的区块链上执行,有着不同的性能考虑和安全性要求。

solidity 特有的数据类型有哪些​

Solidity 作为一种智能合约编程语言,拥有一些专门为区块链环境设计的特有数据类型。以下是一些 Solidity 特有的数据类型:

  1. 地址(Address):

    • address: 用于存储以太坊地址,是一个 20 字节的值,通常代表账户地址或合约地址。
    • address payable: 类似于address,但是拥有transfer和send方法,可以接收以太币。
  2. 合约(Contracts):

    • 合约类型允许一个合约变量代表另一个合约,使得合约间的交互成为可能。
  3. 枚举(Enums):

    • enum: 用于创建自定义类型,限制变量到几个预定义的值中。
  4. 映射(Mappings):

    • mapping: 类似于哈希表或字典,是一种键值对存储结构,其中键是不可遍历的。
  5. 固定大小的字节类型:

    • bytes1到bytes32: 表示从 1 个字节到 32 个字节的固定大小的字节数组。
    • bytes: 动态大小的字节数组,但与bytes1到bytes32不同,它不是值类型。
  6. 固定点数:

    • fixed和ufixed: Solidity 支持固定点数,但这些类型目前还没有完全实现,不建议在生产环境中使用。
  7. 函数类型:

    • 函数类型是指向函数的引用,它可以用来实现回调函数或将函数作为参数传递。
  8. 存储位置(Storage, Memory, and Calldata):

    • storage: 代表永久存储,是状态变量的默认存储位置。
    • memory: 代表临时存储,用于函数执行期间的数据存储。
    • calldata: 类似于memory,但是专门用于存储函数的输入参数,是只读的,并且比memory更加节约 gas。

这些类型是为了与以太坊虚拟机(EVM)的特性和限制相适应,同时也考虑了智能合约开发中的安全性和效率。例如,地址类型和映射类型是处理以太坊账户和状态存储的基础,而存储位置关键字则是 Solidity 中特有的内存管理机制,它们帮助开发者以成本效益的方式使用 EVM 的存储资源。

有哪些功能型的合约?​

在 Solidity 和其他智能合约编程语言中,"功能型合约"通常指的是那些提供特定功能或服务的合约。这些合约可以独立使用,也可以作为更大应用的一部分。以下是一些常见的功能型合约类型:

  1. 代币合约:

    • ERC-20 合约: 实现了 ERC-20 标准的代币合约,用于创建可交换的代币。
    • ERC-721 合约: 实现了 ERC-721 标准的非同质化代币(NFT)合约,用于创建独一无二的代币。
    • ERC-1155 合约: 实现了 ERC-1155 标准,可以创建可交换和非同质化代币的多代币合约。
  2. 众筹合约:

    • 用于实现众筹或集资功能,允许用户向项目捐款或投资。
  3. 投票合约:

    • 用于组织投票和治理决策,允许持有代币的用户对提案进行投票。
  4. 贷款和借贷合约:

    • 实现去中心化的贷款和借贷机制,用户可以借出或借入资产。
  5. 去中心化交易所(DEX)合约:

    • 用于实现代币之间的去中心化交易,如自动做市商(AMM)模型。
  6. 预言机合约:

    • 用于从链外世界获取数据,并将其提供给链上的智能合约。
  7. 游戏合约:

    • 用于构建区块链游戏,可以包括角色、物品和其他游戏逻辑。
  8. 保险合约:

    • 提供去中心化的保险服务,根据合约条款自动处理索赔。
  9. 拍卖合约:

    • 用于实现去中心化的拍卖机制,如荷兰式或英式拍卖。
  10. 钱包合约:

    • 多重签名钱包合约,要求多个签名才能执行交易,增加安全性。
  11. 自动化任务合约:

    • 如以太坊上的定时任务(Ethereum Alarm Clock),可以在未来的某个时间点自动执行合约函数。
  12. 流动性池合约:

    • 用于去中心化金融(DeFi)平台,允许用户提供流动性并获得交易费用作为回报。
  13. 分红合约:

    • 自动分配利润或代币给持有特定代币的用户。

这些合约类型只是智能合约应用的一部分,实际上,随着区块链技术的发展,可能会出现更多新的合约类型和用例。智能合约的设计和实现通常需要考虑安全性、效率和合规性,因为它们直接处理价值转移和业务逻辑。

除了我之前列举的功能型合约类型,还有其他一些常见的智能合约,它们在区块链应用中扮演特定的角色:

  1. 身份和认证合约:

    • 用于身份验证和授权,可以与现实世界的身份信息桥接,以实现链上身份管理。
  2. 分布式自治组织(DAO)合约:

    • 用于创建和管理去中心化组织,成员可以投票决定组织的方向和资金使用。
  3. 抵押和锁定合约:

    • 允许用户锁定代币以参与系统治理或获得奖励,例如在质押证明(Proof of Stake, PoS)系统中。
  4. 逃逸舱(Escape Hatch)或紧急停止合约:

    • 用于在合约出现安全问题时提供紧急停止机制,可以限制损失。
  5. 库合约:

    • 提供可重用的代码逻辑,其他合约可以通过继承或委托调用库合约的函数。
  6. 工厂合约:

    • 用于部署新合约实例,可以创建和管理多个合约实例。
  7. 桥接合约:

    • 用于不同区块链之间的资产或数据传输,例如将代币从一个链桥接到另一个链。
  8. 隐私合约:

    • 提供隐私保护功能,如零知识证明(ZKP)或混币器,以隐藏交易细节。
  9. 数据存储合约:

    • 专门用于存储和管理链上数据,可能采用特定的数据结构来优化读写效率。
  10. 市场合约:

    • 用于创建各种市场,如预测市场,用户可以就特定事件的结果进行投注。
  11. 债务和债券合约:

    • 允许创建和管理债务工具,如债券或债务债权。
  12. 合成资产合约:

    • 用于创建跟踪现实世界资产价格的合成代币,如合成股票或货币。
  13. 奖励和激励合约:

    • 为用户提供完成特定任务或达成目标的奖励。
  14. 托管和中介合约:

    • 在买卖双方之间提供托管服务,确保交易的公平性。

每种合约类型都有其特定的用途和设计模式,开发者可以根据应用需求组合不同的合约来构建复杂的去中心化应用(DApps)。随着区块链技术的不断发展,新的合约类型和模式也在不断出现,推动着这个领域的创新。

不能对映射进行递归调用,也就是说,您不能列举它们的键​

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }

struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys;
uint size;
}

type Iterator is uint;

library IterableMapping {
function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex;
self.data[key].value = value;
if (keyIndex > 0)
return true;
else {
keyIndex = self.keys.length;
self.keys.push();
self.data[key].keyIndex = keyIndex + 1;
self.keys[keyIndex].key = key;
self.size++;
return false;
}
}

function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex;
if (keyIndex == 0)
return false;
delete self.data[key];
self.keys[keyIndex - 1].deleted = true;
self.size --;
}

function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0;
}

function iterateStart(itmap storage self) internal view returns (Iterator) {
return iteratorSkipDeleted(self, 0);
}

function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {
return Iterator.unwrap(iterator) < self.keys.length;
}

function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {
return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1);
}

function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {
uint keyIndex = Iterator.unwrap(iterator);
key = self.keys[keyIndex].key;
value = self.data[key].value;
}

function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return Iterator.wrap(keyIndex);
}
}

// 如何使用
contract User {
// 只是一个保存我们数据的结构体。
itmap data;
// 对数据类型应用库函数。
using IterableMapping for itmap;

// 插入一些数据
function insert(uint k, uint v) public returns (uint size) {
// 这将调用 IterableMapping.insert(data, k, v)
data.insert(k, v);
// 我们仍然可以访问结构中的成员,
// 但我们应该注意不要乱动他们。
return data.size;
}

// 计算所有存储数据的总和。
function sum() public view returns (uint s) {
for (
Iterator i = data.iterateStart();
data.iterateValid(i);
i = data.iterateNext(i)
) {
(, uint value) = data.iterateGet(i);
s += value;
}
}
}

这段代码是 Solidity 语言编写的,它实现了一个可迭代的映射(itmap),这是 Solidity 中的一个高级数据结构,因为 Solidity 的标准映射类型不支持迭代或获取所有键的列表。IterableMapping库通过结合映射和数组来提供这样的功能。

下面是代码的一些关键点的解释:

  1. 定义数据结构:

    • IndexValue 结构体包含一个映射的键在数组中的索引和值。
    • KeyFlag 结构体包含键和一个标记,指示该键是否已被删除。
    • itmap 结构体包含一个映射,映射uint到IndexValue,一个KeyFlag数组,以及一个表示映射大小的size变量。
  2. 定义迭代器:

    • Iterator 是一个封装了uint的自定义类型,用于在可迭代映射中追踪位置。
  3. IterableMapping库:

    • insert 函数用于向映射中插入一个键值对。如果键已存在,则更新值并返回true;如果键不存在,则添加键值对并返回false。
    • remove 函数用于从映射中移除一个键,并将其标记为已删除。
    • contains 函数检查一个键是否存在于映射中。
    • iterateStart 函数返回迭代器的初始位置。
    • iterateValid 函数检查迭代器当前位置是否有效。
    • iterateNext 函数移动迭代器到下一个有效位置。
    • iterateGet 函数根据迭代器的位置获取键值对。
    • iteratorSkipDeleted 函数是一个私有函数,用于跳过已删除的键。
  4. 使用库:

    • User 合约定义了一个itmap类型的变量data。
    • 使用using IterableMapping for itmap;语句,允许User合约调用IterableMapping库中定义的函数,就像它们是itmap类型的方法一样。
    • insert 函数插入键值对并返回映射的新大小。
    • sum 函数计算所有存储在映射中的值的总和。

关于您提到的IterableMapping没有data的问题,这里有一些可能导致误解的地方。IterableMapping是一个库,它定义了一组函数,这些函数通过参数接收itmap类型的存储引用,然后对其进行操作。在 Solidity 中,库函数可以像操作自己的状态变量一样操作传入的参数,这就是为什么您看到self关键字在这些函数中被用来代表传入的itmap实例。

User合约中的data变量是itmap类型的实例,它实际上是使用IterableMapping库的数据存储。当您在User合约中调用data.insert(k, v)时,您实际上是在调用IterableMapping库的insert函数,并将data作为参数传入。这就是为什么您在User合约中没有直接看到data变量被定义在IterableMapping库中,因为它是作为参数传递给库函数的。

类比以太坊 的 EVM, JAVA EVM 怎么没有提到过 ABI?​

Java 虚拟机(JVM)和以太坊虚拟机(EVM)虽然都是虚拟机,但它们的设计目的和运行的上下文有很大的不同。

JVM(Java 虚拟机)​

JVM 是用来运行 Java 字节码的虚拟机。Java 源代码被编译成字节码(.class文件),然后可以在任何安装了 JVM 的平台上运行。JVM 负责将这些字节码转换为特定操作系统上的机器码。在这个过程中,JVM 使用的是 Java 的类和方法信息,这些信息是在 Java 源代码被编译成字节码时嵌入的。Java 的字节码已经包含了足够的信息来指导 JVM 如何执行代码,包括类型信息、方法签名等。

在 Java 的世界中,"接口"通常是指一个可以被实现的抽象类型,而不是一个数据交换的格式。Java 程序在运行时通过类加载器和反射机制来动态加载和交互,这些机制内置于 JVM 中,无需类似于 ABI 的外部定义。

EVM(以太坊虚拟机)​

EVM 是专门为执行以太坊智能合约而设计的虚拟机。智能合约是用 Solidity(或其他语言)编写的,然后编译成 EVM 能够理解的字节码。不同于 JVM,EVM 的字节码需要与外部世界(如钱包、其他合约、用户界面)进行交互,而这些交互必须通过交易来完成。

因为交易只能包含字节数据,所以需要一种标准化的方式来编码和解码这些数据,以便 EVM 能够正确地执行智能合约的函数调用。这就是 ABI 的角色。ABI 定义了如何将函数调用和参数转换为字节串,以及如何将交易的结果(也是字节串)转换回可读的格式。

总结​

所以,JVM 没有类似于 ABI 的概念,因为它是为了运行 Java 字节码而设计的,而这些字节码已经包含了所有必要的类型和结构信息。另一方面,EVM 需要 ABI 来定义如何在智能合约和调用它们的外部世界之间传递信息。这是因为 EVM 的上下文是去中心化的区块链环境,其中任何人都可以与智能合约交互,而且这些交互必须通过标准化的数据格式来完成。

ABI 是工具,是外界与 EVM 交互的媒介​

是的,您可以将以太坊的 ABI(Application Binary Interface)理解为一个工具或协议,它充当了外部世界(如前端应用、钱包或其他智能合约)与 EVM(以太坊虚拟机)之间的媒介。ABI 定义了如何将函数调用、参数、和返回值编码成一种可以由 EVM 处理的格式,并且在数据返回时如何解码。

在以太坊中,智能合约的代码是以字节码的形式部署在区块链上的。当外部账户想要与智能合约进行交互时(比如调用合约的函数),它们需要发送包含特定信息的交易。这些信息包括:

  • 要调用的合约的地址。
  • 要调用的合约函数的标识符(也就是函数选择器)。
  • 任何传递给函数的参数。

ABI 规定了如何将这些信息编码为一个字节串,这样智能合约就能理解和执行相应的函数调用。同样地,当合约函数产生输出时,ABI 也规定了如何将这些输出解码回原始的数据类型和结构,以便应用程序能够理解。

因此,ABI 是智能合约开发和交互中至关重要的一部分,它确保了数据的一致性和正确的交互流程。

encode 与 encodePacked​

在以太坊智能合约中,尤其是在使用 Solidity 语言时,encode 和 encodePacked 是 ABI 编码函数的两种形式,它们用于将参数编码为字节串。这些函数是abi库的一部分,其用途是为了准备数据,使其可以在智能合约的函数调用中作为参数传递,或者在链外与合约交互时使用。下面是这两个函数的主要区别:

abi.encode​

abi.encode 函数执行标准的 ABI 编码。它将参数打包成严格遵守 ABI 规范的字节串。这意味着每个参数都会被编码为固定长度的字节块,如果参数是动态类型(例如字符串或字节数组),则会首先编码其长度,然后是数据本身。

这种编码方式有助于确保数据的一致性,因为编码的输出会包含足够的信息来准确地解码每个参数。这是与其他合约交互或者调用合约函数时最常用的编码方式。

例如:

bytes memory encodedData = abi.encode(arg1, arg2, arg3);

abi.encodePacked​

abi.encodePacked 函数执行紧密打包(tight-packing)的 ABI 编码。与abi.encode不同的是,abi.encodePacked不会对参数进行填充,它会将参数直接连续地拼接在一起,不考虑数据类型。这种编码方式可以节省空间,因为它不会添加任何额外的填充字节,但它也可能导致歧义,因为不同的输入组合可能产生相同的编码输出。

紧密打包通常用于计算哈希值(例如在使用keccak256哈希函数时)或者在需要节省空间的场景中。但是,由于它可能导致歧义,因此不建议用于需要精确解码参数的场景。

例如:

bytes memory encodedPackedData = abi.encodePacked(arg1, arg2, arg3);

使用场景​

  • abi.encode: 当您需要将数据传递给另一个智能合约的函数,或者在链外与合约交互时,需要确保数据可以被准确解码。
  • abi.encodePacked: 当您需要对数据进行哈希处理,或者在存储或传输时需要节省空间,并且不需要后续解码时。

总结来说,abi.encode 提供了一种安全可靠的编码方式,保证了数据的完整性和可解码性,而abi.encodePacked 提供了一种更为紧凑的编码方式,但牺牲了一定的安全性和可读性。在选择使用哪种编码方式时,开发者需要根据实际的应用场景和需求来决定。

try catch​

在 Solidity 中,当你使用try/catch结构来调用一个外部函数时,你可以指定一个returns子句来接收该函数返回的值。这是try/catch语法中特有的一种用法,它允许你在try块内直接处理外部调用的返回值。

在你提供的代码示例中:

try feed.getData(token) returns (uint v) {
return (v, true);
}

这里的returns (uint v)是try语句的一部分,它表示如果feed.getData(token)调用成功,那么返回的uint值将被存储在变量v中。然后,在try块中,你可以使用这个变量v。如果getData函数成功执行并返回一个值,那么这个值会被赋给v,然后try块内的代码会被执行,这里它返回了一个包含该值和true的元组,表示调用成功。

这种用法是 Solidity 特有的,因为它与其他编程语言中的try/catch语法不同。在其他语言中,通常不需要(也不允许)在try关键字后面指定返回值。Solidity 之所以这样设计,是为了让合约开发者能够更方便地处理智能合约函数调用的返回值。

这个语法在处理智能合约函数调用时非常有用,因为它允许开发者在同一个语句中既调用函数也处理可能的返回值,同时还能捕获和处理任何可能发生的错误。

自动生成 get 方法​

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract Complex {
struct Data {
uint a;
bytes3 b;
mapping(uint => uint) map;
uint[3] c;
uint[] d;
bytes e;
}
mapping(uint => mapping(bool => Data[])) public data;
}

在 Solidity 中,当你声明一个public状态变量,编译器会为你自动生成一个 getter 函数。然而,这个自动生成的 getter 函数有一些限制,尤其是当状态变量的类型比较复杂时。

对于mapping和array类型的状态变量,getter 函数会为你提供一种方法来访问单个元素,但不会为整个集合提供一个完整的视图。对于嵌套的数据结构,如在你的例子中的mapping(uint => mapping(bool => Data[]))类型的data变量,情况变得更加复杂。

在你的Complex合约中,data变量是一个嵌套映射,其值是一个包含Data结构体的数组。对于这种类型的变量,Solidity 可以帮你生成一个 getter 函数,但是你只能通过特定的键来访问数组中的单个Data元素。

例如,假设你想访问data映射中uint键为1,bool键为true,数组索引为0的Data元素。你可以这样做:

function getData(uint key, bool flag, uint index) public view returns (uint, bytes3, uint[3] memory, uint[] memory, bytes memory) {
Data storage dataElement = data[key][flag][index];
return (dataElement.a, dataElement.b, dataElement.c, dataElement.d, dataElement.e);
}

但是,请注意以下几点:

  1. mapping内部的元素不能通过自动生成的 getter 函数直接访问。你需要手动创建一个函数来返回映射中的特定数据。

  2. 对于Data结构体中的mapping类型成员map,Solidity 不会生成 getter 函数。映射类型的数据是不可枚举的,也就是说你不能获得所有键或所有值的列表。你只能通过指定一个键来查询对应的值。

  3. 对于动态数组(如Data结构体中的uint[] d)和bytes类型,getter 函数会要求你指定一个索引来访问单个元素。

因此,对于复杂类型,如Complex合约中的data变量,你可能需要编写自定义的函数来提供对嵌套数据和映射的更复杂查询。在实践中,通常会根据合约的业务逻辑需求来决定是否需要这样的自定义访问函数。

合约线性继承​

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract X {}
contract A is X {}
// 这段代码不会编译
contract C is A, X {}

在 Solidity 中,当你使用继承时,如果一个合约通过多个路径继承了同一个基合约,那么它会导致继承的菱形问题(也称为钻石问题)。这个问题是因为编译器不知道应该从哪个路径继承基合约的属性和行为。

在你的例子中:

  • 合约A继承自合约X。
  • 合约C尝试同时继承合约A和合约X。

由于合约A已经继承了合约X,所以合约C通过继承合约A已经间接继承了合约X。这时,合约C直接继承合约X就会导致编译错误,因为这会造成合约X被继承了两次,这在 Solidity 中是不允许的。

正确的继承应该是这样的:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract X {}
contract A is X {}
contract C is A {} // 正确的继承,合约C只需要继承合约A

在这个修正后的代码中,合约C只继承自合约A,而合约A已经继承了合约X的所有属性和行为,所以合约C也间接继承了合约X的所有内容,没有必要再直接继承合约X。

Solidity 0.6.0 之后的版本引入了一个新的特性,叫做 C3 线性化,它能够正确处理这种类型的继承,并且可以解决继承的菱形问题。但是,你的代码中的错误是因为合约C直接继承了同一个合约两次,这是不允许的,而不是因为 Solidity 的线性化处理问题。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base1 {
constructor() {}
}

contract Base2 {
constructor() {}
}

// 构造函数按以下顺序执行:
// 1 - Base1
// 2 - Base2
// 3 - Derived1
contract Derived1 is Base1, Base2 {
constructor() Base1() Base2() {}
}

// 构造函数按以下顺序执行:
// 1 - Base2
// 2 - Base1
// 3 - Derived2
contract Derived2 is Base2, Base1 {
constructor() Base2() Base1() {}
}

// 构造函数仍按以下顺序执行:
// 1 - Base2
// 2 - Base1
// 3 - Derived3
contract Derived3 is Base2, Base1 {
constructor() Base1() Base2() {}
}

fallback receive​

在编程中,"fallback" 和 "receive" 函数是 Solidity 语言中的特殊函数,用于智能合约与以太坊区块链交互。这些函数允许智能合约在接收以太币(ETH)时执行代码。在 Solidity 0.6.x 版本之后,fallback 和 receive 函数有了明确的区分:

  • receive() 函数是一个新引入的专用函数,只有在合约接收到纯粹的以太币(没有数据)时才会被调用。这个函数是可选的,如果存在,它必须是外部的(external)且没有参数,也没有返回值。
  • fallback() 函数在合约接收以太币并且没有任何其他函数匹配(即调用数据为空或调用了不存在的函数)时被调用。fallback 函数可以有一个 payable 修饰符,允许它接收以太币。

在 Solidity 中,当一个合约收到以太币但没有调用任何函数,或调用的函数不存在时,会根据输入数据的存在与否来决定调用 receive() 还是 fallback()。

solidity 事件是用来做什么的?js 有哪些方式可以监听事件,这些监听方式之间有什么区别​

Solidity 中的事件(Events)是智能合约与区块链外部世界之间通信的一种机制。事件是一种用于记录区块链上发生的事情的日志功能,它们在智能合约代码中被触发,并被存储在交易的日志中。事件不能从合约内部读取,但可以通过区块链的历史记录来检索。

事件通常用于以下几个方面:

  1. 通知客户端:智能合约可以通过事件向前端应用程序通知某些条件已经满足或者某些状态已经改变。
  2. 低成本存储:由于直接在智能合约上存储数据的成本较高,事件可以作为一种存储机制,尽管它们不是可从合约内部访问的。
  3. 记录历史:事件日志提供了一种方式来记录历史数据,这些数据可以被外部应用程序查询,以了解合约的状态变化。

在 JavaScript 中,可以使用几种不同的方法来监听 Solidity 事件:

  1. 使用 Web3.js:

    • myContract.events.MyEvent():这个 API 创建了一个订阅,当新的事件出现时,它会被触发。
    • myContract.getPastEvents('MyEvent'):这个 API 用于获取过去的事件。
  2. 使用 Ethers.js:

    • contract.on(event, listener):这个方法允许你订阅一个事件,每次事件被触发时,指定的监听器函数就会被调用。
    • contract.queryFilter(eventFilter):这个方法用于检索满足过滤条件的事件的历史。

这些监听方法之间的区别主要在于它们是如何与以太坊节点交互的,以及它们提供的 API 的不同。例如,Web3.js 和 Ethers.js 是两个不同的库,它们提供的 API 和内部实现可能有所不同。另外,getPastEvents 和 queryFilter 用于获取历史事件,而 events.MyEvent() 和 contract.on(event, listener) 用于订阅实时发生的事件。

监听实时事件通常涉及到创建一个 WebSocket 连接到以太坊节点,而获取过去的事件则通过 HTTP 或 IPC 连接来查询节点的历史数据。实时事件监听对于需要即时响应合约事件的应用程序很重要,比如更新用户界面,而历史事件查询则对于分析和展示合约的历史活动很有用。 当然,以下是使用 JavaScript 和流行的以太坊客户端库 Web3.js 和 Ethers.js 监听 Solidity 事件的示例代码。

Solidity 合约示例​

首先,让我们定义一个简单的 Solidity 合约,它包含一个可触发事件的函数。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleContract {
// 定义一个事件
event ValueChanged(address indexed author, uint256 oldValue, uint256 newValue);

uint256 public value;

// 设置一个新值并触发事件
function setValue(uint256 _value) public {
emit ValueChanged(msg.sender, value, _value);
value = _value;
}
}

使用 Web3.js 监听事件​

const Web3 = require('web3');

// 连接到以太坊节点
const web3 = new Web3('wss://some-node-url');

// 获取合约实例
const contract = new web3.eth.Contract(abi, contractAddress);

// 实时监听 ValueChanged 事件
contract.events.ValueChanged(
{
fromBlock: 'latest',
},
function (error, event) {
if (error) {
console.error('Error:', error);
} else {
console.log('Event:', event.returnValues);
}
}
);

// 获取过去的 ValueChanged 事件
contract.getPastEvents(
'ValueChanged',
{
fromBlock: 0,
toBlock: 'latest',
},
function (error, events) {
if (error) {
console.error('Error:', error);
} else {
events.forEach((event) => console.log('Past event:', event.returnValues));
}
}
);

使用 Ethers.js 监听事件​

const { ethers } = require('ethers');

// 连接到以太坊节点
const provider = new ethers.providers.WebSocketProvider('wss://some-node-url');

// 获取合约实例
const contract = new ethers.Contract(contractAddress, abi, provider);

// 实时监听 ValueChanged 事件
contract.on('ValueChanged', (author, oldValue, newValue, event) => {
console.log({
author,
oldValue: oldValue.toString(),
newValue: newValue.toString(),
event,
});
});

// 获取过去的 ValueChanged 事件
async function getPastEvents() {
const filter = contract.filters.ValueChanged();
const events = await contract.queryFilter(filter, 0, 'latest');
events.forEach((event) => {
console.log('Past event:', event.args);
});
}

getPastEvents();

在这些示例中:

  • abi 是合约的抽象二进制接口(ABI),它是一个 JSON 数组,描述了合约的函数和事件。
  • contractAddress 是部署合约的以太坊地址。
  • wss://some-node-url 应该替换为实际的以太坊节点 WebSocket URL。

在 Web3.js 示例中,我们使用 events.ValueChanged 来实时监听事件,并使用 getPastEvents 来获取历史事件。在 Ethers.js 示例中,我们使用 contract.on 方法来监听实时事件,使用 contract.queryFilter 方法来获取历史事件。

请注意,实际的节点 URL、合约地址、ABI 和其他配置应该根据你的具体情况进行替换。此外,为了实际运行这些代码,你需要安装相应的 npm 包:

npm install web3
npm install ethers

并且确保你有一个支持 WebSocket 的以太坊节点 URL。

创建一个合约实例为什么需要 provider?​

在以太坊中,合约实例是与区块链上的智能合约进行交互的本地副本。为了与区块链上的智能合约进行通信,你需要一个方式来连接到区块链网络。这就是所谓的 "provider" 的作用。

提供者(Provider)是一个抽象,它负责处理与以太坊网络的通信。它可以发送和接收交易,执行调用,订阅事件等操作。在不同的上下文中,"provider" 可能指代不同的东西:

  1. JSON-RPC Provider:这是一个实现了以太坊的 JSON-RPC 协议的 HTTP 或 WebSocket 端点。它是以太坊节点提供的服务接口,允许发送请求来执行各种操作,比如获取当前的区块号、发送交易、调用智能合约的方法等。

  2. Web3 Provider:在使用 Web3.js 时,provider 是一个封装了 JSON-RPC 通信的对象。它可以是 HTTPProvider、WebSocketProvider 或者 IPCProvider 等,取决于你的连接方式。

  3. Ethers Provider:在使用 Ethers.js 时,provider 是一个封装了与以太坊节点通信的对象,并且提供了一些额外的功能和简化的 API。Ethers.js 提供了多种预定义的 provider,如 JsonRpcProvider、Web3Provider、InfuraProvider 等。

创建合约实例需要 provider 的原因是,合约实例需要通过 provider 与区块链上的实际合约进行通信。无论是读取合约状态、监听事件、还是发送交易修改状态,这些操作都需要通过网络发送到区块链节点,并且接收节点的响应。Provider 就是这个通信过程的中介。

例如,当你调用合约的一个函数时:

  • 如果它是一个 view 或 pure 函数,provider 会发送一个调用请求到区块链,执行该函数并返回结果,而不改变任何状态。
  • 如果它是一个需要改变状态的函数,provider 会将交易发送到区块链,这个交易包含了执行该函数的所有必要信息,一旦被矿工打包进区块,函数就会被执行。

在使用 JavaScript 库如 Web3.js 或 Ethers.js 时,你通常会这样创建一个合约实例:

// Web3.js
const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY');
const contract = new web3.eth.Contract(abi, contractAddress);

// Ethers.js
const provider = new ethers.providers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY');
const contract = new ethers.Contract(contractAddress, abi, provider);

在这两个例子中,'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY' 是一个 Infura 节点的 URL,它提供了连接到以太坊主网的 JSON-RPC 接口。你需要用你自己的 Infura API 密钥替换 'YOUR_INFURA_API_KEY'。这个 provider 会处理与该节点的所有通信。

这个不理解为什么 var depositEvent = clientReceipt.Deposit(); 返回一个事件?​

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;

contract ClientReceipt {
event Deposit(
address indexed from,
bytes32 indexed id,
uint value
);

function deposit(bytes32 id) public payable {
// 事件是用 `emit` 发出的,后面是事件的名称和括号里的参数(如果有)。
// 任何这样的调用(甚至是深度嵌套)都可以通过过滤 `Deposit`
// 从JavaScript API中检测出来。
emit Deposit(msg.sender, id, msg.value);
}
}


var abi = /* 由编译器产生的abi */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* 地址 */);

var depositEvent = clientReceipt.Deposit();

// 监视变化
depositEvent.watch(function(error, result){
// 结果包含非索引的参数和给 `Deposit` 调用的 topics。
if (!error)
console.log(result);
});


// 或者通过回调立即开始监视
var depositEvent = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});

在 Solidity 中,事件是智能合约的日志记录机制,用于发出区块链上的某些操作的信号,这些信号可以被前端应用程序或者区块链监听器捕获。事件是区块链上的一种轻量级的通信方式,因为它们只写入日志而不改变任何状态。

在你给出的示例中,ClientReceipt 合约定义了一个名为 Deposit 的事件。当合约中的 deposit 函数被调用时,它会触发(emit)这个事件,并将某些数据记录到区块链的日志中。

在 Web3.js 0.x 版本中,你可以通过合约实例调用一个事件方法来创建一个事件监听器。这个方法返回一个事件观察器对象,它有一个 watch 方法可以用来设置一个回调函数,每当新的事件发生时这个回调函数就会被调用。

这里是如何理解这段代码的:

  1. var depositEvent = clientReceipt.Deposit(); 这一行代码不是返回一个事件,而是返回的是一个事件监听器对象。这个对象可以用来监听 Deposit 事件。

  2. depositEvent.watch(function(error, result){ ... }); 这一行代码设置了一个回调函数,每当 Deposit 事件被触发时,这个回调函数就会被调用。error 参数会包含错误信息(如果有的话),而 result 参数会包含事件的数据。

  3. 同样的,var depositEvent = clientReceipt.Deposit(function(error, result) { ... }); 这行代码立即开始监听事件,并为每个新的事件调用提供的回调函数。

要注意的是,这是 Web3.js 0.x 版本的语法。在 Web3.js 1.x 版本中,事件监听的语法有所不同,使用的是 .events 属性和 on 方法。例如:

clientReceipt.events
.Deposit(
{
fromBlock: 0,
},
function (error, event) {
if (!error) {
console.log(event);
}
}
)
.on('data', function (event) {
console.log(event); // 监听到新事件时的数据
})
.on('changed', function (event) {
// 当一个事件被区块链上的某些操作移除时
})
.on('error', console.error);

在使用 Web3.js 1.x 版本时,你需要确保你的代码使用正确的语法来监听事件。

事件会被记录到区块链的 Log 中,区块链的 Log 分为索引和数据。我们可以指定最多 3 个参数为 indexed,表示它们可以被索​

类似数据库索引

是的,区块链中的事件(Event)日志的索引参数类似于传统数据库中的索引。在 Solidity 智能合约中,当你声明一个事件时,你可以使用indexed关键字标记最多三个参数。这些被标记为indexed的参数会被特殊处理,使得它们可以被区块链的节点更高效地查询和访问,类似于数据库索引的作用。

当一个事件被触发并且记录到区块链上时,所有的indexed参数会被存储在一个特别的数据结构中,称为“topics”。这些 topics 使得外部的应用程序、服务或者区块链浏览器能够根据这些索引参数来过滤和搜索事件日志。例如,如果你想要找到所有特定用户的交易,你可以根据用户的地址(如果它是一个indexed参数)来过滤事件。

非indexed参数则被存储在日志的数据部分,这些数据不是为了高效检索而优化的,因此搜索这些数据会更慢且更耗费资源。

在 Solidity 中,事件的声明可能看起来像这样:

event Transfer(address indexed _from, address indexed _to, uint256 _value);

在这个例子中,_from和_to是indexed参数,意味着你可以根据发送者和接收者地址来过滤Transfer事件。_value则是非索引参数,包含了转账的金额。

使用事件日志的索引可以大大提高特定查询的效率,这对于开发者在构建 DApps(去中心化应用)时是非常有用的,特别是当他们需要根据特定条件来检索历史事件时。然而,由于区块链的存储成本相对较高,所以使用索引也需要考虑到成本和效率的平衡。

assert require revert 的区别​

在 Solidity 中,assert、require 和 revert 都是用于错误处理的控制结构,它们可以用来验证条件并在条件不满足时恢复状态改变并退回剩余的 Gas。然而,它们各自有不同的使用场景和副作用:

  1. require:

    • 用于验证输入和条件,以及在执行之前检查合约的状态。
    • 如果 require 的条件失败,它将恢复所有状态改变,退回剩余的 Gas,并返回一个错误信息。
    • require 是用于输入验证、条件检查和访问控制的首选方法。

    示例:

    require(condition, "Error message if condition is false");
  2. assert:

    • 用于检查代码不应该达到的状态,即检查内部错误和不变量。
    • assert 只应在代码中的某些东西出错时使用,例如,一个不可能为假的条件。
    • 如果 assert 的条件失败,它会消耗所有剩余的 Gas,并恢复状态改变。这通常指示有一个严重的错误,如算术运算溢出或不变量的破坏。
    • assert 不应用于外部输入或条件的检查。

    示例:

    assert(condition);
  3. revert:

    • 是一个函数,用于在复杂的条件下或在执行了一些逻辑之后恢复操作。
    • revert 将恢复所有状态改变,退回剩余的 Gas,并允许提供一个错误信息。
    • 当需要更复杂的逻辑来决定是否恢复时,revert 是有用的。

    示例:

    if (!condition) {
    revert("Error message if condition is false");
    }

总结:

  • 使用 require 来验证输入和条件,以及保护函数免受不良状态或输入的影响。
  • 使用 assert 来检查代码中不应该发生的情况,例如不变量的破坏。
  • 使用 revert 来在更复杂的逻辑后恢复操作,并提供详细的错误信息。