-
Notifications
You must be signed in to change notification settings - Fork 20.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ABIGEN v2 #26782
base: master
Are you sure you want to change the base?
ABIGEN v2 #26782
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty cool! I don't like the *2
naming, but I guess Felix likes it, so I won't argue to hard against it. I'm going to do some more tests and then approve
Something is broken here:
With this contract it produces invalid code:
Produced code:
(out is never used) I think the issue is that you generate Unpacking functions for solidity functions that have no return value. |
You should probably do with fmt the same thing as with the other imports: |
Aha, we don't need an unpack method when there are no return params. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add some tests, and more documentation.
@s1na are you still actively working on this PR? It is of interest to our project so we would be willing to devote some time to helping getting it over the line. Our use case is: we are building a state channel client in Go. Currently, it manages a private key, and acts as a signer. It uses the One suggestion I would have is to maintain backward compatibility with the current version of |
Hi @geoknee, great to see your interest in this PR. I'm not actively working on this, although I plan to finish it at some point. Your help is appreciated. I want to first address
We really want to get v2 out instead of packing more features into v1. I saw you have already generated v2 bindings for your contract. It'd be already great to hear if things are working and you've had any friction points. The biggest outstanding point is to add a test suite for v2. |
Fair enough, we can use both side by side in our project without too much pain.
I did spot a couple of problems:
statechannels/go-nitro@a1e9dd7 I pushed a fix here s1na#10.
|
c044348
to
008028e
Compare
accounts/abi/bind/v2/v2_test.go
Outdated
"context" | ||
"encoding/json" | ||
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" | ||
"github.com/ethereum/go-ethereum/accounts/abi/bind/testdata/v2_generated_testcase" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Build fails on this import atm
@MariusVanDerWijden I have a few things I am trying to wrap with this. It's in a bit of a messy state, and I'd ask to hold off on review until I can get this finished (couple of hours). |
accounts/abi/bind/dep_tree.go
Outdated
err := deployer.linkAndDeploy(tr) | ||
res := deployer.result() | ||
accumRes.Accumulate(res) | ||
if err != nil { | ||
return accumRes, err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
err := deployer.linkAndDeploy(tr) | |
res := deployer.result() | |
accumRes.Accumulate(res) | |
if err != nil { | |
return accumRes, err | |
} | |
if err := deployer.linkAndDeploy(tr); err != nil { | |
return accumRes, err | |
} | |
accumRes.Accumulate(deployer.result()) |
Afaict, if it linkAndDeploy
exits with errors, there's nothing new to add to the accumulated result, so might aswell write it a bit more go-idiomatic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the call to linkAndDeploy
deploys multiple contracts and then fails at some point, we want the accumulated result to include whatever succeeded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I didn't see that there's a recursive linkAndDeploy
internally, which might add things and still return with error later.
accounts/abi/bind/dep_tree.go
Outdated
Addrs: make(map[string]common.Address), | ||
} | ||
for _, meta := range deployParams.contracts { | ||
unlinkedContracts[meta.Pattern] = meta.Bin[2:] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we fully in control of the inputs here? Do we know with 100% certainty that the meta.Bin
starts with 0x
-prefix?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that users are sourcing everything from the output of solidity abi compilation (and it does prefix the contract bin with 0x
). That's why I haven't yet added more defensive checks throughout this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the contract deployer can be empty in the ABI. I can see no reason why that would ever be a valid result of the solidity compilation process.
accounts/abi/bind/dep_tree.go
Outdated
for _, tr := range deps { | ||
deployer := newDepTreeDeployer(deploy) | ||
if deployParams.inputs != nil { | ||
deployer.input = map[string][]byte{tr.pattern: deployParams.inputs[tr.pattern]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it named tr
? It's definitely not obvious to a casual reader.
And what does pattern
signify, really? Please document or name it more appropriate. For me, it's a bit black magic what is happening here, with the setting of deployer.input
to a map, involving the pattern, if the deployParams.inputs
is non-nil.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tr
represents the root of a deployment tree here: it's a contract/library that isn't a dependency of any other contracts.
a contract's pattern is a legitimate solidity concept (reference): it's the "34 character prefix of the hex encoding of the keccak256 hash of the fully qualified library name". A unique identifier for each contract/library.
I think for clarity, it probably deserves its own type (even if just a typedef on string).
…ndency tree handling as a result of embedding it in the bindings.
accounts/abi/bind/dep_tree.go
Outdated
overrideAddrs map[string]common.Address | ||
deployedAddrs map[string]common.Address |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can drop overrideAddrs
, and shove everything into deployedAddrs
. The deployerTxs can be used if the user wants to know which happened now and which happened previously
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WDYT about filtering the overrides out of the DeploymentResult
at the end of LinkAndDeploy
based on their non-presence in the map of deployed txs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like that, yes.
Here are some suggested changes: holiman@4ad21ba
I wanted to try it out in code to see if it worked. Seems to, so far
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ as you can see in that commit, I don't see any need for Accumulate
to exist? It's accumulated in the deployer
return rootMetadatas | ||
} | ||
|
||
func __linkDeps(metadata MetaData, depMap map[string]*MetaData, roots *map[string]struct{}) MetaData { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's up with this name? Why such a strange prefix?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's the recursing part of linkDeps
. I didn't know what to call it. That code needs a bit of love (documentation/renaming where appropriate) but I'm trying to port the v1 bind tests over to v2 atm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that's now how we name things in go (the underscores). I'm sure you can figure out a more descriptive name
accounts/abi/bind/dep_tree.go
Outdated
// if this contract/library depends on other libraries deploy them (and their dependencies) first | ||
for _, dep := range metadata.Deps { | ||
if err := d.linkAndDeploy(dep); err != nil { | ||
return err | ||
} | ||
} | ||
// if we just deployed any prerequisite contracts, link their deployed addresses into the bytecode to produce | ||
// a deployer bytecode for this contract. | ||
deployerCode := metadata.Bin | ||
for _, dep := range metadata.Deps { | ||
linkAddr, ok := d.deployedAddrs[dep.Pattern] | ||
if !ok { | ||
linkAddr = d.overrideAddrs[dep.Pattern] | ||
} | ||
deployerCode = strings.ReplaceAll(deployerCode, "__$"+dep.Pattern+"$__", strings.ToLower(linkAddr.String()[2:])) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can combine these two steps, and also make it look nicer if you change the signature to return common.Address, error
:
// if this contract/library depends on other libraries deploy them (and their dependencies) first
deployerCode := metadata.Bin
for _, dep := range metadata.Deps {
addr, err := d.linkAndDeploy(dep)
if err != nil {
return common.Address{}, err
}
// link their deployed addresses into the bytecode to produce
deployerCode = strings.ReplaceAll(deployerCode, "__$"+dep.Pattern+"$__", strings.ToLower(addr.String()[2:]))
}
(still store the addr/tx as previous, though)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See second commit in 62a7806
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. I'm going to pull these in to the branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, I wouldn't mind if you squash things a bit, perhaps squash sequences of commits from one author into one commit.
accounts/abi/bind/dep_tree.go
Outdated
if deployParams.inputs != nil { | ||
deployer.input = map[string][]byte{contract.Pattern: deployParams.inputs[contract.Pattern]} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This clause is not covered by testing (commenting it out doesn't cause failures). And it's not surprising, because the clause doesn't make sense.
On each iteration of a contract
, it overwrites the input
-map with the current contract
input.
Why not just copy/clone the deployParams.inputs
map to the deployer
, already when creating the deployer
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Afaict you can turn it into
func LinkAndDeploy(deployParams *DeploymentParams, deploy DeployFn) (res *DeploymentResult, err error) {
deployer := newDepTreeDeployer(deployParams.overrides, deploy, deployParams.inputs)
for _, contract := range deployParams.contracts {
if _ err := deployer.linkAndDeploy(contract); err != nil {
return deployer.result(), err
}
}
return deployer.result(), nil
accounts/abi/bind/bindv2.go
Outdated
func (b *contractBinder) RegisterCallIdentifier(id string) (string, error) { | ||
return b.registerIdentifier(b.callIdentifiers, id) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WHy is this public accessor needed? Are users expected to use this? In what scenario?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's a public method on a private type. Will move it to lowercase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's a public method on a private type. Will move it to lowercase.
i.e. I didn't mean it for public consumption. a mistake on my part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, if it's not for public, is it even needed? doesn't save a whole lot of typing really
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not really needed no.
accounts/abi/bind/bind.go
Outdated
LangGo: abi.ToCamelCase, | ||
} | ||
// conform to Go naming conventions. | ||
var methodNormalizer = abi.ToCamelCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed at all (now that you removed the lang-dependent map). It's never actually changed, is it? So why not just use abi.ToCamelCase
and keep it simple?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with capitalise
below, really
normalized.Inputs = make([]abi.Argument, len(original.Inputs)) | ||
copy(normalized.Inputs, original.Inputs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
normalized.Inputs = make([]abi.Argument, len(original.Inputs)) | |
copy(normalized.Inputs, original.Inputs) | |
normalized.Inputs = slices.Clone(original.Inputs) |
And same for outputs.
…e subtest that failed.
accounts/abi/bind/bindv2.go
Outdated
|
||
// normalizeErrorOrEventFields normalizes errors/events for emitting through bindings: | ||
// Any anonymous fields are given generated names. | ||
func (cb *contractBinder) normalizeErrorOrEventFields(originalInputs abi.Arguments) abi.Arguments { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method does multiple things. Howver, it only uses cb
in order to do this:
if hasStruct(input.Type) {
cb.binder.BindStructType(input.Type)
}
Hence, would it be possible to change into
// normalizeErrorOrEventFields normalizes errors/events for emitting through bindings:
// Any anonymous fields are given generated names.
func (cb *contractBinder) normalizeErrorOrEventFields(originalInputs abi.Arguments) abi.Arguments {
args := normalizeArgs(originalInputs)
// Soup up the struct types
for _, input := range args {
if hasStruct(input.Type) {
cb.binder.BindStructType(input.Type)
}
}
return args
}
func normalizeArgs(originalInputs abi.Arguments) abi.Arguments {
...
?
Would make it more testable
accounts/abi/bind/bindv2.go
Outdated
if !used[capitalise(normalizedArguments[i].Name)] { | ||
used[capitalise(normalizedArguments[i].Name)] = true | ||
break | ||
} | ||
normalizedArguments[i].Name = fmt.Sprintf("%s%d", normalizedArguments[i].Name, index) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks slightly wrong to me. We check for capitalized version of the name, and then iterate on changing the name. But we don't actually capitalize the name. So the used-check uses the capitalized version, but non-capitalized is set as name.
It's a big confusing, and looks like a bug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it be something like this?
func normalizeArguments(args abi.Arguments) abi.Arguments {
args := slices.Clone(args)
used := make(map[string]bool)
for i, input := range args {
if input.Name == "" || isKeyWord(input.Name) {
args[i].Name = fmt.Sprintf("arg%d", i)
}
name := capitalise(args[i].Name)
for index := 0; ; index++ {
if !used[name] {
used[name] = true
break
}
// try next
name = capitalise(fmt.Sprintf("%s%d", args[i].Name, index))
}
args[i].Name = name
}
return args
}
} | ||
|
||
// bindEvent normalizes an event and registers it to be emitted in the bindings. | ||
func (cb *contractBinder) bindEvent(original abi.Event) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move bindEvent
and bindError
so they are right below bindMethod
type tmplDataV2 struct { | ||
Package string // Name of the package to place the generated file in | ||
Contracts map[string]*tmplContractV2 // List of contracts to generate into this file | ||
Libraries map[string]string // Map of the contract's name to link pattern |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only thing that's different which warranted addition of templDataV2
.
…t fails before the fix, and also also a few more test cases for args normalization.
…t the backing-array provided by the API user from being mutated). re-enable deployment-with-overrides test
…the results of feeding v1 binding test ABIs through the v2 generator.
This PR adds a new version of abigen which will co-exist parallel to the existing version for a while. To generate v2 bindings provide the
--v2
flag toabigen
cmd.Summary
The main point of abigen v2 is having a lightweight generated binding which allows for more composability. This is possible now thanks to Go generics. "only" methods to pack and unpack call and return data are generated for a contract. As well as unpacking of logs.
To interact with the contract a library is available with functions such as
Call
,Transact
,FilterLogs
, etc. These take in the packed calldata, or a function pointer to unpack return data.Features
The new version is at feature-parity with v1 at a much lower generated binding size. The only missing feature as of now is sessions.
Example
V1 and v2 bindings for a sample contract are available here: https://gist.github.com/s1na/05f2d241b07372b41ba1747ce6e098b7. A sample script using v2 is available in
main.go
.