區塊鏈應用與以太坊的交互
我們要談的交互
首先要明確一點,以太坊是一個去中心化的平臺,他不可能為了某個項目而新增交互接口。
我這里說的交互是指應用是鏈上合約的交互,更明確的說,是chainlink,arbitrum,cosmos這些鏈下應用與鏈上合約的交互。
所以這里我不是想說以下交互方式:
- 通過錢包交互:將應用打包成一個網頁,連接類似與
小狐貍這樣的錢包,與鏈發生交互。 - 手動構造交易:構建交易tx并使用私鑰對交易進行簽名,然后直接發送到鏈給定的接口上。
對于一些簡單的調用,通過上面兩種方式是可行的,例如我們只是做一些nft的構造,通過錢包是最合適的。但是對于向arbitrum這類
鏈上的應用,如果要做一個交互式單步證明,在這個過程中我需要監控鏈上合約拋出的event,分析event并構造出相應結果。
這個時候錢包就很難插手,而如果主動構造交易并簽名,那么過程太繁瑣。實際上以太坊上已經提供了相關的工具鏈。
我們要談的交互方式就是,通過以太坊支持的工具鏈實現與以太坊的交互
工具鏈

簡單而言是使用以太坊工具將sol的合約代碼轉換成go的類文件,并對調用細節進行封裝。
而在應用層(arbitrum,chainlink這一層)可以直接將對應參數傳過去就可以.
以arbitrum為例:
生成go代碼
生成工具在: https://github.com/OffchainLabs/nitro/blob/master/solgen/gen.go
由于arbitrum用到鏈makefile,所我們沒法通過運行這個文件(go run gen.go)的方式,去生成合約文件。
不過實際上這是一個路徑的問題,在代碼的第71行:
filePaths, err := filepath.Glob(filepath.Join(parent, "contracts", "build", "contracts", "src", "*", "*.sol", "*.json"))
if err != nil {
log.Fatal(err)
}
filePathsSafeSmartAccount, err := filepath.Glob(filepath.Join(parent, "safe-smart-account", "build", "artifacts", "contracts", "*", "*.sol", "*.json"))
if err != nil {
log.Fatal(err)
}
filePathsSafeSmartAccountOuter, err := filepath.Glob(filepath.Join(parent, "safe-smart-account", "build", "artifacts", "contracts", "*.sol", "*.json"))
if err != nil {
log.Fatal(err)
}
這里實際上就指定了合約代碼的路徑,當然如果只是初次下載合約文件應該是看不到build目錄的,需要在合約所在項目構建一下,才能生成這個build文件
構造方法:
yarn --cwd contracts build
yarn --cwd contracts build:forge:yul
# 其實就是hardhat compile的產物
然后就會在solgen這個目錄下生成對應的go文件

