CKB 与以太坊 RPC 接口实战指南:获取链上数据、交互合约与转账

·

前置准备

本指南基于 Go 语言示范,所有示例均通过 以太坊 RPC 接口 完成,无需下载完整归档节点。为了节省时间,使用公共网络提供的节点;在本地开发阶段,再切换到自行搭建的 Dev 网络。

👉 想要快速体验链上交互而不重复造轮子?这里能帮你一键测试

首先安装 go-ethereum:

go get -u -v github.com/ethereum/go-ethereum/ethclient

查询转账记录

以太坊的交易信息里默认不携带发送者地址。我们需要从交易签名中 重推导公钥,再换算出地址,这便是一次“逆向寻址”。

package main
import (
    "context"
    "log"
    "math/big"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/godump/doa"
)

func main() {
    ethClient := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
    blockNumber := doa.Try(ethClient.BlockNumber(context.Background()))
    block := doa.Try(ethClient.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber))))

    for _, tx := range block.Transactions() {
        sender := doa.Try(types.Sender(
            types.LatestSignerForChainID(tx.ChainId()),
            tx,
        ))
        amount, _ := tx.Value().Float64()
        log.Printf("发送者 %s → 接收者 %s 数量 %.6f ETH", sender, *tx.To(), amount/1e18)
    }
}

运行后将输出最新区块中所有交易的 交易发送者接收者ETH 数量,帮助分析链上资金流向。

查询 USDT-ERC20 转账记录

USDT 是以太坊上最具代表性的 ERC-20 代币。通过 Transfer 事件日志(Topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef)即可精准定位所有 USDT 转账。

package main
import (
    "context"
    "log"
    "math/big"
    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/godump/doa"
)

