go-ethereum/accounts/abi/bind/v2/lib.go

333 lines
10 KiB
Go

package v2
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
"math/big"
)
func FilterLogs[T any](instance bind.ContractInstance, opts *bind.FilterOpts, eventID common.Hash, unpack func(*types.Log) (*T, error), topics ...[]any) (*EventIterator[T], error) {
backend := instance.Backend()
c := bind.NewBoundContract(instance.Address(), abi.ABI{}, backend, backend, backend)
logs, sub, err := c.FilterLogs(opts, eventID.String(), topics...)
if err != nil {
return nil, err
}
return &EventIterator[T]{unpack: unpack, logs: logs, sub: sub}, nil
}
// WatchOpts is the collection of options to fine tune subscribing for events
// within a bound contract.
type WatchOpts struct {
Start *uint64 // Start of the queried range (nil = latest)
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
}
func watchLogs(backend V2Backend, address common.Address, opts *WatchOpts, eventID common.Hash, query ...[]interface{}) (chan types.Log, event.Subscription, error) {
// Don't crash on a lazy user
if opts == nil {
opts = new(WatchOpts)
}
// Append the event selector to the query parameters and construct the topic set
query = append([][]interface{}{{eventID}}, query...)
topics, err := abi.MakeTopics(query...)
if err != nil {
return nil, nil, err
}
// Start the background filtering
logs := make(chan types.Log, 128)
config := ethereum.FilterQuery{
Addresses: []common.Address{address},
Topics: topics,
}
if opts.Start != nil {
config.FromBlock = new(big.Int).SetUint64(*opts.Start)
}
sub, err := backend.SubscribeFilterLogs(ensureContext(opts.Context), config, logs)
if err != nil {
return nil, nil, err
}
return logs, sub, nil
}
func WatchLogs[T any](address common.Address, backend V2Backend, opts *WatchOpts, eventID common.Hash, unpack func(*types.Log) (*T, error), sink chan<- *T, topics ...[]any) (event.Subscription, error) {
logs, sub, err := watchLogs(backend, address, opts, eventID, topics...)
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
ev, err := unpack(&log)
if err != nil {
return err
}
select {
case sink <- ev:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// EventIterator is returned from FilterLogs and is used to iterate over the raw logs and unpacked data for events.
type EventIterator[T any] struct {
Event *T // Event containing the contract specifics and raw log
unpack func(*types.Log) (*T, error) // Unpack function for the event
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *EventIterator[T]) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
res, err := it.unpack(&log)
if err != nil {
it.fail = err
return false
}
it.Event = res
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
res, err := it.unpack(&log)
if err != nil {
it.fail = err
return false
}
it.Event = res
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *EventIterator[T]) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *EventIterator[T]) Close() error {
it.sub.Unsubscribe()
return nil
}
func Transact(instance bind.ContractInstance, opts *bind.TransactOpts, input []byte) (*types.Transaction, error) {
var (
addr = instance.Address()
backend = instance.Backend()
)
c := bind.NewBoundContract(addr, abi.ABI{}, backend, backend, backend)
return c.RawTransact(opts, input)
}
// ensureContext is a helper method to ensure a context is not nil, even if the
// user specified it as such.
func ensureContext(ctx context.Context) context.Context {
if ctx == nil {
return context.Background()
}
return ctx
}
// SignerFn is a signer function callback when a contract requires a method to
// sign the transaction before submission.
type SignerFn func(common.Address, *types.Transaction) (*types.Transaction, error)
// TransactOpts is the collection of authorization data required to create a
// 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)
AccessList types.AccessList // Access list to set for the transaction execution (nil = no access list)
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
}
func estimateGasLimit(backend V2Backend, address common.Hash, opts *TransactOpts, contract *common.Address, input []byte, gasPrice, gasTipCap, gasFeeCap, value *big.Int) (uint64, error) {
if contract != nil {
// Gas estimation cannot succeed without code for method invocations.
if code, err := backend.PendingCodeAt(ensureContext(opts.Context), address); err != nil {
return 0, err
} else if len(code) == 0 {
return 0, ErrNoCode
}
}
msg := ethereum.CallMsg{
From: opts.From,
To: contract,
GasPrice: gasPrice,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Value: value,
Data: input,
}
return backend.EstimateGas(ensureContext(opts.Context), msg)
}
func getNonce(backend V2Backend, opts *TransactOpts) (uint64, error) {
if opts.Nonce == nil {
return backend.PendingNonceAt(ensureContext(opts.Context), opts.From)
} else {
return opts.Nonce.Uint64(), nil
}
}
func createLegacyTx(backend V2Backend, address common.Hash, opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
if opts.GasFeeCap != nil || opts.GasTipCap != nil || opts.AccessList != nil {
return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas or accessList specified but london is not active yet")
}
// Normalize value
value := opts.Value
if value == nil {
value = new(big.Int)
}
// Estimate GasPrice
gasPrice := opts.GasPrice
if gasPrice == nil {
price, err := backend.SuggestGasPrice(ensureContext(opts.Context))
if err != nil {
return nil, err
}
gasPrice = price
}
// Estimate GasLimit
gasLimit := opts.GasLimit
if opts.GasLimit == 0 {
var err error
gasLimit, err = estimateGasLimit(backend, address, opts, contract, input, gasPrice, nil, nil, value)
if err != nil {
return nil, err
}
}
// create the transaction
nonce, err := getNonce(backend, opts)
if err != nil {
return nil, err
}
baseTx := &types.LegacyTx{
To: contract,
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
Value: value,
Data: input,
}
return types.NewTx(baseTx), nil
}
const basefeeWiggleMultiplier = 2
func createDynamicTx(backend V2Backend, opts *TransactOpts, contract *common.Address, input []byte, head *types.Header) (*types.Transaction, error) {
// Normalize value
value := opts.Value
if value == nil {
value = new(big.Int)
}
// Estimate TipCap
gasTipCap := opts.GasTipCap
if gasTipCap == nil {
tip, err := backend.SuggestGasTipCap(ensureContext(opts.Context))
if err != nil {
return nil, err
}
gasTipCap = tip
}
// Estimate FeeCap
gasFeeCap := opts.GasFeeCap
if gasFeeCap == nil {
gasFeeCap = new(big.Int).Add(
gasTipCap,
new(big.Int).Mul(head.BaseFee, big.NewInt(basefeeWiggleMultiplier)),
)
}
if gasFeeCap.Cmp(gasTipCap) < 0 {
return nil, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap)
}
// Estimate GasLimit
gasLimit := opts.GasLimit
if opts.GasLimit == 0 {
var err error
gasLimit, err = c.estimateGasLimit(opts, contract, input, nil, gasTipCap, gasFeeCap, value)
if err != nil {
return nil, err
}
}
// create the transaction
nonce, err := c.getNonce(opts)
if err != nil {
return nil, err
}
baseTx := &types.DynamicFeeTx{
To: contract,
Nonce: nonce,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Gas: gasLimit,
Value: value,
Data: input,
AccessList: opts.AccessList,
}
return types.NewTx(baseTx), nil
}
func Transfer(instance bind.ContractInstance, opts *bind.TransactOpts) (*types.Transaction, error) {
backend := instance.Backend()
c := bind.NewBoundContract(instance.Address(), abi.ABI{}, backend, backend, backend)
return c.Transfer(opts)
}