用 Go Ethereum 在链上直接读取 ERC20 智能合约信息
我也不知道这个有什么用,只是感觉做起来比较好玩。
在看 Görli Testnet 的 Etherscan 的时候,比如这个链接: https://goerli.etherscan.io/token/0x3ffc03f05d1869f493c7dbf913e636c6280e0ff9#readContract ,可以直接看到一个合约的一些基本信息,比如 Decimals,Max Total Supply 之类的
于是好奇有没有什么合理的办法直接从链上读到这些数据(而不是基于别人的 API 套娃整一个新的 API 出来)。
Why
其实网上有一些类似的文章(可以参考本文底部的 References),但是有一些问题,比如大家的文章的 solidity 都是一个非常上古的版本:
pragma solidity ^0.4.24;
而且文章中要查询 ERC20 的合约都是自己写了一个 interface,但是后来我发现对于这种 Public Interface 完全可以使用 OpenZeppelin 写好的, 比如 ERC20 的在 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol 这里。
Get Started
为了解决这个问题,我们需要以下工具:
- 一个 Eth 的节点(或者一个 Ethereum Gateway 也可以,比如 Infura 或者 Cloudflare 的免费服务)
- Go Ethereum 包(包含 abigen 啥的)
- Solidity compiler(比如 solc-js 或者 solc)
- nodejs (这个我相信大家电脑上都有,如果没有的话可以去 nodesource 整一个)
Prepare Env
Eth Node
对于 Eth 节点我们直接用公共服务就好,或者如果实在想自建一个的话可以直接用下方 docker-compose.yml
快速启动一个:
version: "3"
services:
geth:
image: ethereum/client-go:latest
restart: unless-stopped
ports:
- 30303:30303
- 30303:30303/udp
- 127.0.0.1:8545:8545
- 127.0.0.1:8546:8546
- 127.0.0.1:8551:8551
volumes:
- ./data:/root/.ethereum
healthcheck:
test: [ "CMD-SHELL", "geth attach --exec eth.blockNumber" ]
interval: 10s
timeout: 5s
retries: 5
command:
- --http
- --goerli
- --cache=8192
- --http.api=eth,net,web3,engine,admin
- --http.addr=0.0.0.0
- --http.vhosts=*
- --http.corsdomain=*
- --maxpeers=200
- --ws
- --ws.origins=*
- --ws.addr=0.0.0.0
- --ws.api=eth,net,web3
- --graphql
- --graphql.corsdomain=*
- --graphql.vhosts=*
- --authrpc.addr=0.0.0.0
- --authrpc.jwtsecret=/root/.ethereum/jwt.hex
- --authrpc.vhosts=*
- --authrpc.port=8551
- --txlookuplimit=0
prysm:
image: gcr.io/prysmaticlabs/prysm/beacon-chain
pull_policy: always
container_name: beacon
restart: unless-stopped
stop_grace_period: 2m
volumes:
- ./prysm_data:/data
- ./data:/geth
depends_on:
geth:
condition: service_healthy
ports:
- 127.0.0.1:4000:4000
- 127.0.0.1:3500:3500
command:
- --accept-terms-of-use
- --datadir=/data
- --disable-monitoring
- --rpc-host=0.0.0.0
- --execution-endpoint=http://geth:8551
- --jwt-secret=/geth/jwt.hex
- --rpc-host=0.0.0.0
- --rpc-port=4000
- --grpc-gateway-corsdomain=*
- --grpc-gateway-host=0.0.0.0
- --grpc-gateway-port=3500
Go Ethereum
在我所用的系统上似乎没有合适的包,既然都是一堆 Binary,不如直接下载然后解压到 /usr/bin
下:
wget https://gethstore.blob.core.windows.net/builds/geth-alltools-linux-amd64-1.10.18-de23cf91.tar.gz
tar -xvf geth-alltools-linux-amd64-1.10.18-de23cf91.tar.gz
cd geth-alltools-linux-amd64-1.10.18-de23cf91
sudo mv abigen /usr/bin/
sudo mv geth /usr/bin
Solc
这里一开始我跟着某个教程 npm install solc -g
,然后发现…
The commandline options of solcjs are not compatible with solc and tools (such as geth) expecting the behaviour of solc will not work with solcjs.
让我非常冒火,于是本着能找 Binary 就不要给自己找事情的原则,去找了 Binary 然后解压到 /usr/bin
下:
wget https://github.com/ethereum/solidity/releases/download/v0.8.14/solc-static-linux
chmod +x solc-static-linux
mv solc-static-linux /usr/bin/solc
Gen ABI
由于我们打算用 Go 来写一整套东西,所以需要用 Go Ethereum 来读取,而要让 Go Ethereum 能读取合约相关的信息,需要对应的 ABI,大概流程类似如下:
Solidity(ERC20.sol) –(solc)–> ABI(ERC20.abi) –(abigen)–> Go Package(erc20.go)
这里 ERC20.sol 我们就不像网上一些文章一样手搓了,而是直接使用 OpenZeppelin 提供的,关于 OpenZeppelin 的一点介绍如下:
A library for secure smart contract development. Build on a solid foundation of community-vetted code.
要使用他们已经写好的合约信息,找一个空白目录,然后直接 npm install @openzeppelin/contracts
即可在本地 node_modules
下拿到 OpenZeppelin 的所有合约,然后我们在 node_modules/@openzeppelin/contracts/token/ERC20
下可以看到我们想要的 ERC20.sol
文件,这个时候我们构建 abi
solc --abi /path/to/node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol --base-path /path/to/node_modules --output-dir /path/to/
这时你可以在当前目录下找到一个 ERC20.abi
文件,然后我们用 abigen 拿到对应的 erc20.go
abigen --abi=ERC20.abi --pkg=token --out=erc20.go
Read Contract
有了上面拿到的 erc20.go
文件之后,我们在同目录下搞个 read.go
用来驱动,可以这么写:
package main
import (
"fmt"
"log"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("https://goerli.knat.network")
if err != nil {
log.Fatal(err)
}
contract_address := common.HexToAddress("0x3ffc03f05d1869f493c7dbf913e636c6280e0ff9")
// use erc20.go to init instance
contract, err := NewToken(contract_address, client)
decimals, err := contract.Decimals(nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("decimals:", decimals)
symbol, err := contract.Symbol(&bind.CallOpts{})
if err != nil {
log.Fatal(err)
}
fmt.Println("symbol:", symbol)
total_supply, err := contract.TotalSupply(nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("total_supply:", total_supply)
}
然后就可以拿到我们要的信息了~
go run .
decimals: 18
symbol: TEST
total_supply: 1000000000000000000000000000000001300115100103598665898811424167730937505693
🤔 接下来就是找个 DB 写爆咯?