diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index eb47e49989..f103910c17 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -151,6 +151,18 @@ func DeployContract(opts *TransactOpts, abi abi.ABI, bytecode []byte, backend Co return c.address, tx, c, nil } +func DeployContractRaw(opts *TransactOpts, abi abi.ABI, bytecode []byte, backend ContractBackend, packedParams []byte) (common.Address, *types.Transaction, *BoundContract, error) { + // Otherwise try to deploy the contract + c := NewBoundContract(common.Address{}, abi, backend, backend, backend) + + tx, err := c.transact(opts, nil, append(bytecode, packedParams...)) + if err != nil { + return common.Address{}, nil, nil, err + } + c.address = crypto.CreateAddress(opts.From, tx.Nonce()) + return c.address, tx, c, nil +} + // Call invokes the (constant) contract method with params as input values and // sets the output to result. The result type might be a single field for simple // returns, a slice of interfaces for anonymous returns and a struct for named @@ -179,6 +191,10 @@ func (c *BoundContract) Call(opts *CallOpts, results *[]interface{}, method stri return c.abi.UnpackIntoInterface(res[0], method, output) } +func (c *BoundContract) CallRaw(opts *CallOpts, input []byte) ([]byte, error) { + return c.call(opts, input) +} + func (c *BoundContract) call(opts *CallOpts, input []byte) ([]byte, error) { // Don't crash on a lazy user if opts == nil { diff --git a/accounts/abi/bind/bind.go b/accounts/abi/bind/bind.go index d5aab35a5d..1f5d598e61 100644 --- a/accounts/abi/bind/bind.go +++ b/accounts/abi/bind/bind.go @@ -312,17 +312,19 @@ func bind(types []string, abis []string, bytecodes []string, fsigs []map[string] } contracts[types[i]] = &tmplContract{ - Type: capitalise(types[i]), - InputABI: strings.ReplaceAll(strippedABI, "\"", "\\\""), - InputBin: strings.TrimPrefix(strings.TrimSpace(bytecodes[i]), "0x"), - Constructor: evmABI.Constructor, - Calls: calls, - Transacts: transacts, - Fallback: fallback, - Receive: receive, - Events: events, - Libraries: make(map[string]string), + Type: capitalise(types[i]), + InputABI: strings.ReplaceAll(strippedABI, "\"", "\\\""), + InputBin: strings.TrimPrefix(strings.TrimSpace(bytecodes[i]), "0x"), + Constructor: evmABI.Constructor, + Calls: calls, + Transacts: transacts, + Fallback: fallback, + Receive: receive, + Events: events, + Libraries: make(map[string]string), + AllLibraries: make(map[string]string), } + // Function 4-byte signatures are stored in the same sequence // as types, if available. if len(fsigs) > i { @@ -340,14 +342,54 @@ func bind(types []string, abis []string, bytecodes []string, fsigs []map[string] if _, ok := isLib[name]; !ok { isLib[name] = struct{}{} } + } } + // Check if that type has already been identified as a library + for i := 0; i < len(types); i++ { + _, ok := isLib[types[i]] + contracts[types[i]].Library = ok + } + + // recursively traverse the library dependency graph + // of the contract, flattening it into a list. + // + // For abigenv2, we do not generate contract deploy + // methods (which in v1 recursively deploy their + // library dependencies). So, the entire set of + // library dependencies is required, and we will + // the order to deploy and link them at runtime. + var findDeps func(contract *tmplContract) map[string]struct{} + findDeps = func(contract *tmplContract) map[string]struct{} { + // 1) match all libraries that this contract depends on + re, err := regexp.Compile("__\\$([a-f0-9]+)\\$__") + if err != nil { + panic(err) + } + libBin := contracts[contract.Type].InputBin + matches := re.FindAllStringSubmatch(libBin, -1) + var result map[string]struct{} + + // 2) recurse, gathering nested library dependencies + for _, match := range matches { + pattern := match[1] + result[pattern] = struct{}{} + depContract := contracts[pattern] + for subPattern, _ := range findDeps(depContract) { + result[subPattern] = struct{}{} + } + } + return result + } + // take the set of library patterns, convert it to a map of pattern -> type + deps := findDeps(contracts[types[i]]) + contracts[types[i]].AllLibraries = make(map[string]string) + for contractPattern, _ := range deps { + contractType := libs[contractPattern] + contracts[types[i]].AllLibraries[contractType] = contractPattern + } } - // Check if that type has already been identified as a library - for i := 0; i < len(types); i++ { - _, ok := isLib[types[i]] - contracts[types[i]].Library = ok - } + // Generate the contract template data content and render it data := &tmplData{ Package: pkg, diff --git a/accounts/abi/bind/lib.go b/accounts/abi/bind/lib.go index 6d6dd7b52e..3ab6a8aee6 100644 --- a/accounts/abi/bind/lib.go +++ b/accounts/abi/bind/lib.go @@ -17,7 +17,6 @@ package bind import ( - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" ) @@ -30,10 +29,5 @@ type ContractInstance interface { 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) - return c.call(opts, input) + Backend() ContractBackend } diff --git a/accounts/abi/bind/template.go b/accounts/abi/bind/template.go index fa75281069..2e7ff7f4f5 100644 --- a/accounts/abi/bind/template.go +++ b/accounts/abi/bind/template.go @@ -32,18 +32,19 @@ type tmplData struct { // tmplContract contains the data needed to generate an individual contract binding. type tmplContract struct { - Type string // Type name of the main contract binding - InputABI string // JSON ABI used as the input to generate the binding from - InputBin string // Optional EVM bytecode used to generate deploy code from - FuncSigs map[string]string // Optional map: string signature -> 4-byte signature - Constructor abi.Method // Contract constructor for deploy parametrization - Calls map[string]*tmplMethod // Contract calls that only read state data - Transacts map[string]*tmplMethod // Contract calls that write state data - Fallback *tmplMethod // Additional special fallback function - Receive *tmplMethod // Additional special receive function - Events map[string]*tmplEvent // Contract events accessors - Libraries map[string]string // Same as tmplData, but filtered to only keep what the contract needs - Library bool // Indicator whether the contract is a library + Type string // Type name of the main contract binding + InputABI string // JSON ABI used as the input to generate the binding from + InputBin string // Optional EVM bytecode used to generate deploy code from + FuncSigs map[string]string // Optional map: string signature -> 4-byte signature + Constructor abi.Method // Contract constructor for deploy parametrization + Calls map[string]*tmplMethod // Contract calls that only read state data + Transacts map[string]*tmplMethod // Contract calls that write state data + Fallback *tmplMethod // Additional special fallback function + Receive *tmplMethod // Additional special receive function + Events map[string]*tmplEvent // Contract events accessors + Libraries map[string]string // Same as tmplData, but filtered to only keep direct deps that the contract needs + AllLibraries map[string]string // same as Libraries, but all direct/indirect library dependencies + Library bool // Indicator whether the contract is a library } // tmplMethod is a wrapper around an abi.Method that contains a few preprocessed diff --git a/accounts/abi/bind/template2.go b/accounts/abi/bind/template2.go index 85f83605d9..9c1396ba69 100644 --- a/accounts/abi/bind/template2.go +++ b/accounts/abi/bind/template2.go @@ -38,6 +38,12 @@ var ( {{end}} {{range $contract := .Contracts}} + var {{$contract.Type}}LibraryDeps = map[string]*bind.MetaData{ + {{range $pattern, $name := .Libraries}} + "{{$pattern}}": &{{$name}}MetaData, + {{end}} + } + // {{.Type}}MetaData contains all meta data concerning the {{.Type}} contract. var {{.Type}}MetaData = &bind.MetaData{ ABI: "{{.InputABI}}", @@ -52,20 +58,14 @@ var ( {{end}} } - // {{.Type}}Instance represents a deployed instance of the {{.Type}} contract. - type {{.Type}}Instance struct { - {{.Type}} - 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) *{{.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 @@ -86,7 +86,8 @@ var ( return _{{$contract.Type}}.deployCode } - func (_{{$contract.Type}} *{{$contract.Type}}) PackConstructor({{range .Constructor.Inputs}}, {{.Name}} {{bindtype .Type $structs}} {{end}}) ([]byte, error) { + // TODO: test constructor with inputs + func (_{{$contract.Type}} *{{$contract.Type}}) PackConstructor({{range .Constructor.Inputs}} {{.Name}} {{bindtype .Type $structs}} {{end}}) ([]byte, error) { return _{{$contract.Type}}.abi.Pack("" {{range .Constructor.Inputs}}, {{.Name}}{{end}}) } diff --git a/accounts/abi/bind/testdata/v2_testcase_library/contract.sol b/accounts/abi/bind/testdata/v2_testcase_library/contract.sol new file mode 100644 index 0000000000..399f5c41cd --- /dev/null +++ b/accounts/abi/bind/testdata/v2_testcase_library/contract.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +library RecursiveDep { + function AddOne(uint256 val) public pure returns (uint256 ret) { + return val + 1; + } +} + +// Array function to delete element at index and re-organize the array +// so that there are no gaps between the elements. +library Array { + using RecursiveDep for uint256; + + function remove(uint256[] storage arr, uint256 index) public { + // Move the last element into the place to delete + require(arr.length > 0, "Can't remove from empty array"); + arr[index] = arr[arr.length - 1]; + arr[index] = arr[index].AddOne(); + arr.pop(); + } +} + +contract TestArray { + using Array for uint256[]; + + uint256[] public arr; + + function testArrayRemove(uint256 value) public { + for (uint256 i = 0; i < 3; i++) { + arr.push(i); + } + + arr.remove(1); + + assert(arr.length == 2); + assert(arr[0] == 0); + assert(arr[1] == 2); + } + + constructor(uint256 foobar) { + + } +} diff --git a/accounts/abi/bind/v2/lib.go b/accounts/abi/bind/v2/lib.go index 9b350fb4a3..eb049b77b6 100644 --- a/accounts/abi/bind/v2/lib.go +++ b/accounts/abi/bind/v2/lib.go @@ -1,8 +1,6 @@ package v2 import ( - "context" - "errors" "fmt" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" @@ -10,12 +8,63 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/event" - "math/big" + "regexp" + "strings" ) -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) +type ContractInstance struct { + Address common.Address + Backend bind.ContractBackend +} + +func DeployContracts(auth *bind.TransactOpts, backend bind.ContractBackend, constructorInput []byte, contracts map[string]*bind.MetaData) { + // match if the contract has dynamic libraries that need to be linked + hasDepsMatcher, err := regexp.Compile("__\\$.*\\$__") + if err != nil { + panic(err) + } + + // deps we are linking + wipDeps := make(map[string]string) + for id, metadata := range contracts { + wipDeps[id] = metadata.Bin + } + + // nested iteration: find contracts without library dependencies first, + // deploy them, link them into any other contracts that depend on them. + // repeat this until there are no more contracts to link/deploy + for { + for id, contractBin := range wipDeps { + if !hasDepsMatcher.MatchString(contractBin) { + // this library/contract doesn't depend on any others + // it can be deployed as-is. + abi, err := contracts[id].GetAbi() + if err != nil { + panic(err) + } + addr, _, _, err := bind.DeployContractRaw(auth, *abi, []byte(contractBin), backend, constructorInput) + if err != nil { + panic(err) + } + delete(wipDeps, id) + + // embed the address of the deployed contract into any + // libraries/contracts that depend on it. + for id, contractBin := range wipDeps { + contractBin = strings.ReplaceAll(contractBin, fmt.Sprintf("__$%s%__", id), fmt.Sprintf("__$%s$__", addr.String())) + wipDeps[id] = contractBin + } + } + } + if len(wipDeps) == 0 { + break + } + } +} + +func FilterLogs[T any](instance *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 @@ -23,44 +72,10 @@ func FilterLogs[T any](instance bind.ContractInstance, opts *bind.FilterOpts, ev 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...) +func WatchLogs[T any](instance *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...) if err != nil { return nil, err } @@ -166,167 +181,14 @@ 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) return c.Transfer(opts) } + +func CallRaw(instance bind.ContractInstance, opts *bind.CallOpts, input []byte) ([]byte, error) { + backend := instance.Backend() + c := bind.NewBoundContract(instance.Address(), abi.ABI{}, backend, backend, backend) + return c.CallRaw(opts, input) +} diff --git a/accounts/abi/bind/v2/v2_test.go b/accounts/abi/bind/v2/v2_test.go index c232bd0cff..61b5ac1844 100644 --- a/accounts/abi/bind/v2/v2_test.go +++ b/accounts/abi/bind/v2/v2_test.go @@ -75,28 +75,52 @@ func TestV2(t *testing.T) { Backend: backend, Client: backend.Client(), } - address, _, boundContract, err := bind.DeployContract(&opts, contractABI, common.Hex2Bytes(v2_generated_testcase.V2GeneratedTestcaseMetaData.Bin), &bindBackend) + address, tx, _, err := bind.DeployContract(&opts, contractABI, common.Hex2Bytes(v2_generated_testcase.V2GeneratedTestcaseMetaData.Bin), &bindBackend) if err != nil { t.Fatal(err) } + + _, err = bind.WaitDeployed(context.Background(), &bindBackend, tx) + if err != nil { + t.Fatalf("error deploying bound contract: %+v", 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) + //contractInstance := v2_generated_testcase.NewV2GeneratedTestcaseInstance(contract, address, bindBackend) + contractInstance := ContractInstance{ + Address: address, + Backend: bindBackend, + } + sinkCh := make(chan *v2_generated_testcase.V2GeneratedTestcaseStruct) // 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) { + unpackStruct := func(log *types.Log) (*v2_generated_testcase.V2GeneratedTestcaseStruct, error) { res, err := contract.UnpackStructEvent(log) - return *res, err + return res, err + } + watchOpts := bind.WatchOpts{ + Start: nil, + Context: context.Background(), } // 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) + sub, err := WatchLogs[v2_generated_testcase.V2GeneratedTestcaseStruct](&contractInstance, &watchOpts, v2_generated_testcase.V2GeneratedTestcaseStructEventID(), unpackStruct, sinkCh, nil) if err != nil { t.Fatal(err) } defer sub.Unsubscribe() // send a balance to our contract (contract must accept ether by default) } + +func TestDeployment(t *testing.T) { + DeployContracts +} + +/* test-cases that should be extracted from v1 tests + +* EventChecker + + */