0%

ABI格式和使用

什么是 ABI (Application Binary Interface)?

在 Solidity 中,ABI(应用二进制接口)是智能合约与外界(包括其他合约和用户界面)交互的桥梁。它定义了智能合约中函数的编码方式、输入参数和返回值的格式,以便在区块链中进行数据的传输和解析。

ABI 的作用

  1. 数据编码与解码:在区块链中,所有的数据都以二进制形式存储和传输。ABI 提供了标准化的编码规则,使得智能合约的函数调用可以正确地解释输入和输出数据。
  2. 合约交互:外部程序(如 DApp)需要 ABI 文件来与合约交互,了解合约的函数和参数。
  3. 兼容性:通过 ABI,智能合约之间可以互操作,即使它们是由不同的开发人员编写的。

ABI 的重要性

  • 统一性:ABI 使得智能合约与外部交互的过程标准化,避免了自定义协议带来的复杂性。
  • 跨语言支持:无论是前端、后端,还是区块链中的其他合约,都可以基于 ABI 与合约进行交互。

ABI的原理

ABI 的结构

ABI 通常是以 JSON 格式生成的文件,描述了智能合约的函数、事件及其参数。

示例合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.8.0;

contract Example {
uint256 public value;

event ValueChanged(uint256 newValue);

function setValue(uint256 _value) public {
value = _value;
emit ValueChanged(_value);
}

function getValue() public view returns (uint256) {
return value;
}
}

ABI 的 JSON 表示

运行 solc --abi Example.sol 会生成以下 ABI 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newValue",
"type": "uint256"
}
],
"name": "ValueChanged",
"type": "event"
},
{
"inputs": [],
"name": "getValue",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_value",
"type": "uint256"
}
],
"name": "setValue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "value",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]

ABI 的关键字段

  1. type
    定义元素的类型:
    • function:表示合约函数。
    • event:表示事件。
    • constructor:表示构造函数。
    • fallback:表示回退函数。
    • receive:表示接收以太币的特殊函数。
  2. name
    函数或事件的名称(不适用于匿名函数)。
  3. inputs
    表示函数或事件的输入参数列表。每个参数包含:
    • internalType:Solidity 内部类型。
    • name:参数名称。
    • type:外部使用的类型(如 uint256)。
  4. outputs
    表示函数的返回值列表,格式与 inputs 相同。
  5. stateMutability
    表示函数的状态:
    • view:只读,不修改状态。
    • pure:不读写状态。
    • nonpayable:不允许发送以太币。
    • payable:允许发送以太币。
  6. anonymous
    仅适用于事件,表示事件是否匿名。

ABI 编码规则

ABI 定义了调用合约函数时参数的编码方式。以下是编码的关键点:

编码示例

调用 setValue(42) 的编码过程如下:

  1. 函数签名的 Keccak 哈希:
    1
    setValue(uint256) -> 0x55241077
  2. 参数的编码:
    1
    42 -> 000000000000000000000000000000000000000000000000000000000000002a
  3. 完整的编码:
    1
    0x55241077000000000000000000000000000000000000000000000000000000000000002a

ABI 解码规则

返回值的解码也遵循 ABI 的规则。
例如,getValue() 返回 42 时:

  1. 返回数据:
    1
    0x000000000000000000000000000000000000000000000000000000000000002a
  2. 解码为 42

ABI 的使用

使用场景 1: 智能合约调用

场景描述:

通过 Golang 使用 ABI 文件调用智能合约上的函数,例如调用只读函数或发送交易调用状态变更函数。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"context"
"fmt"
"log"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
)

func main() {
// 1. 连接到 Ethereum 节点
client, err := ethclient.Dial("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID")
if err != nil {
log.Fatalf("Failed to connect to Ethereum node: %v", err)
}

// 2. 合约地址和 ABI 定义
contractAddress := "0xYourContractAddress"
contractABI := `[{"constant":true,"inputs":[],"name":"getValue","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]`

// 3. 解析 ABI
parsedABI, err := abi.JSON(strings.NewReader(contractABI))
if err != nil {
log.Fatalf("Failed to parse ABI: %v", err)
}

// 4. 构造合约调用
callOpts := &bind.CallOpts{Context: context.Background()}
data, err := parsedABI.Pack("getValue")
if err != nil {
log.Fatalf("Failed to pack data: %v", err)
}

// 5. 调用合约
msg := ethereum.CallMsg{
To: &contractAddress,
Data: data,
}
result, err := client.CallContract(context.Background(), msg, nil)
if err != nil {
log.Fatalf("Failed to call contract: %v", err)
}

// 6. 解析返回值
var value *big.Int
err = parsedABI.UnpackIntoInterface(&value, "getValue", result)
if err != nil {
log.Fatalf("Failed to unpack result: %v", err)
}
fmt.Printf("Value: %s\n", value.String())
}

关键点:

  1. 使用 abi.JSON 解析 ABI。
  2. 使用 Pack 将函数调用和参数编码。
  3. 通过 CallContract 调用合约。
  4. 使用 UnpackIntoInterface 解码返回值。

使用场景 2: 监听合约事件

场景描述:

通过 Golang 和 ABI 监听合约事件的发生。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"context"
"log"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/ethclient"
)

func main() {
// 1. 连接到 Ethereum 节点
client, err := ethclient.Dial("wss://mainnet.infura.io/ws/v3/YOUR_INFURA_PROJECT_ID")
if err != nil {
log.Fatalf("Failed to connect to Ethereum node: %v", err)
}

// 2. 合约地址和事件的 ABI 定义
contractAddress := "0xYourContractAddress"
eventABI := `[{"anonymous":false,"inputs":[{"indexed":false,"name":"newValue","type":"uint256"}],"name":"ValueChanged","type":"event"}]`

// 3. 解析事件 ABI
parsedABI, err := abi.JSON(strings.NewReader(eventABI))
if err != nil {
log.Fatalf("Failed to parse ABI: %v", err)
}

// 4. 设置日志查询过滤器
query := ethereum.FilterQuery{
Addresses: []common.Address{common.HexToAddress(contractAddress)},
}
logs := make(chan types.Log)
sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
if err != nil {
log.Fatalf("Failed to subscribe to logs: %v", err)
}

// 5. 监听事件
for logEvent := range logs {
var event struct {
NewValue *big.Int
}
err := parsedABI.UnpackIntoInterface(&event, "ValueChanged", logEvent.Data)
if err != nil {
log.Printf("Failed to unpack event: %v", err)
} else {
log.Printf("New Value: %s", event.NewValue.String())
}
}

_ = sub // Handle subscription lifecycle as needed
}

关键点:

  1. 使用 FilterQuery 设置日志过滤器。
  2. 通过 SubscribeFilterLogs 监听事件。
  3. 使用 ABI 的 UnpackIntoInterface 解码事件数据。