我們具體看一下這個生成的代碼怎么用
使用生成的代碼
首先可以看到在生成的代碼中,每一個合約都有一個對應的類
eg:
// ChallengeLibTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
type ChallengeLibTransactorRaw struct {
Contract *ChallengeLibTransactor // Generic write-only contract binding to access the raw methods on
}
合約中的方法則對應到類的方法
eg:
// Solidity: function oneStepProveExecution(uint64 challengeIndex, (uint256,uint256,bytes32[],uint256) selection, bytes proof) returns()
func (_ChallengeManager *ChallengeManagerTransactor) OneStepProveExecution(opts *bind.TransactOpts, challengeIndex uint64, selection ChallengeLibSegmentSelection, proof []byte) (*types.Transaction, error) {
return _ChallengeManager.contract.Transact(opts, "oneStepProveExecution", challengeIndex, selection, proof)
}
其中關鍵在與這里的opts,如果我們繼續進到這個Transact方法里面會發現,鏈上的信息都是由這里的opts獲取的,用戶簽名接口,用戶信息等
那么對于一個合約調用就分為兩部分,一是調用參數,也就是這里的opts和合約參數,一是對接一臺的的client
參數
進入到這里的opts,這是以太坊里面的數據結構
// valid Ethereum transaction.
type TransactOpts struct {
From common.Address // Ethereum account to send the transaction from
Nonce *big.Int // Nonce to use for the transaction execution (nil = use pending state)
Signer SignerFn // Method to use for signing the transaction (mandatory)
Value *big.Int // Funds to transfer along the transaction (nil = 0 = no funds)
GasPrice *big.Int // Gas price to use for the transaction execution (nil = gas price oracle)
GasFeeCap *big.Int // Gas fee cap to use for the 1559 transaction execution (nil = gas price oracle)
GasTipCap *big.Int // Gas priority fee cap to use for the 1559 transaction execution (nil = gas price oracle)
GasLimit uint64 // Gas limit to set for the transaction execution (0 = estimate)
GasMargin uint64 // Arbitrum: adjusts gas estimate by this many basis points (0 = no adjustment)
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
NoSend bool // Do all transact steps but do not send the transaction
}
我們可以看到,這里包含用戶信息的簽名數據
對于鏈下的開發者,我們需要構造這個結構,來調用方法
client
我們有鏈合約的調用參數,那么就需要有一個client來為我們發送交易,(雖然構造交易的時候也用到鏈client,但這都是已經被工具封裝好的,開發者沒必要細究它是怎么構建的)
實際上client是在我們構建合約對象時構建的
// NewChallengeManager creates a new instance of ChallengeManager, bound to a specific deployed contract.
func NewChallengeManager(address common.Address, backend bind.ContractBackend) (*ChallengeManager, error) {
contract, err := bindChallengeManager(address, backend, backend, backend)
if err != nil {
return nil, err
}
return &ChallengeManager{ChallengeManagerCaller: ChallengeManagerCaller{contract: contract}, ChallengeManagerTransactor: ChallengeManagerTransactor{contract: contract}, ChallengeManagerFilterer: ChallengeManagerFilterer{contract: contract}}, nil
}
在構建ChallengeManger這個合約對象時,我們需要給他一個backend,這里的backend就是鏈的client,地址也就是鏈上的合約地址
可以看到backend也是bind這個包里面的,實際上它也是以太坊源碼里面的包
type ContractBackend interface {
ContractCaller
ContractTransactor
ContractFilterer
}
type ContractCaller interface {
// CodeAt returns the code of the given account. This is needed to differentiate
// between contract internal errors and the local chain being out of sync.
CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error)
// CallContract executes an Ethereum contract call with the specified data as the
// input.
CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
}
type ContractTransactor interface {
ethereum.GasEstimator
ethereum.GasPricer
ethereum.GasPricer1559
ethereum.TransactionSender
// HeaderByNumber returns a block header from the current canonical chain. If
// number is nil, the latest known header is returned.
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
// PendingCodeAt returns the code of the given account in the pending state.
PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error)
// PendingNonceAt retrieves the current pending nonce associated with an account.
PendingNonceAt(ctx context.Context, account common.Address) (uint64, error)
}
type ContractFilterer interface {
ethereum.LogFilterer
}
這個client看起來構造很麻煩,實際上也是有跡可循的,這里面都是以太坊里的數據結構,所以理論上以太坊里面已經有對象實現了這些接口
type Client struct {
c rpc.ClientInterface
}
type ClientInterface interface {
CallContext(ctx_in context.Context, result interface{}, method string, args ...interface{}) error
EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*ClientSubscription, error)
BatchCallContext(ctx context.Context, b []BatchElem) error
Close()
}
// Client represents a connection to an RPC server.
type Client struct {
idgen func() ID // for subscriptions
isHTTP bool // connection type: http, ws or ipc
services *serviceRegistry
idCounter atomic.Uint32
// This function, if non-nil, is called when the connection is lost.
reconnectFunc reconnectFunc
// config fields
batchItemLimit int
batchResponseMaxSize int
// writeConn is used for writing to the connection on the caller's goroutine. It should
// only be accessed outside of dispatch, with the write lock held. The write lock is
// taken by sending on reqInit and released by sending on reqSent.
writeConn jsonWriter
// for dispatch
close chan struct{}
closing chan struct{} // closed when client is quitting
didClose chan struct{} // closed when client quits
reconnected chan ServerCodec // where write/reconnect sends the new connection
readOp chan readOp // read messages
readErr chan error // errors from read
reqInit chan *requestOp // register response IDs, takes write lock
reqSent chan error // signals write completion, releases write lock
reqTimeout chan *requestOp // removes response IDs when call timeout expires
}

浙公網安備 33010602011771號