func main() {
    ethClient := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
    blockNumber := doa.Try(ethClient.BlockNumber(context.Background()))
    query := ethereum.FilterQuery{
        Addresses: []common.Address{
            common.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"),
        },
        Topics: [][]common.Hash{
            {common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")},
        },
        FromBlock: big.NewInt(int64(blockNumber)),
        ToBlock:   big.NewInt(int64(blockNumber)),
    }
    txlog := doa.Try(ethClient.FilterLogs(context.Background(), query))
    for _, e := range txlog {
        amount := new(big.Int).SetBytes(e.Data)
        tokenAmount, _ := amount.Float64()
        log.Printf("交易哈希 %s:%s → %s 数量 %.6f USDT-ERC20",
            e.TxHash.Hex(),
            common.HexToAddress(e.Topics[1].Hex()),
            common.HexToAddress(e.Topics[2].Hex()),
            tokenAmount/1e6,
        )
    }
}

查询地址 USDT 余额

无交易 也能读取余额,只需要调用合约的只读函数 balanceOf(address)。使用 ABI 规则的函数签名 70a08231 构造交易输入即可。

package main
import (
    "context"
    "encoding/hex"
    "log"
    "math/big"
    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/godump/doa"
)

func main() {
    ethClient := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
    target := common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")
    data := doa.Try(hex.DecodeString(
        "70a08231000000000000000000000000F977814e90dA44bFA03b6295A0616a897441aceC",
    ))
    ret := doa.Try(ethClient.CallContract(context.Background(), ethereum.CallMsg{
        To:   &target,
        Data: data,
    }, nil))
    balance := new(big.Int).SetBytes(ret)
    total, _ := balance.Float64()
    log.Printf("当前 USDT 余额:%.6f", total/1e6)
}

查询随机地址余额(趣味彩票)

演示 随机私钥公钥地址 的派生逻辑并实时查询余额。尽管成功几率极低,它仍是学习密钥派生及链上资产管理的好方法。

package main
import (
    "context"
    "crypto/ecdsa"
    "encoding/hex"
    "log"
    "math/big"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/godump/doa"
)

func main() {
    ethClient := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
    for {
        pri := doa.Try(crypto.GenerateKey())
        pub := pri.Public().(*ecdsa.PublicKey)
        adr := crypto.PubkeyToAddress(*pub)
        bal, _ := ethClient.BalanceAt(context.Background(), adr, nil)
        log.Println(
            "尝试私钥:", "0x"+hex.EncodeToString(crypto.FromECDSA(pri)),
            "地址:", adr,
            "余额:", bal,
        )
        if bal.Cmp(big.NewInt(0)) > 0 {
            log.Println("撞库成功!")
            break
        }
    }
}
👉 想测试链上随机私钥但并不想消耗主网燃料?一键切换测试网即可

签名与验签

去中心化场景中,数字签名 确保数据完整性。以 secp256k1 算法为例,示范生成密钥、签名及二次验证。

package main
import (
    "crypto/rand"
    "encoding/hex"
    "log"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/godump/doa"
)

func main() {
    pri := doa.Try(crypto.HexToECDSA(
        "0000000000000000000000000000000000000000000000000000000000000001",
    ))
    pub := pri.Public()
    msg := make([]byte, 32)
    rand.Read(msg)
    sig := doa.Try(crypto.Sign(crypto.Keccak256(msg), pri))
    ok := crypto.VerifySignature(
        crypto.CompressPubkey(pub.(*ecdsa.PublicKey)),
        crypto.Keccak256(msg),
        sig[:64],
    )
    log.Println("验证结果:", ok)
}

构建并发送 ETH 转账交易

向黑洞地址转账 1 ETH,演示完整生命周期:获取 nonce、估算 GasPrice、签名广播。

package main
import (
    "context"
    "fmt"
    "math/big"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/godump/doa"
)

func main() {
    client := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
    priKey := doa.Try(crypto.HexToECDSA("...")) // 填写私钥
    pubKey := priKey.Public()
    from := crypto.PubkeyToAddress(*pubKey.(*ecdsa.PublicKey))

    nonce := doa.Try(client.PendingNonceAt(context.Background(), from))
    value := big.NewInt(1e18)                 // 1 ETH in wei
    gasLimit := uint64(21000)
    gasPrice := doa.Try(client.SuggestGasPrice(context.Background()))
    to := common.HexToAddress("0x0000000000000000000000000000000000000000")

    tx := types.NewTransaction(nonce, to, value, gasLimit, gasPrice, nil)
    signedTx := doa.Try(types.SignTx(tx, types.NewEIP155Signer(doa.Try(client.NetworkID(context.Background()))), priKey))
    doa.Nil(client.SendTransaction(context.Background(), signedTx))
    fmt.Println("交易哈希:", signedTx.Hash())
}

发行 ERC-20 或自定义合约

Solidity Storage 小合约:编译后字节码上传链上,即可存取任意数字。

pragma solidity ^0.8.0;
contract Storage {
    uint256 number;
    function set(uint256 num) public { number = num; }
    function get() public view returns (uint256) { return number; }
}

部署流程:本地读取已编译字节码 storage → 组合交易 → 签名 → 上链。

data, _ := os.ReadFile("storage")
// ... 剩余部署代码同上

调用合约只读函数

无需签名,利用 CallContract 即可向合约 只读函数 调阅数据:

ret, _ := client.CallContract(context.Background(), ethereum.CallMsg{
    To:   &contractAddress,
    Data: crypto.Keccak256([]byte("get()"))[:4],
}, nil)
fmt.Println("当前存储:", big.NewInt(0).SetBytes(ret))

写状态,则将函数选择器 + 参数拼接字节码后通过交易发送,示例可参考上节 data 构造。

本地开发节点快速启动

git clone https://github.com/ethereum/go-ethereum --branch release/1.13
cd go-ethereum && make geth
./build/bin/geth --dev --http --http.api eth,web3

另开终端:

geth attach http://127.0.0.1:8545
> eth.sendTransaction({
    from: eth.accounts[0],
    to: '0x7e5f4552091a69125d5dfcb7b8c2659029395bdf',
    value: web3.toWei(10000, 'ether')
})

本地链秒级出块、无限 ETH,是真正适合脚本联调的沙盒环境。


常见问题(FAQ)

Q1:为什么一定要计算交易签名的发送者地址?

A1:以太坊交易本身只携带签名字段(r, s, v),节点通过 ECDSA 从签名推出公钥,再派生地址,相当于做一次“身份验证”。

Q2:USDT 余额与 ETH 余额查询有什么区别?

A2:ETH 余额通过 eth_getBalance 一条 RPC 即可获得;USDT 作为 ERC-20 合约,需要调用合约内 balanceOf(address) 函数。

Q3:如何在测试网而非主网运行示例?

A3:将节点 URL 换成 Goerli、Sepolia 任意公开 RPC,并在合约、地址、链 ID 全部更新即可。

Q4:部署合约的手续费何时确认?

A4:交易广播后等待出块即可。Dev 链几秒内出块;主网则需要参看所付 gasPrice 与网络拥堵情况。

Q5:为什么我签名通过后验证仍旧失败?

A5:请校验哈希前后有无额外填充,务必使用 crypto.Keccak256(str) 而非直接对字符串签名。

Q6:如何批量查询多个地址的 USDT 余额?

A6:可并发代用 CallContract,但更高效的是离线计算 ContractCall 的 calldata,再一次性发送到多调用打包合约(如 Multicall)。