区块链应用与以太坊的交互
我们要谈的交互
首先要明确一点,以太坊是一个去中心化的平台,他不可能为了某个项目而新增交互接口。
我这里说的交互是指应用是链上合约的交互,更明确的说,是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 }
本文作者:bighu
本文链接:https://www.cnblogs.com/blogforeverything/p/18408576
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步