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) }