stage point before I try to dedup contract interaction api
This commit is contained in:
parent
aa651aecab
commit
a2479e1aed
|
@ -50,6 +50,7 @@ func JSON(reader io.Reader) (ABI, error) {
|
|||
|
||||
var abi ABI
|
||||
if err := dec.Decode(&abi); err != nil {
|
||||
fmt.Println(err)
|
||||
return ABI{}, err
|
||||
}
|
||||
return abi, nil
|
||||
|
|
|
@ -28,6 +28,10 @@ type ContractInstance interface {
|
|||
Backend() ContractBackend
|
||||
}
|
||||
|
||||
type ContractInstanceV2 interface {
|
||||
Address() common.Address
|
||||
}
|
||||
|
||||
func CallRaw(instance ContractInstance, opts *CallOpts, input []byte) ([]byte, error) {
|
||||
backend := instance.Backend()
|
||||
c := NewBoundContract(instance.Address(), abi.ABI{}, backend, backend, backend)
|
||||
|
|
|
@ -55,22 +55,17 @@ var (
|
|||
// {{.Type}}Instance represents a deployed instance of the {{.Type}} contract.
|
||||
type {{.Type}}Instance struct {
|
||||
{{.Type}}
|
||||
address common.Address
|
||||
backend bind.ContractBackend
|
||||
address common.Address // consider removing this, not clear what it's used for now (and why did we need custom deploy method on previous abi?)
|
||||
}
|
||||
|
||||
func New{{.Type}}Instance(c *{{.Type}}, address common.Address, backend bind.ContractBackend) *{{.Type}}Instance {
|
||||
return &{{.Type}}Instance{Db: *c, address: address, backend: backend}
|
||||
func New{{.Type}}Instance(c *{{.Type}}, address common.Address) *{{.Type}}Instance {
|
||||
return &{{.Type}}Instance{ {{$contract.Type}}: *c, address: address}
|
||||
}
|
||||
|
||||
func (i *{{$contract.Type}}Instance) Address() common.Address {
|
||||
return i.address
|
||||
}
|
||||
|
||||
func (i *{{$contract.Type}}Instance) Backend() bind.ContractBackend {
|
||||
return i.backend
|
||||
}
|
||||
|
||||
// {{.Type}} is an auto generated Go binding around an Ethereum contract.
|
||||
type {{.Type}} struct {
|
||||
abi abi.ABI
|
||||
|
@ -136,7 +131,7 @@ var (
|
|||
Raw *types.Log // Blockchain specific contextual infos
|
||||
}
|
||||
|
||||
func (_{{$contract.Type}} *{{$contract.Type}}) {{.Normalized.Name}}EventID() common.Hash {
|
||||
func {{$contract.Type}}{{.Normalized.Name}}EventID() common.Hash {
|
||||
return common.HexToHash("{{.Original.ID}}")
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
type V2Backend interface {
|
||||
SuggestGasPrice(ctx context.Context) (*big.Int, error)
|
||||
PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error)
|
||||
PendingNonceAt(ctx context.Context, account common.Address) (uint64, error)
|
||||
SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error)
|
||||
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
|
||||
SendTransaction(ctx context.Context, tx *types.Transaction) error
|
||||
SuggestGasTipCap(ctx context.Context) (*big.Int, error)
|
||||
EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error)
|
||||
}
|
|
@ -1,12 +1,16 @@
|
|||
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) {
|
||||
|
@ -19,10 +23,44 @@ func FilterLogs[T any](instance bind.ContractInstance, opts *bind.FilterOpts, ev
|
|||
return &EventIterator[T]{unpack: unpack, logs: logs, sub: sub}, nil
|
||||
}
|
||||
|
||||
func WatchLogs[T any](instance bind.ContractInstance, opts *bind.WatchOpts, eventID common.Hash, unpack func(*types.Log) (*T, error), sink chan<- *T, topics ...[]any) (event.Subscription, error) {
|
||||
backend := instance.Backend()
|
||||
c := bind.NewBoundContract(instance.Address(), abi.ABI{}, backend, backend, backend)
|
||||
logs, sub, err := c.WatchLogs(opts, eventID.String(), topics...)
|
||||
// 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
|
||||
}
|
||||
|
@ -128,6 +166,165 @@ func Transact(instance bind.ContractInstance, opts *bind.TransactOpts, input []b
|
|||
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)
|
||||
|
|
|
@ -4,6 +4,9 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind/testdata/v2_generated_testcase"
|
||||
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"io"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
|
@ -18,65 +21,6 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
const deployer = "6080604052348015600e575f80fd5b506102098061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c80636da1cd55146100435780637b0cb83914610061578063bf54fad41461006b575b5f80fd5b61004b610087565b60405161005891906100e9565b60405180910390f35b61006961008f565b005b61008560048036038101906100809190610130565b6100c8565b005b5f8054905090565b607b7f72c79b1cb25b1b49ae522446226e1591b80634619cef7e71846da52b61b7061d6040516100be906101b5565b60405180910390a2565b805f8190555050565b5f819050919050565b6100e3816100d1565b82525050565b5f6020820190506100fc5f8301846100da565b92915050565b5f80fd5b61010f816100d1565b8114610119575f80fd5b50565b5f8135905061012a81610106565b92915050565b5f6020828403121561014557610144610102565b5b5f6101528482850161011c565b91505092915050565b5f82825260208201905092915050565b7f737472696e6700000000000000000000000000000000000000000000000000005f82015250565b5f61019f60068361015b565b91506101aa8261016b565b602082019050919050565b5f6020820190508181035f8301526101cc81610193565b905091905056fea2646970667358221220212a3a765a98254b596386fdfd10318f9a4bf19e8c9ca9ffa363f990c1798bf664736f6c634300081a0033"
|
||||
|
||||
const contractABIStr = `
|
||||
[
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "firstArg",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "string",
|
||||
"name": "secondArg",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "ExampleEvent",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "emitEvent",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "num",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "mutateStorageVal",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "retrieveStorageVal",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
var testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
|
||||
// JSON returns a parsed ABI interface and error if it failed.
|
||||
|
@ -96,10 +40,13 @@ func TestV2(t *testing.T) {
|
|||
types.GenesisAlloc{
|
||||
testAddr: {Balance: big.NewInt(10000000000000000)},
|
||||
},
|
||||
func(nodeConf *node.Config, ethConf *ethconfig.Config) {
|
||||
ethConf.Genesis.Difficulty = big.NewInt(0)
|
||||
},
|
||||
)
|
||||
defer backend.Close()
|
||||
|
||||
contractABI, err := JSON(strings.NewReader(contractABIStr))
|
||||
contractABI, err := JSON(strings.NewReader(v2_generated_testcase.V2GeneratedTestcaseMetaData.ABI))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -120,15 +67,6 @@ func TestV2(t *testing.T) {
|
|||
return signedTx, nil
|
||||
},
|
||||
Context: context.Background(),
|
||||
/*
|
||||
Value: nil,
|
||||
GasPrice: nil,
|
||||
GasFeeCap: nil,
|
||||
GasTipCap: nil,
|
||||
GasLimit: 0,
|
||||
AccessList: nil,
|
||||
NoSend: false,
|
||||
*/
|
||||
}
|
||||
// we should just be able to use the backend directly, instead of using
|
||||
// this deprecated interface. However, the simulated backend no longer
|
||||
|
@ -137,9 +75,28 @@ func TestV2(t *testing.T) {
|
|||
Backend: backend,
|
||||
Client: backend.Client(),
|
||||
}
|
||||
_, _, _, err = bind.DeployContract(&opts, contractABI, common.Hex2Bytes(deployer), &bindBackend)
|
||||
address, _, boundContract, err := bind.DeployContract(&opts, contractABI, common.Hex2Bytes(v2_generated_testcase.V2GeneratedTestcaseMetaData.Bin), &bindBackend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contract, err := v2_generated_testcase.NewV2GeneratedTestcase()
|
||||
if err != nil {
|
||||
t.Fatal(err) // can't happen here with the example used. consider removing this block
|
||||
}
|
||||
contractInstance := v2_generated_testcase.NewV2GeneratedTestcaseInstance(contract, address)
|
||||
sinkCh := make(chan *v2_generated_testcase.V2GeneratedTestcase)
|
||||
// q: what extra functionality is given by specifying this as a custom method, instead of catching emited methods
|
||||
// from the sync channel?
|
||||
unpackStruct := func(log *types.Log) (v2_generated_testcase.V2GeneratedTestcaseStruct, error) {
|
||||
res, err := contract.UnpackStructEvent(log)
|
||||
return *res, err
|
||||
}
|
||||
// TODO: test using various topics
|
||||
// q: does nil topics mean to accept any?
|
||||
sub, err := WatchLogs[v2_generated_testcase.V2GeneratedTestcaseStruct](contractInstance, v2_generated_testcase.V2GeneratedTestcaseStructEventID(), unpackStruct, sinkCh)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sub.Unsubscribe()
|
||||
// send a balance to our contract (contract must accept ether by default)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue