The interaction we're talking about
First of all, let's make it clear that Ether is a decentralized platform, and it's not possible for him to add new interactive interfaces for the sake of a particular project.
The interaction I'm talking about here is the interaction between the application and the on-chain contract, or more specifically, the interaction between the off-chain application and the on-chain contract, such as chainlink, arbitrum, cosmos.
So here I'm not trying to talk about the following interactions:
- Interact via wallet: package the app as a web page that connects similarly with the
fig. sly and treacherous person
Such a wallet that interacts with the chain. - Manual construction of the transaction: construct the transaction tx and sign it with the private key, then send it directly to the interface given by the chain.
For some simple calls, it is feasible to go through the above two ways, e.g. if we are just doing some nft construction, going through the wallet is the way to go. But for calls to arbitrum, such as
If I want to do an interactive single-step proof for an on-chain application, I need to monitor the event thrown by the on-chain contract, analyze the event, and construct the corresponding result during the process.
At this point it becomes difficult for the wallet to step in, and the process is too cumbersome if the transaction is actively constructed and signed. The toolchain for this is actually already available on Ether.
The type of interaction we're going to talk about is that of interacting with the Ethernet via an Ethernet-backed toolchain implementation
toolchain
In simple terms it is the use of an ethereum tool to convert sol's contract code into a go class file and encapsulate the call details.
In the application layer (arbitrum, chainlink) you can pass the corresponding parameters directly.
Take arbitrum for example.
Generate go code
Generate tools in:/OffchainLabs/nitro/blob/master/solgen/
Since arbitrum use chain makefile, so we can not run this file (go run) way, to generate the contract file.
It's actually a path issue though, on line 71 of the code:
filePaths, err := ((parent, "contracts", "build", "contracts", "src", "*", "*.sol", "*.json"))
if err != nil {
(err)
}
filePathsSafeSmartAccount, err := ((parent, "safe-smart-account", "build", "artifacts", "contracts", "*", "*.sol", "*.json"))
if err != nil {
(err)
}
filePathsSafeSmartAccountOuter, err := ((parent, "safe-smart-account", "build", "artifacts", "contracts", "*.sol", "*.json"))
if err != nil {
(err)
}
This actually specifies the path to the contract code, of course, if you just download the contract file for the first time, you should not see the build directory, and you need to build the project where the contract is located in order to generate the build file.
Construction Methods:
yarn --cwd contracts build
yarn --cwd contracts build:forge:yul
# In fact, it'shardhat compileproduct of
Then the corresponding go file will be generated in the solgen directory
Let's look specifically at how this generated code works
Using the generated code
First you can see that in the generated code, each contract has a corresponding class
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
}
The methods in the contract then correspond to the methods of the class
eg:
// Solidity: function oneStepProveExecution(uint64 challengeIndex, (uint256,uint256,bytes32[],uint256) selection, bytes proof) returns()
func (_ChallengeManager *ChallengeManagerTransactor) OneStepProveExecution(opts *, challengeIndex uint64, selection ChallengeLibSegmentSelection, proof []byte) (*, error) {
return _ChallengeManager.(opts, "oneStepProveExecution", challengeIndex, selection, proof)
}
The key here is with the opts, if we continue into the Transact method will find that the chain of information are obtained by the opts here, the user signature interface, user information and so on.
Then for a contract call is divided into two parts, one is the call parameters, that is, the opts and contract parameters here, and the other is the client of the docking machine.
parameters
Go to the opts here, this is the data structure inside the ethereum.
// valid Ethereum transaction.
type TransactOpts struct {
From // Ethereum account to send the transaction from
Nonce * // Nonce to use for the transaction execution (nil = use pending state)
Signer SignerFn // Method to use for signing the transaction (mandatory)
Value * // Funds to transfer along the transaction (nil = 0 = no funds)
GasPrice * // Gas price to use for the transaction execution (nil = gas price oracle)
GasFeeCap * // Gas fee cap to use for the 1559 transaction execution (nil = gas price oracle)
GasTipCap * // 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 // Network context to support cancellation and timeouts (nil = no timeout)
NoSend bool // Do all transact steps but do not send the transaction
}
As we can see, here is the signature data containing the user's information
For developers down the chain, we need to construct this structure to call the method
client
We have the call parameters for the chain contract, so we need a client to send the transaction for us. (Although the chain client is also used in constructing the transaction, this is already encapsulated by the tool, so there is no need for the developer to look into how it is constructed.)
The client is actually constructed when we build the contract object
// NewChallengeManager creates a new instance of ChallengeManager, bound to a specific deployed contract.
func NewChallengeManager(address , backend ) (*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
}
In the construction of ChallengeManger contract object, we need to give him a backend, here the backend is the chain of clients, the address is the chain of contracts address
You can see that backend is also in the bind package, which is actually a package in the ethereum source code.
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 , contract , blockNumber *) ([]byte, error)
// CallContract executes an Ethereum contract call with the specified data as the
// input.
CallContract(ctx , call , blockNumber *) ([]byte, error)
}
type ContractTransactor interface {
ethereum.GasPricer1559
// HeaderByNumber returns a block header from the current canonical chain. If
// number is nil, the latest known header is returned.
HeaderByNumber(ctx , number *) (*, error)
// PendingCodeAt returns the code of the given account in the pending state.
PendingCodeAt(ctx , account ) ([]byte, error)
// PendingNonceAt retrieves the current pending nonce associated with an account.
PendingNonceAt(ctx , account ) (uint64, error)
}
type ContractFilterer interface {
}
The client looks like a pain in the ass to construct, but it's actually well documented. It's all about the data structures in the ether, so theoretically there are already objects in the ether that implement these interfaces
type Client struct {
c
}
type ClientInterface interface {
CallContext(ctx_in , result interface{}, method string, args ...interface{}) error
EthSubscribe(ctx , channel interface{}, args ...interface{}) (*ClientSubscription, error)
BatchCallContext(ctx , 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
}