From 2e51d367c013760197da9744867672dfaf52c2bf Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Thu, 10 Mar 2022 14:00:59 +0100 Subject: [PATCH] Contract revamp (#167) * Update go.mod * Abigen improv * Improve contract * Update docs * Update changelog * Update changelog 2 * Update README --- CHANGELOG.md | 4 + README.md | 216 +-------- .../builtin => builtin}/ens/artifacts/ENS.abi | 0 .../builtin => builtin}/ens/artifacts/ENS.bin | 0 .../ens/artifacts/Resolver.abi | 0 .../ens/artifacts/Resolver.bin | 0 {contract/builtin => builtin}/ens/ens.go | 32 +- .../builtin => builtin}/ens/ens_artifacts.go | 0 .../builtin => builtin}/ens/ens_resolver.go | 7 +- .../ens/ens_resolver_test.go | 1 - {contract/builtin => builtin}/ens/resolver.go | 36 +- .../ens/resolver_artifacts.go | 0 {contract/builtin => builtin}/ens/utils.go | 0 .../builtin => builtin}/ens/utils_test.go | 0 .../erc20/artifacts/ERC20.abi | 0 {contract/builtin => builtin}/erc20/erc20.go | 22 +- .../erc20/erc20_artifacts.go | 0 .../builtin => builtin}/erc20/erc20_test.go | 10 +- cmd/abigen/gen.go | 18 +- cmd/abigen/testdata/testdata.go | 14 +- cmd/go.mod | 25 + cmd/go.sum | 7 - cmd/version/version.go | 4 +- contract/contract.go | 447 +++++++++--------- contract/contract_test.go | 87 +++- contract/provider.go | 5 - ens/ens.go | 2 +- examples/contract-call-basic.go | 39 ++ examples/contract-call-from.go | 36 ++ examples/contract-deploy.go | 27 ++ examples/contract-transaction.go | 46 ++ scripts/build-abigen.sh | 67 --- scripts/build-artifacts.sh | 8 +- structs.go | 10 + wallet/signer.go | 4 +- website/components/primitives.jsx | 5 + website/pages/contract.mdx | 83 ++++ website/pages/integrations/ens.mdx | 2 +- website/pages/meta.json | 1 + 39 files changed, 655 insertions(+), 610 deletions(-) rename {contract/builtin => builtin}/ens/artifacts/ENS.abi (100%) rename {contract/builtin => builtin}/ens/artifacts/ENS.bin (100%) rename {contract/builtin => builtin}/ens/artifacts/Resolver.abi (100%) rename {contract/builtin => builtin}/ens/artifacts/Resolver.bin (100%) rename {contract/builtin => builtin}/ens/ens.go (77%) rename {contract/builtin => builtin}/ens/ens_artifacts.go (100%) rename {contract/builtin => builtin}/ens/ens_resolver.go (68%) rename {contract/builtin => builtin}/ens/ens_resolver_test.go (94%) rename {contract/builtin => builtin}/ens/resolver.go (82%) rename {contract/builtin => builtin}/ens/resolver_artifacts.go (100%) rename {contract/builtin => builtin}/ens/utils.go (100%) rename {contract/builtin => builtin}/ens/utils_test.go (100%) rename {contract/builtin => builtin}/erc20/artifacts/ERC20.abi (100%) rename {contract/builtin => builtin}/erc20/erc20.go (86%) rename {contract/builtin => builtin}/erc20/erc20_artifacts.go (100%) rename {contract/builtin => builtin}/erc20/erc20_test.go (80%) delete mode 100644 contract/provider.go create mode 100644 examples/contract-call-basic.go create mode 100644 examples/contract-call-from.go create mode 100644 examples/contract-deploy.go create mode 100644 examples/contract-transaction.go delete mode 100755 scripts/build-abigen.sh create mode 100644 website/pages/contract.mdx diff --git a/CHANGELOG.md b/CHANGELOG.md index c7144217..042ebc5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ +# 0.1.1 (Not released) + +- Introduce `NodeProvider` and update `Contract` and `abigen` format. [[GH-167](https://github.com/umbracle/ethgo/issues/167)] + # 0.1.0 (5 March, 2022) - Initial public release. diff --git a/README.md b/README.md index b427557b..e5d7104a 100644 --- a/README.md +++ b/README.md @@ -1,218 +1,14 @@ # Eth-Go -## JsonRPC +Ethgo is a lightweight SDK in Go to interact with Ethereum compatible blockchains. -```golang -package main +- Website: https://ethgo.dev -import ( - "fmt" - - "github.com/umbracle/ethgo" - "github.com/umbracle/ethgo/jsonrpc" -) +Ethgo provides the next key features: -func main() { - client, err := jsonrpc.NewClient("https://mainnet.infura.io") - if err != nil { - panic(err) - } +- **Simple**: Light and with a small number of direct dependencies. - number, err := client.Eth().BlockNumber() - if err != nil { - panic(err) - } +- **Ethereum ecosystem**: Native integration with other tools from the ecosystem like `ens` and `etherscan`. - header, err := client.Eth().GetBlockByNumber(ethgo.BlockNumber(number), true) - if err != nil { - panic(err) - } - - fmt.Println(header) -} -``` - -## ABI - -The ABI codifier uses randomized tests with e2e integration tests with a real Geth client to ensure that the codification is correct and provides the same results as the AbiEncoder from Solidity. - -To use the library import: - -``` -"github.com/umbracle/ethgo/abi" -``` - -Declare basic objects: - -```golang -typ, err := abi.NewType("uint256") -``` - -or - -```golang -typ = abi.MustNewType("uint256") -``` - -and use it to encode/decode the data: - -```golang -num := big.NewInt(1) - -encoded, err := typ.Encode(num) -if err != nil { - panic(err) -} - -decoded, err := typ.Decode(encoded) // decoded as interface -if err != nil { - panic(err) -} - -num2 := decoded.(*big.Int) -fmt.Println(num.Cmp(num2) == 0) // num == num2 -``` - -You can also codify structs as Solidity tuples: - -```golang -import ( - "fmt" - - "github.com/umbracle/ethgo" - "github.com/umbracle/ethgo/abi" - "math/big" -) - -func main() { - typ := abi.MustNewType("tuple(address a, uint256 b)") - - type Obj struct { - A ethgo.Address - B *big.Int - } - obj := &Obj{ - A: ethgo.Address{0x1}, - B: big.NewInt(1), - } - - // Encode - encoded, err := typ.Encode(obj) - if err != nil { - panic(err) - } - - // Decode output into a map - res, err := typ.Decode(encoded) - if err != nil { - panic(err) - } - - // Decode into a struct - var obj2 Obj - if err := typ.DecodeStruct(encoded, &obj2); err != nil { - panic(err) - } - - fmt.Println(res) - fmt.Println(obj) -} -``` - -## Wallet - -As for now the library only provides primitive abstractions to send signed abstractions. The intended goal is to abstract the next steps inside the contract package. - -```golang -// Generate a random wallet -key, _ := wallet.GenerateKey() - -to := ethgo.Address{0x1} -transferVal := big.NewInt(1000) - -// Create the transaction -txn := ðgo.Transaction{ - To: &to, - Value: transferVal, - Gas: 100000, -} - -// Create the signer object and sign -signer := wallet.NewEIP155Signer(chainID) -txn, _ = signer.SignTx(txn, key) - -// Send the signed transaction -data := txn.MarshalRLP() -hash, _ := c.Eth().SendRawTransaction(data) -``` - -## ENS - -Resolve names on the Ethereum Name Service registrar. - -```golang -import ( - "fmt" - - "github.com/umbracle/ethgo/jsonrpc" - "github.com/umbracle/ethgo/ens" -) - -func main() { - client, err := jsonrpc.NewClient("https://mainnet.infura.io") - if err != nil { - panic(err) - } - - ens, err := ens.NewENS(ens.WithClient(client)) - if err != nil { - panic(err) - } - addr, err := ens.Resolve("ens_address") - if err != nil { - panic(err) - } - fmt.Println(addr) -} -``` - -## Block tracker - -```golang -import ( - "fmt" - - "github.com/umbracle/ethgo/jsonrpc" - "github.com/umbracle/ethgo/blocktracker" -) - -func main() { - client, err := jsonrpc.NewClient("https://mainnet.infura.io") - if err != nil { - panic(err) - } - - tracker = blocktracker.NewBlockTracker(client, WithBlockMaxBacklog(1000)) - if err := tracker.Init(); err != nil { - panic(err) - } - go tracker.Start() - - sub := tracker.Subscribe() - go func() { - for { - select { - case evnt := <-sub: - fmt.Println(evnt) - case <-ctx.Done(): - return - } - } - } -} -``` - -## Tracker - -Complete example of the tracker [here](./tracker/README.md) +- **Command-line-interface**: Ethgo is both a Golang SDK library and a CLI. diff --git a/contract/builtin/ens/artifacts/ENS.abi b/builtin/ens/artifacts/ENS.abi similarity index 100% rename from contract/builtin/ens/artifacts/ENS.abi rename to builtin/ens/artifacts/ENS.abi diff --git a/contract/builtin/ens/artifacts/ENS.bin b/builtin/ens/artifacts/ENS.bin similarity index 100% rename from contract/builtin/ens/artifacts/ENS.bin rename to builtin/ens/artifacts/ENS.bin diff --git a/contract/builtin/ens/artifacts/Resolver.abi b/builtin/ens/artifacts/Resolver.abi similarity index 100% rename from contract/builtin/ens/artifacts/Resolver.abi rename to builtin/ens/artifacts/Resolver.abi diff --git a/contract/builtin/ens/artifacts/Resolver.bin b/builtin/ens/artifacts/Resolver.bin similarity index 100% rename from contract/builtin/ens/artifacts/Resolver.bin rename to builtin/ens/artifacts/Resolver.bin diff --git a/contract/builtin/ens/ens.go b/builtin/ens/ens.go similarity index 77% rename from contract/builtin/ens/ens.go rename to builtin/ens/ens.go index 8f50aa10..b37d957f 100644 --- a/contract/builtin/ens/ens.go +++ b/builtin/ens/ens.go @@ -1,5 +1,5 @@ // Code generated by ethgo/abigen. DO NOT EDIT. -// Hash: c514a7355ba5a0b591945fc4b6594465c20a1a13b45a03a2d1779ee133d714a9 +// Hash: bfee2618a5908e1a24f19dcce873d3b8e797374138dd7604f7b593db3cca5c17 // Version: 0.1.0 package ens @@ -14,6 +14,7 @@ import ( var ( _ = big.NewInt + _ = jsonrpc.NewClient ) // ENS is a solidity contract @@ -22,18 +23,13 @@ type ENS struct { } // DeployENS deploys a new ENS contract -func DeployENS(provider *jsonrpc.Client, from ethgo.Address, args ...interface{}) *contract.Txn { - return contract.DeployContract(provider, from, abiENS, binENS, args...) +func DeployENS(provider *jsonrpc.Client, from ethgo.Address, args []interface{}, opts ...contract.ContractOption) (contract.Txn, error) { + return contract.DeployContract(abiENS, binENS, args, opts...) } // NewENS creates a new instance of the contract at a specific address -func NewENS(addr ethgo.Address, provider *jsonrpc.Client) *ENS { - return &ENS{c: contract.NewContract(addr, abiENS, provider)} -} - -// Contract returns the contract object -func (e *ENS) Contract() *contract.Contract { - return e.c +func NewENS(addr ethgo.Address, opts ...contract.ContractOption) *ENS { + return &ENS{c: contract.NewContract(addr, abiENS, opts...)} } // calls @@ -101,39 +97,39 @@ func (e *ENS) Ttl(node [32]byte, block ...ethgo.BlockNumber) (retval0 uint64, er // txns // SetOwner sends a setOwner transaction in the solidity contract -func (e *ENS) SetOwner(node [32]byte, owner ethgo.Address) *contract.Txn { +func (e *ENS) SetOwner(node [32]byte, owner ethgo.Address) (contract.Txn, error) { return e.c.Txn("setOwner", node, owner) } // SetResolver sends a setResolver transaction in the solidity contract -func (e *ENS) SetResolver(node [32]byte, resolver ethgo.Address) *contract.Txn { +func (e *ENS) SetResolver(node [32]byte, resolver ethgo.Address) (contract.Txn, error) { return e.c.Txn("setResolver", node, resolver) } // SetSubnodeOwner sends a setSubnodeOwner transaction in the solidity contract -func (e *ENS) SetSubnodeOwner(node [32]byte, label [32]byte, owner ethgo.Address) *contract.Txn { +func (e *ENS) SetSubnodeOwner(node [32]byte, label [32]byte, owner ethgo.Address) (contract.Txn, error) { return e.c.Txn("setSubnodeOwner", node, label, owner) } // SetTTL sends a setTTL transaction in the solidity contract -func (e *ENS) SetTTL(node [32]byte, ttl uint64) *contract.Txn { +func (e *ENS) SetTTL(node [32]byte, ttl uint64) (contract.Txn, error) { return e.c.Txn("setTTL", node, ttl) } // events func (e *ENS) NewOwnerEventSig() ethgo.Hash { - return e.c.ABI().Events["NewOwner"].ID() + return e.c.GetABI().Events["NewOwner"].ID() } func (e *ENS) NewResolverEventSig() ethgo.Hash { - return e.c.ABI().Events["NewResolver"].ID() + return e.c.GetABI().Events["NewResolver"].ID() } func (e *ENS) NewTTLEventSig() ethgo.Hash { - return e.c.ABI().Events["NewTTL"].ID() + return e.c.GetABI().Events["NewTTL"].ID() } func (e *ENS) TransferEventSig() ethgo.Hash { - return e.c.ABI().Events["Transfer"].ID() + return e.c.GetABI().Events["Transfer"].ID() } diff --git a/contract/builtin/ens/ens_artifacts.go b/builtin/ens/ens_artifacts.go similarity index 100% rename from contract/builtin/ens/ens_artifacts.go rename to builtin/ens/ens_artifacts.go diff --git a/contract/builtin/ens/ens_resolver.go b/builtin/ens/ens_resolver.go similarity index 68% rename from contract/builtin/ens/ens_resolver.go rename to builtin/ens/ens_resolver.go index 1be96554..85ef3beb 100644 --- a/contract/builtin/ens/ens_resolver.go +++ b/builtin/ens/ens_resolver.go @@ -2,16 +2,17 @@ package ens import ( "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/contract" "github.com/umbracle/ethgo/jsonrpc" ) type ENSResolver struct { e *ENS - provider *jsonrpc.Client + provider *jsonrpc.Eth } func NewENSResolver(addr ethgo.Address, provider *jsonrpc.Client) *ENSResolver { - return &ENSResolver{NewENS(addr, provider), provider} + return &ENSResolver{NewENS(addr, contract.WithJsonRPC(provider.Eth())), provider.Eth()} } func (e *ENSResolver) Resolve(addr string, block ...ethgo.BlockNumber) (res ethgo.Address, err error) { @@ -21,7 +22,7 @@ func (e *ENSResolver) Resolve(addr string, block ...ethgo.BlockNumber) (res ethg return } - resolver := NewResolver(resolverAddr, e.provider) + resolver := NewResolver(resolverAddr, contract.WithJsonRPC(e.provider)) res, err = resolver.Addr(addrHash, block...) return } diff --git a/contract/builtin/ens/ens_resolver_test.go b/builtin/ens/ens_resolver_test.go similarity index 94% rename from contract/builtin/ens/ens_resolver_test.go rename to builtin/ens/ens_resolver_test.go index 74c2e05d..2f69fd9f 100644 --- a/contract/builtin/ens/ens_resolver_test.go +++ b/builtin/ens/ens_resolver_test.go @@ -11,7 +11,6 @@ import ( ) var ( - url = "https://mainnet.infura.io" mainnetAddr = ethgo.HexToAddress("0x314159265dD8dbb310642f98f50C066173C1259b") ) diff --git a/contract/builtin/ens/resolver.go b/builtin/ens/resolver.go similarity index 82% rename from contract/builtin/ens/resolver.go rename to builtin/ens/resolver.go index a4d6b841..563fb591 100644 --- a/contract/builtin/ens/resolver.go +++ b/builtin/ens/resolver.go @@ -1,5 +1,5 @@ // Code generated by ethgo/abigen. DO NOT EDIT. -// Hash: 0c04761c33a894f2ef98ca34c1107d52c1e79c1913d2e632ceec744a37453c33 +// Hash: 3d1ecdf4aa6a2c578e0c3bbb14cc28ae2c8ebc4495f7d6128959f961afd0f635 // Version: 0.1.0 package ens @@ -14,6 +14,7 @@ import ( var ( _ = big.NewInt + _ = jsonrpc.NewClient ) // Resolver is a solidity contract @@ -22,18 +23,13 @@ type Resolver struct { } // DeployResolver deploys a new Resolver contract -func DeployResolver(provider *jsonrpc.Client, from ethgo.Address, args ...interface{}) *contract.Txn { - return contract.DeployContract(provider, from, abiResolver, binResolver, args...) +func DeployResolver(provider *jsonrpc.Client, from ethgo.Address, args []interface{}, opts ...contract.ContractOption) (contract.Txn, error) { + return contract.DeployContract(abiResolver, binResolver, args, opts...) } // NewResolver creates a new instance of the contract at a specific address -func NewResolver(addr ethgo.Address, provider *jsonrpc.Client) *Resolver { - return &Resolver{c: contract.NewContract(addr, abiResolver, provider)} -} - -// Contract returns the contract object -func (r *Resolver) Contract() *contract.Contract { - return r.c +func NewResolver(addr ethgo.Address, opts ...contract.ContractOption) *Resolver { + return &Resolver{c: contract.NewContract(addr, abiResolver, opts...)} } // calls @@ -171,48 +167,48 @@ func (r *Resolver) SupportsInterface(interfaceID [4]byte, block ...ethgo.BlockNu // txns // SetABI sends a setABI transaction in the solidity contract -func (r *Resolver) SetABI(node [32]byte, contentType *big.Int, data []byte) *contract.Txn { +func (r *Resolver) SetABI(node [32]byte, contentType *big.Int, data []byte) (contract.Txn, error) { return r.c.Txn("setABI", node, contentType, data) } // SetAddr sends a setAddr transaction in the solidity contract -func (r *Resolver) SetAddr(node [32]byte, addr ethgo.Address) *contract.Txn { +func (r *Resolver) SetAddr(node [32]byte, addr ethgo.Address) (contract.Txn, error) { return r.c.Txn("setAddr", node, addr) } // SetContent sends a setContent transaction in the solidity contract -func (r *Resolver) SetContent(node [32]byte, hash [32]byte) *contract.Txn { +func (r *Resolver) SetContent(node [32]byte, hash [32]byte) (contract.Txn, error) { return r.c.Txn("setContent", node, hash) } // SetName sends a setName transaction in the solidity contract -func (r *Resolver) SetName(node [32]byte, name string) *contract.Txn { +func (r *Resolver) SetName(node [32]byte, name string) (contract.Txn, error) { return r.c.Txn("setName", node, name) } // SetPubkey sends a setPubkey transaction in the solidity contract -func (r *Resolver) SetPubkey(node [32]byte, x [32]byte, y [32]byte) *contract.Txn { +func (r *Resolver) SetPubkey(node [32]byte, x [32]byte, y [32]byte) (contract.Txn, error) { return r.c.Txn("setPubkey", node, x, y) } // events func (r *Resolver) ABIChangedEventSig() ethgo.Hash { - return r.c.ABI().Events["ABIChanged"].ID() + return r.c.GetABI().Events["ABIChanged"].ID() } func (r *Resolver) AddrChangedEventSig() ethgo.Hash { - return r.c.ABI().Events["AddrChanged"].ID() + return r.c.GetABI().Events["AddrChanged"].ID() } func (r *Resolver) ContentChangedEventSig() ethgo.Hash { - return r.c.ABI().Events["ContentChanged"].ID() + return r.c.GetABI().Events["ContentChanged"].ID() } func (r *Resolver) NameChangedEventSig() ethgo.Hash { - return r.c.ABI().Events["NameChanged"].ID() + return r.c.GetABI().Events["NameChanged"].ID() } func (r *Resolver) PubkeyChangedEventSig() ethgo.Hash { - return r.c.ABI().Events["PubkeyChanged"].ID() + return r.c.GetABI().Events["PubkeyChanged"].ID() } diff --git a/contract/builtin/ens/resolver_artifacts.go b/builtin/ens/resolver_artifacts.go similarity index 100% rename from contract/builtin/ens/resolver_artifacts.go rename to builtin/ens/resolver_artifacts.go diff --git a/contract/builtin/ens/utils.go b/builtin/ens/utils.go similarity index 100% rename from contract/builtin/ens/utils.go rename to builtin/ens/utils.go diff --git a/contract/builtin/ens/utils_test.go b/builtin/ens/utils_test.go similarity index 100% rename from contract/builtin/ens/utils_test.go rename to builtin/ens/utils_test.go diff --git a/contract/builtin/erc20/artifacts/ERC20.abi b/builtin/erc20/artifacts/ERC20.abi similarity index 100% rename from contract/builtin/erc20/artifacts/ERC20.abi rename to builtin/erc20/artifacts/ERC20.abi diff --git a/contract/builtin/erc20/erc20.go b/builtin/erc20/erc20.go similarity index 86% rename from contract/builtin/erc20/erc20.go rename to builtin/erc20/erc20.go index d60b6fe1..ab3ee9ca 100644 --- a/contract/builtin/erc20/erc20.go +++ b/builtin/erc20/erc20.go @@ -1,5 +1,5 @@ // Code generated by ethgo/abigen. DO NOT EDIT. -// Hash: 2f4ca06adf3ede4e375fbb39f8806c16a90078c56d3b7703325b97d8d194729b +// Hash: a1a873d70d345feef023ee086fd6135b24d775444b950ee9d5ea411e72b0f373 // Version: 0.1.0 package erc20 @@ -14,6 +14,7 @@ import ( var ( _ = big.NewInt + _ = jsonrpc.NewClient ) // ERC20 is a solidity contract @@ -22,13 +23,8 @@ type ERC20 struct { } // NewERC20 creates a new instance of the contract at a specific address -func NewERC20(addr ethgo.Address, provider *jsonrpc.Client) *ERC20 { - return &ERC20{c: contract.NewContract(addr, abiERC20, provider)} -} - -// Contract returns the contract object -func (e *ERC20) Contract() *contract.Contract { - return e.c +func NewERC20(addr ethgo.Address, opts ...contract.ContractOption) *ERC20 { + return &ERC20{c: contract.NewContract(addr, abiERC20, opts...)} } // calls @@ -156,26 +152,26 @@ func (e *ERC20) TotalSupply(block ...ethgo.BlockNumber) (retval0 *big.Int, err e // txns // Approve sends a approve transaction in the solidity contract -func (e *ERC20) Approve(spender ethgo.Address, value *big.Int) *contract.Txn { +func (e *ERC20) Approve(spender ethgo.Address, value *big.Int) (contract.Txn, error) { return e.c.Txn("approve", spender, value) } // Transfer sends a transfer transaction in the solidity contract -func (e *ERC20) Transfer(to ethgo.Address, value *big.Int) *contract.Txn { +func (e *ERC20) Transfer(to ethgo.Address, value *big.Int) (contract.Txn, error) { return e.c.Txn("transfer", to, value) } // TransferFrom sends a transferFrom transaction in the solidity contract -func (e *ERC20) TransferFrom(from ethgo.Address, to ethgo.Address, value *big.Int) *contract.Txn { +func (e *ERC20) TransferFrom(from ethgo.Address, to ethgo.Address, value *big.Int) (contract.Txn, error) { return e.c.Txn("transferFrom", from, to, value) } // events func (e *ERC20) ApprovalEventSig() ethgo.Hash { - return e.c.ABI().Events["Approval"].ID() + return e.c.GetABI().Events["Approval"].ID() } func (e *ERC20) TransferEventSig() ethgo.Hash { - return e.c.ABI().Events["Transfer"].ID() + return e.c.GetABI().Events["Transfer"].ID() } diff --git a/contract/builtin/erc20/erc20_artifacts.go b/builtin/erc20/erc20_artifacts.go similarity index 100% rename from contract/builtin/erc20/erc20_artifacts.go rename to builtin/erc20/erc20_artifacts.go diff --git a/contract/builtin/erc20/erc20_test.go b/builtin/erc20/erc20_test.go similarity index 80% rename from contract/builtin/erc20/erc20_test.go rename to builtin/erc20/erc20_test.go index 0b1ea647..72f683aa 100644 --- a/contract/builtin/erc20/erc20_test.go +++ b/builtin/erc20/erc20_test.go @@ -5,18 +5,18 @@ import ( "github.com/stretchr/testify/assert" "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/contract" "github.com/umbracle/ethgo/jsonrpc" "github.com/umbracle/ethgo/testutil" ) var ( - url = "https://mainnet.infura.io" zeroX = ethgo.HexToAddress("0xe41d2489571d322189246dafa5ebde1f4699f498") ) func TestERC20Decimals(t *testing.T) { c, _ := jsonrpc.NewClient(testutil.TestInfuraEndpoint(t)) - erc20 := NewERC20(zeroX, c) + erc20 := NewERC20(zeroX, contract.WithJsonRPC(c.Eth())) decimals, err := erc20.Decimals() assert.NoError(t, err) @@ -27,7 +27,7 @@ func TestERC20Decimals(t *testing.T) { func TestERC20Name(t *testing.T) { c, _ := jsonrpc.NewClient(testutil.TestInfuraEndpoint(t)) - erc20 := NewERC20(zeroX, c) + erc20 := NewERC20(zeroX, contract.WithJsonRPC(c.Eth())) name, err := erc20.Name() assert.NoError(t, err) @@ -36,7 +36,7 @@ func TestERC20Name(t *testing.T) { func TestERC20Symbol(t *testing.T) { c, _ := jsonrpc.NewClient(testutil.TestInfuraEndpoint(t)) - erc20 := NewERC20(zeroX, c) + erc20 := NewERC20(zeroX, contract.WithJsonRPC(c.Eth())) symbol, err := erc20.Symbol() assert.NoError(t, err) @@ -45,7 +45,7 @@ func TestERC20Symbol(t *testing.T) { func TestTotalSupply(t *testing.T) { c, _ := jsonrpc.NewClient(testutil.TestInfuraEndpoint(t)) - erc20 := NewERC20(zeroX, c) + erc20 := NewERC20(zeroX, contract.WithJsonRPC(c.Eth())) supply, err := erc20.TotalSupply() assert.NoError(t, err) diff --git a/cmd/abigen/gen.go b/cmd/abigen/gen.go index 8a351135..ba63823d 100644 --- a/cmd/abigen/gen.go +++ b/cmd/abigen/gen.go @@ -189,6 +189,7 @@ import ( var ( _ = big.NewInt + _ = jsonrpc.NewClient ) // {{.Name}} is a solidity contract @@ -197,18 +198,13 @@ type {{.Name}} struct { } {{if .Contract.Bin}} // Deploy{{.Name}} deploys a new {{.Name}} contract -func Deploy{{.Name}}(provider *jsonrpc.Client, from ethgo.Address, args ...interface{}) *contract.Txn { - return contract.DeployContract(provider, from, abi{{.Name}}, bin{{.Name}}, args...) +func Deploy{{.Name}}(provider *jsonrpc.Client, from ethgo.Address, args []interface{}, opts ...contract.ContractOption) (contract.Txn, error) { + return contract.DeployContract(abi{{.Name}}, bin{{.Name}}, args, opts...) } {{end}} // New{{.Name}} creates a new instance of the contract at a specific address -func New{{.Name}}(addr ethgo.Address, provider *jsonrpc.Client) *{{.Name}} { - return &{{.Name}}{c: contract.NewContract(addr, abi{{.Name}}, provider)} -} - -// Contract returns the contract object -func ({{.Ptr}} *{{.Name}}) Contract() *contract.Contract { - return {{.Ptr}}.c +func New{{.Name}}(addr ethgo.Address, opts ...contract.ContractOption) *{{.Name}} { + return &{{.Name}}{c: contract.NewContract(addr, abi{{.Name}}, opts...)} } // calls @@ -236,14 +232,14 @@ func ({{$.Ptr}} *{{$.Name}}) {{funcName $key}}({{range $index, $val := tupleElem // txns {{range $key, $value := .Abi.Methods}}{{if not .Const}} // {{funcName $key}} sends a {{$key}} transaction in the solidity contract -func ({{$.Ptr}} *{{$.Name}}) {{funcName $key}}({{range $index, $input := tupleElems .Inputs}}{{if $index}}, {{end}}{{clean .Name}} {{arg .}}{{end}}) *contract.Txn { +func ({{$.Ptr}} *{{$.Name}}) {{funcName $key}}({{range $index, $input := tupleElems .Inputs}}{{if $index}}, {{end}}{{clean .Name}} {{arg .}}{{end}}) (contract.Txn, error) { return {{$.Ptr}}.c.Txn("{{$key}}"{{range $index, $elem := tupleElems .Inputs}}, {{clean $elem.Name}}{{end}}) } {{end}}{{end}} // events {{range $key, $value := .Abi.Events}} func ({{$.Ptr}} *{{$.Name}}) {{funcName $key}}EventSig() ethgo.Hash { - return {{$.Ptr}}.c.ABI().Events["{{funcName $key}}"].ID() + return {{$.Ptr}}.c.GetABI().Events["{{funcName $key}}"].ID() } {{end}}` diff --git a/cmd/abigen/testdata/testdata.go b/cmd/abigen/testdata/testdata.go index 684fa4b8..7ab92bfc 100644 --- a/cmd/abigen/testdata/testdata.go +++ b/cmd/abigen/testdata/testdata.go @@ -14,6 +14,7 @@ import ( var ( _ = big.NewInt + _ = jsonrpc.NewClient ) // Testdata is a solidity contract @@ -22,13 +23,8 @@ type Testdata struct { } // NewTestdata creates a new instance of the contract at a specific address -func NewTestdata(addr ethgo.Address, provider *jsonrpc.Client) *Testdata { - return &Testdata{c: contract.NewContract(addr, abiTestdata, provider)} -} - -// Contract returns the contract object -func (t *Testdata) Contract() *contract.Contract { - return t.c +func NewTestdata(addr ethgo.Address, opts ...contract.ContractOption) *Testdata { + return &Testdata{c: contract.NewContract(addr, abiTestdata, opts...)} } // calls @@ -61,12 +57,12 @@ func (t *Testdata) CallBasicInput(block ...ethgo.BlockNumber) (retval0 *big.Int, // txns // TxnBasicInput sends a txnBasicInput transaction in the solidity contract -func (t *Testdata) TxnBasicInput(val1 ethgo.Address, val2 *big.Int) *contract.Txn { +func (t *Testdata) TxnBasicInput(val1 ethgo.Address, val2 *big.Int) (contract.Txn, error) { return t.c.Txn("txnBasicInput", val1, val2) } // events func (t *Testdata) EventBasicEventSig() ethgo.Hash { - return t.c.ABI().Events["EventBasic"].ID() + return t.c.GetABI().Events["EventBasic"].ID() } diff --git a/cmd/go.mod b/cmd/go.mod index 79a07e0f..1f3a0eef 100644 --- a/cmd/go.mod +++ b/cmd/go.mod @@ -9,4 +9,29 @@ require ( require github.com/spf13/pflag v1.0.5 +require ( + github.com/Masterminds/goutils v1.1.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible // indirect + github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/fatih/color v1.7.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.0.0 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/mattn/go-colorable v0.0.9 // indirect + github.com/mattn/go-isatty v0.0.3 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/posener/complete v1.1.1 // indirect + github.com/umbracle/fastrlp v0.0.0-20211229195328-c1416904ae17 // indirect + github.com/valyala/fastjson v1.4.1 // indirect + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect +) + replace github.com/umbracle/ethgo => ../ diff --git a/cmd/go.sum b/cmd/go.sum index 1b2d5fba..84c60292 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -50,14 +50,12 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI= github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -80,13 +78,10 @@ github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM52 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -176,7 +171,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= @@ -187,5 +181,4 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/cmd/version/version.go b/cmd/version/version.go index fd456d73..4baa1729 100644 --- a/cmd/version/version.go +++ b/cmd/version/version.go @@ -7,10 +7,10 @@ var ( GitCommit string // Version is the main version at the moment. - Version = "0.1.0" + Version = "0.1.1" // VersionPrerelease is a marker for the version. - VersionPrerelease = "" + VersionPrerelease = "dev" ) // GetVersion returns a string representation of the version diff --git a/contract/contract.go b/contract/contract.go index 6b289753..bad361cf 100644 --- a/contract/contract.go +++ b/contract/contract.go @@ -8,298 +8,315 @@ import ( "github.com/umbracle/ethgo" "github.com/umbracle/ethgo/abi" "github.com/umbracle/ethgo/jsonrpc" + "github.com/umbracle/ethgo/wallet" ) -// Contract is an Ethereum contract -type Contract struct { - addr ethgo.Address - from *ethgo.Address - abi *abi.ABI - provider *jsonrpc.Client +// Provider handles the interactions with the Ethereum 1x node +type Provider interface { + Call(ethgo.Address, []byte, *CallOpts) ([]byte, error) + Txn(ethgo.Address, ethgo.Key, []byte, *TxnOpts) (Txn, error) } -// DeployContract deploys a contract -func DeployContract(provider *jsonrpc.Client, from ethgo.Address, abi *abi.ABI, bin []byte, args ...interface{}) *Txn { - return &Txn{ - from: from, - provider: provider, - method: abi.Constructor, - args: args, - bin: bin, - } +type jsonRPCNodeProvider struct { + client *jsonrpc.Eth } -// NewContract creates a new contract instance -func NewContract(addr ethgo.Address, abi *abi.ABI, provider *jsonrpc.Client) *Contract { - return &Contract{ - addr: addr, - abi: abi, - provider: provider, +func (j *jsonRPCNodeProvider) Call(addr ethgo.Address, input []byte, opts *CallOpts) ([]byte, error) { + msg := ðgo.CallMsg{ + To: &addr, + Data: input, } + if opts.From != ethgo.ZeroAddress { + msg.From = opts.From + } + rawStr, err := j.client.Call(msg, opts.Block) + if err != nil { + return nil, err + } + raw, err := hex.DecodeString(rawStr[2:]) + if err != nil { + return nil, err + } + return raw, nil } -// ABI returns the abi of the contract -func (c *Contract) ABI() *abi.ABI { - return c.abi -} - -// Addr returns the address of the contract -func (c *Contract) Addr() ethgo.Address { - return c.addr -} - -// SetFrom sets the origin of the calls -func (c *Contract) SetFrom(addr ethgo.Address) { - c.from = &addr -} +func (j *jsonRPCNodeProvider) Txn(addr ethgo.Address, key ethgo.Key, input []byte, opts *TxnOpts) (Txn, error) { + var err error -// EstimateGas estimates the gas for a contract call -func (c *Contract) EstimateGas(method string, args ...interface{}) (uint64, error) { - return c.Txn(method, args).EstimateGas() -} + from := key.Address() -// Call calls a method in the contract -func (c *Contract) Call(method string, block ethgo.BlockNumber, args ...interface{}) (map[string]interface{}, error) { - m := c.abi.GetMethod(method) - if m == nil { - return nil, fmt.Errorf("method %s not found", method) + // estimate gas price + if opts.GasPrice == 0 { + opts.GasPrice, err = j.client.GasPrice() + if err != nil { + return nil, err + } + } + // estimate gas limit + if opts.GasLimit == 0 { + msg := ðgo.CallMsg{ + From: from, + To: nil, + Data: input, + Value: opts.Value, + GasPrice: opts.GasPrice, + } + if addr != ethgo.ZeroAddress { + msg.To = &addr + } + opts.GasLimit, err = j.client.EstimateGas(msg) + if err != nil { + return nil, err + } } - data, err := m.Encode(args) + chainID, err := j.client.ChainID() if err != nil { return nil, err } - // Call function - msg := ðgo.CallMsg{ - To: &c.addr, - Data: data, + // send transaction + rawTxn := ðgo.Transaction{ + From: from, + Input: input, + GasPrice: opts.GasPrice, + Gas: opts.GasLimit, + Value: opts.Value, } - if c.from != nil { - msg.From = *c.from + if addr != ethgo.ZeroAddress { + rawTxn.To = &addr } - rawStr, err := c.provider.Eth().Call(msg, block) + signer := wallet.NewEIP155Signer(chainID.Uint64()) + signedTxn, err := signer.SignTx(rawTxn, key) if err != nil { return nil, err } - - // Decode output - raw, err := hex.DecodeString(rawStr[2:]) + txnRaw, err := signedTxn.MarshalRLPTo(nil) if err != nil { return nil, err } - resp, err := m.Decode(raw) - if err != nil { - return nil, err - } - return resp, nil -} - -// Txn creates a new transaction object -func (c *Contract) Txn(method string, args ...interface{}) *Txn { - m, ok := c.abi.Methods[method] - if !ok { - // TODO, return error - panic(fmt.Errorf("method %s not found", method)) - } - return &Txn{ - from: *c.from, - addr: &c.addr, - provider: c.provider, - method: m, - args: args, + txn := &jsonrpcTransaction{ + txn: signedTxn, + txnRaw: txnRaw, + client: j.client, } + return txn, nil } -// Txn is a transaction object -type Txn struct { - from ethgo.Address - addr *ethgo.Address - provider *jsonrpc.Client - method *abi.Method - args []interface{} - data []byte - bin []byte - gasLimit uint64 - gasPrice uint64 - value *big.Int - hash ethgo.Hash - receipt *ethgo.Receipt +type jsonrpcTransaction struct { + hash ethgo.Hash + client *jsonrpc.Eth + txn *ethgo.Transaction + txnRaw []byte } -func (t *Txn) isContractDeployment() bool { - return t.bin != nil +func (j *jsonrpcTransaction) Hash() ethgo.Hash { + return j.hash } -// AddArgs is used to set the arguments of the transaction -func (t *Txn) AddArgs(args ...interface{}) *Txn { - t.args = args - return t +func (j *jsonrpcTransaction) EstimatedGas() uint64 { + return j.txn.Gas } -// SetValue sets the value for the txn -func (t *Txn) SetValue(v *big.Int) *Txn { - t.value = new(big.Int).Set(v) - return t +func (j *jsonrpcTransaction) GasPrice() uint64 { + return j.txn.GasPrice } -// EstimateGas estimates the gas for the call -func (t *Txn) EstimateGas() (uint64, error) { - if err := t.Validate(); err != nil { - return 0, err +func (j *jsonrpcTransaction) Do() error { + hash, err := j.client.SendRawTransaction(j.txnRaw) + if err != nil { + return err } - return t.estimateGas() + j.hash = hash + return nil } -func (t *Txn) estimateGas() (uint64, error) { - if t.isContractDeployment() { - return t.provider.Eth().EstimateGasContract(t.data) +func (j *jsonrpcTransaction) Wait() (*ethgo.Receipt, error) { + if (j.hash == ethgo.Hash{}) { + panic("transaction not executed") } - msg := ðgo.CallMsg{ - From: t.from, - To: t.addr, - Data: t.data, - Value: t.value, + for { + receipt, err := j.client.GetTransactionReceipt(j.hash) + if err != nil { + if err.Error() != "not found" { + return nil, err + } + } + if receipt != nil { + return receipt, nil + } } - return t.provider.Eth().EstimateGas(msg) } -// DoAndWait is a blocking query that combines -// both Do and Wait functions -func (t *Txn) DoAndWait() error { - if err := t.Do(); err != nil { - return err - } - if err := t.Wait(); err != nil { - return err - } - return nil +// Txn is the transaction object returned +type Txn interface { + Hash() ethgo.Hash + EstimatedGas() uint64 + GasPrice() uint64 + Do() error + Wait() (*ethgo.Receipt, error) } -// Do sends the transaction to the network -func (t *Txn) Do() error { - err := t.Validate() - if err != nil { - return err - } +type Opts struct { + JsonRPCEndpoint string + JsonRPCClient *jsonrpc.Eth + Provider Provider + Sender ethgo.Key +} - // estimate gas price - if t.gasPrice == 0 { - t.gasPrice, err = t.provider.Eth().GasPrice() - if err != nil { - return err - } - } - // estimate gas limit - if t.gasLimit == 0 { - t.gasLimit, err = t.estimateGas() - if err != nil { - return err - } +type ContractOption func(*Opts) + +func WithJsonRPCEndpoint(endpoint string) ContractOption { + return func(o *Opts) { + o.JsonRPCEndpoint = endpoint } +} - // send transaction - txn := ðgo.Transaction{ - From: t.from, - Input: t.data, - GasPrice: t.gasPrice, - Gas: t.gasLimit, - Value: t.value, +func WithJsonRPC(client *jsonrpc.Eth) ContractOption { + return func(o *Opts) { + o.JsonRPCClient = client } - if t.addr != nil { - txn.To = t.addr +} + +func WithProvider(provider Provider) ContractOption { + return func(o *Opts) { + o.Provider = provider } - t.hash, err = t.provider.Eth().SendTransaction(txn) - if err != nil { - return err +} + +func WithSender(sender ethgo.Key) ContractOption { + return func(o *Opts) { + o.Sender = sender } - return nil } -// Validate validates the arguments of the transaction -func (t *Txn) Validate() error { - if t.data != nil { - // Already validated - return nil +func DeployContract(abi *abi.ABI, bin []byte, args []interface{}, opts ...ContractOption) (Txn, error) { + a := NewContract(ethgo.Address{}, abi, opts...) + a.bin = bin + return a.Txn("constructor", args...) +} + +func NewContract(addr ethgo.Address, abi *abi.ABI, opts ...ContractOption) *Contract { + opt := &Opts{ + JsonRPCEndpoint: "http://localhost:8545", } - if t.isContractDeployment() { - t.data = append(t.data, t.bin...) + for _, c := range opts { + c(opt) } - if t.method != nil { - data, err := abi.Encode(t.args, t.method.Inputs) - if err != nil { - return fmt.Errorf("failed to encode arguments: %v", err) - } - if !t.isContractDeployment() { - t.data = append(t.method.ID(), data...) - } else { - t.data = append(t.data, data...) - } + + var provider Provider + if opt.Provider != nil { + provider = opt.Provider + } else if opt.JsonRPCClient != nil { + provider = &jsonRPCNodeProvider{client: opt.JsonRPCClient} + } else { + client, _ := jsonrpc.NewClient(opt.JsonRPCEndpoint) + provider = &jsonRPCNodeProvider{client: client.Eth()} } - return nil + + a := &Contract{ + addr: addr, + abi: abi, + provider: provider, + key: opt.Sender, + } + + return a } -// SetGasPrice sets the gas price of the transaction -func (t *Txn) SetGasPrice(gasPrice uint64) *Txn { - t.gasPrice = gasPrice - return t +// Contract is a wrapper to make abi calls to contract with a state provider +type Contract struct { + addr ethgo.Address + abi *abi.ABI + bin []byte + provider Provider + key ethgo.Key } -// SetGasLimit sets the gas limit of the transaction -func (t *Txn) SetGasLimit(gasLimit uint64) *Txn { - t.gasLimit = gasLimit - return t +func (a *Contract) GetABI() *abi.ABI { + return a.abi } -// Wait waits till the transaction is mined -func (t *Txn) Wait() error { - if (t.hash == ethgo.Hash{}) { - panic("transaction not executed") +type TxnOpts struct { + Value *big.Int + GasPrice uint64 + GasLimit uint64 +} + +func (a *Contract) Txn(method string, args ...interface{}) (Txn, error) { + if a.key == nil { + return nil, fmt.Errorf("no key selected") } - var err error - for { - t.receipt, err = t.provider.Eth().GetTransactionReceipt(t.hash) + isContractDeployment := method == "constructor" + + var input []byte + if isContractDeployment { + input = append(input, a.bin...) + } + + var abiMethod *abi.Method + if isContractDeployment { + if a.abi.Constructor != nil { + abiMethod = a.abi.Constructor + } + } else { + if abiMethod = a.abi.GetMethod(method); abiMethod == nil { + return nil, fmt.Errorf("method %s not found", method) + } + } + if abiMethod != nil { + data, err := abi.Encode(args, abiMethod.Inputs) if err != nil { - if err.Error() != "not found" { - return err - } + return nil, fmt.Errorf("failed to encode arguments: %v", err) } - if t.receipt != nil { - break + if isContractDeployment { + input = append(input, data...) + } else { + input = append(abiMethod.ID(), data...) } } - return nil -} -// Receipt returns the receipt of the transaction after wait -func (t *Txn) Receipt() *ethgo.Receipt { - return t.receipt + txn, err := a.provider.Txn(a.addr, a.key, input, &TxnOpts{}) + if err != nil { + return nil, err + } + return txn, nil } -// Event is a solidity event -type Event struct { - event *abi.Event +type CallOpts struct { + Block ethgo.BlockNumber + From ethgo.Address } -// Encode encodes an event -func (e *Event) Encode() ethgo.Hash { - return e.event.ID() -} +func (a *Contract) Call(method string, block ethgo.BlockNumber, args ...interface{}) (map[string]interface{}, error) { + m := a.abi.GetMethod(method) + if m == nil { + return nil, fmt.Errorf("method %s not found", method) + } -// ParseLog parses a log -func (e *Event) ParseLog(log *ethgo.Log) (map[string]interface{}, error) { - return abi.ParseLog(e.event.Inputs, log) -} + data, err := m.Encode(args) + if err != nil { + return nil, err + } -// Event returns a specific event -func (c *Contract) Event(name string) (*Event, bool) { - event, ok := c.abi.Events[name] - if !ok { - return nil, false + opts := &CallOpts{ + Block: ethgo.Latest, + } + if a.key != nil { + opts.From = a.key.Address() } - return &Event{event}, true + rawOutput, err := a.provider.Call(a.addr, data, opts) + if err != nil { + return nil, err + } + + resp, err := m.Decode(rawOutput) + if err != nil { + return nil, err + } + return resp, nil } diff --git a/contract/contract_test.go b/contract/contract_test.go index aba0c098..6098857c 100644 --- a/contract/contract_test.go +++ b/contract/contract_test.go @@ -10,6 +10,7 @@ import ( "github.com/umbracle/ethgo/abi" "github.com/umbracle/ethgo/jsonrpc" "github.com/umbracle/ethgo/testutil" + "github.com/umbracle/ethgo/wallet" ) var ( @@ -17,7 +18,7 @@ var ( addr0B = ethgo.HexToAddress(addr0) ) -func TestContractNoInput(t *testing.T) { +func TestContract_NoInput(t *testing.T) { s := testutil.NewTestServer(t, nil) defer s.Close() @@ -30,7 +31,7 @@ func TestContractNoInput(t *testing.T) { assert.NoError(t, err) p, _ := jsonrpc.NewClient(s.HTTPAddr()) - c := NewContract(addr, abi0, p) + c := NewContract(addr, abi0, WithJsonRPC(p.Eth())) vals, err := c.Call("set", ethgo.Latest) assert.NoError(t, err) @@ -41,13 +42,13 @@ func TestContractNoInput(t *testing.T) { }) assert.NoError(t, err) - c1 := NewContract(addr, abi1, p) + c1 := NewContract(addr, abi1, WithJsonRPC(p.Eth())) vals, err = c1.Call("set", ethgo.Latest) assert.NoError(t, err) assert.Equal(t, vals["0"], big.NewInt(1)) } -func TestContractIO(t *testing.T) { +func TestContract_IO(t *testing.T) { s := testutil.NewTestServer(t, nil) defer s.Close() @@ -59,9 +60,7 @@ func TestContractIO(t *testing.T) { abi, err := abi.NewABI(contract.Abi) assert.NoError(t, err) - p, _ := jsonrpc.NewClient(s.HTTPAddr()) - c := NewContract(addr, abi, p) - c.SetFrom(s.Account(0)) + c := NewContract(addr, abi, WithJsonRPCEndpoint(s.HTTPAddr())) resp, err := c.Call("setA", ethgo.Latest, addr0B, 1000) assert.NoError(t, err) @@ -70,10 +69,38 @@ func TestContractIO(t *testing.T) { assert.Equal(t, resp["1"], big.NewInt(1000)) } -func TestDeployContract(t *testing.T) { +func TestContract_From(t *testing.T) { + s := testutil.NewTestServer(t, nil) + defer s.Close() + + cc := &testutil.Contract{} + cc.AddCallback(func() string { + return `function example() public view returns (address) { + return msg.sender; + }` + }) + + contract, addr := s.DeployContract(cc) + + abi, err := abi.NewABI(contract.Abi) + assert.NoError(t, err) + + from := ethgo.Address{0x1} + c := NewContract(addr, abi, WithSender(from), WithJsonRPCEndpoint(s.HTTPAddr())) + + resp, err := c.Call("example", ethgo.Latest) + assert.NoError(t, err) + assert.Equal(t, resp["0"], from) +} + +func TestContract_Deploy(t *testing.T) { s := testutil.NewTestServer(t, nil) defer s.Close() + // create an address and fund it + key, _ := wallet.GenerateKey() + s.Transfer(key.Address(), big.NewInt(1000000000000000000)) + p, _ := jsonrpc.NewClient(s.HTTPAddr()) cc := &testutil.Contract{} @@ -88,16 +115,14 @@ func TestDeployContract(t *testing.T) { bin, err := hex.DecodeString(artifact.Bin) assert.NoError(t, err) - txn := DeployContract(p, s.Account(0), abi, bin, ethgo.Address{0x1}, 1000) + txn, err := DeployContract(abi, bin, []interface{}{ethgo.Address{0x1}, 1000}, WithJsonRPC(p.Eth()), WithSender(key)) + assert.NoError(t, err) - if err := txn.Do(); err != nil { - t.Fatal(err) - } - if err := txn.Wait(); err != nil { - t.Fatal(err) - } + assert.NoError(t, txn.Do()) + receipt, err := txn.Wait() + assert.NoError(t, err) - i := NewContract(txn.Receipt().ContractAddress, abi, p) + i := NewContract(receipt.ContractAddress, abi, WithJsonRPC(p.Eth())) resp, err := i.Call("val_0", ethgo.Latest) assert.NoError(t, err) assert.Equal(t, resp["0"], ethgo.Address{0x1}) @@ -106,3 +131,33 @@ func TestDeployContract(t *testing.T) { assert.NoError(t, err) assert.Equal(t, resp["0"], big.NewInt(1000)) } + +func TestContract_Transaction(t *testing.T) { + s := testutil.NewTestServer(t, nil) + defer s.Close() + + // create an address and fund it + key, _ := wallet.GenerateKey() + s.Transfer(key.Address(), big.NewInt(1000000000000000000)) + + cc := &testutil.Contract{} + cc.AddEvent(testutil.NewEvent("A").Add("uint256", true)) + cc.EmitEvent("setA", "A", "1") + + artifact, addr := s.DeployContract(cc) + + abi, err := abi.NewABI(artifact.Abi) + assert.NoError(t, err) + + // create a transaction + i := NewContract(addr, abi, WithJsonRPCEndpoint(s.HTTPAddr()), WithSender(key)) + txn, err := i.Txn("setA") + assert.NoError(t, err) + + err = txn.Do() + assert.NoError(t, err) + + receipt, err := txn.Wait() + assert.NoError(t, err) + assert.Len(t, receipt.Logs, 1) +} diff --git a/contract/provider.go b/contract/provider.go deleted file mode 100644 index f0905ec8..00000000 --- a/contract/provider.go +++ /dev/null @@ -1,5 +0,0 @@ -package contract - -// NodeProvider handles the interactions with the Ethereum 1x node -type NodeProvider interface { -} diff --git a/ens/ens.go b/ens/ens.go index 9298dec5..78afa9e9 100644 --- a/ens/ens.go +++ b/ens/ens.go @@ -5,7 +5,7 @@ import ( "log" "github.com/umbracle/ethgo" - "github.com/umbracle/ethgo/contract/builtin/ens" + "github.com/umbracle/ethgo/builtin/ens" "github.com/umbracle/ethgo/jsonrpc" ) diff --git a/examples/contract-call-basic.go b/examples/contract-call-basic.go new file mode 100644 index 00000000..fcbfd8c4 --- /dev/null +++ b/examples/contract-call-basic.go @@ -0,0 +1,39 @@ +package examples + +import ( + "fmt" + "math/big" + + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/abi" + "github.com/umbracle/ethgo/contract" + "github.com/umbracle/ethgo/jsonrpc" +) + +func handleErr(err error) { + if err != nil { + panic(err) + } +} + +// call a contract +func contractCall() { + var functions = []string{ + "function totalSupply() view returns (uint256)", + } + + abiContract, err := abi.NewABIFromList(functions) + handleErr(err) + + // Matic token + addr := ethgo.HexToAddress("0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0") + + client, err := jsonrpc.NewClient("https://mainnet.infura.io") + handleErr(err) + + c := contract.NewContract(addr, abiContract, contract.WithJsonRPC(client.Eth())) + res, err := c.Call("totalSupply", ethgo.Latest) + handleErr(err) + + fmt.Printf("TotalSupply: %s", res["totalSupply"].(*big.Int)) +} diff --git a/examples/contract-call-from.go b/examples/contract-call-from.go new file mode 100644 index 00000000..201213e3 --- /dev/null +++ b/examples/contract-call-from.go @@ -0,0 +1,36 @@ +package examples + +import ( + "fmt" + "math/big" + + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/abi" + "github.com/umbracle/ethgo/contract" + "github.com/umbracle/ethgo/jsonrpc" +) + +// call a contract and use a custom `from` address +func contractCallFrom() { + var functions = []string{ + "function totalSupply() view returns (uint256)", + } + + abiContract, err := abi.NewABIFromList(functions) + handleErr(err) + + // Matic token + addr := ethgo.HexToAddress("0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0") + + client, err := jsonrpc.NewClient("https://mainnet.infura.io") + handleErr(err) + + // from address (msg.sender in solidity) + from := ethgo.Address{0x1} + + c := contract.NewContract(addr, abiContract, contract.WithSender(from), contract.WithJsonRPC(client.Eth())) + res, err := c.Call("totalSupply", ethgo.Latest) + handleErr(err) + + fmt.Printf("TotalSupply: %s", res["totalSupply"].(*big.Int)) +} diff --git a/examples/contract-deploy.go b/examples/contract-deploy.go new file mode 100644 index 00000000..c095a5f7 --- /dev/null +++ b/examples/contract-deploy.go @@ -0,0 +1,27 @@ +package examples + +import ( + "fmt" + + "github.com/umbracle/ethgo/abi" + "github.com/umbracle/ethgo/contract" +) + +func contractDeploy() { + abiContract, err := abi.NewABIFromList([]string{}) + handleErr(err) + + // bytecode of the contract + bin := []byte{} + + txn, err := contract.DeployContract(abiContract, bin, []interface{}{}) + handleErr(err) + + err = txn.Do() + handleErr(err) + + receipt, err := txn.Wait() + handleErr(err) + + fmt.Printf("Contract: %s", receipt.ContractAddress) +} diff --git a/examples/contract-transaction.go b/examples/contract-transaction.go new file mode 100644 index 00000000..6b9e518b --- /dev/null +++ b/examples/contract-transaction.go @@ -0,0 +1,46 @@ +package examples + +import ( + "fmt" + + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/abi" + "github.com/umbracle/ethgo/contract" + "github.com/umbracle/ethgo/jsonrpc" + "github.com/umbracle/ethgo/wallet" +) + +func contractTransaction() { + var functions = []string{ + "function transferFrom(address from, address to, uint256 value)", + } + + abiContract, err := abi.NewABIFromList(functions) + handleErr(err) + + // Matic token + addr := ethgo.HexToAddress("0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0") + + client, err := jsonrpc.NewClient("https://mainnet.infura.io") + handleErr(err) + + // wallet signer + key, err := wallet.GenerateKey() + handleErr(err) + + opts := []contract.ContractOption{ + contract.WithJsonRPC(client.Eth()), + contract.WithSender(key), + } + c := contract.NewContract(addr, abiContract, opts...) + txn, err := c.Txn("transferFrom", ethgo.Latest) + handleErr(err) + + err = txn.Do() + handleErr(err) + + receipt, err := txn.Wait() + handleErr(err) + + fmt.Printf("Transaction mined at: %s", receipt.TransactionHash) +} diff --git a/scripts/build-abigen.sh b/scripts/build-abigen.sh deleted file mode 100755 index aedff1d0..00000000 --- a/scripts/build-abigen.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# -# This script builds the application from source for multiple platforms. -# Based on the code from https://github.com/hashicorp/consul -set -e - -# Get the parent directory of where this script is. -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done -DIR="$( cd -P "$( dirname "$SOURCE" )/../abigen" && pwd )" - -# Change into that directory -cd "$DIR" - -# Determine the arch/os combos we're building for -XC_ARCH=${XC_ARCH:-"amd64"} -XC_OS=${XC_OS:-"linux darwin windows"} - -# Delete the old dir -echo "==> Removing old directory..." -rm -f bin/* -rm -rf pkg/* -mkdir -p bin/ - -# Build! -echo "==> Building..." -"`which gox`" \ - -os="${XC_OS}" \ - -arch="${XC_ARCH}" \ - -osarch="!darwin/arm !darwin/arm64" \ - -output "pkg/{{.OS}}_{{.Arch}}/ethgo" \ - -tags="${GOTAGS}" \ - . - -# Move all the compiled things to the $GOPATH/bin -GOPATH=${GOPATH:-$(go env GOPATH)} -case $(uname) in - CYGWIN*) - GOPATH="$(cygpath $GOPATH)" - ;; -esac -OLDIFS=$IFS -IFS=: MAIN_GOPATH=($GOPATH) -IFS=$OLDIFS - -# Copy our OS/Arch to the bin/ directory -DEV_PLATFORM="./pkg/$(go env GOOS)_$(go env GOARCH)" -for F in $(find ${DEV_PLATFORM} -mindepth 1 -maxdepth 1 -type f); do - cp ${F} bin/ - cp ${F} ${MAIN_GOPATH}/bin/ -done - -# Zip and copy to the dist dir -echo "==> Packaging..." -for PLATFORM in $(find ./pkg -mindepth 1 -maxdepth 1 -type d); do - OSARCH=$(basename ${PLATFORM}) - echo "--> ${OSARCH}" - - pushd $PLATFORM >/dev/null 2>&1 - zip ../${OSARCH}.zip ./* - popd >/dev/null 2>&1 -done - -# Done! -echo -echo "==> Results:" -ls -hl bin/ \ No newline at end of file diff --git a/scripts/build-artifacts.sh b/scripts/build-artifacts.sh index 515a235e..f8a48a55 100755 --- a/scripts/build-artifacts.sh +++ b/scripts/build-artifacts.sh @@ -5,13 +5,13 @@ cd cmd echo "--> Build ENS" -ENS_ARTIFACTS=../contract/builtin/ens/artifacts -go run main.go abigen --source ${ENS_ARTIFACTS}/ENS.abi,${ENS_ARTIFACTS}/Resolver.abi --output ../contract/builtin/ens --package ens +ENS_ARTIFACTS=../builtin/ens/artifacts +go run main.go abigen --source ${ENS_ARTIFACTS}/ENS.abi,${ENS_ARTIFACTS}/Resolver.abi --output ../builtin/ens --package ens echo "--> Build ERC20" -ERC20_ARTIFACTS=../contract/builtin/erc20/artifacts -go run main.go abigen --source ${ERC20_ARTIFACTS}/ERC20.abi --output ../contract/builtin/erc20 --package erc20 +ERC20_ARTIFACTS=../builtin/erc20/artifacts +go run main.go abigen --source ${ERC20_ARTIFACTS}/ERC20.abi --output ../builtin/erc20 --package erc20 echo "--> Build Testdata" go run main.go abigen --source ./abigen/testdata/testdata.abi --output ./abigen/testdata --package testdata diff --git a/structs.go b/structs.go index d0e5e8eb..09ef1e39 100644 --- a/structs.go +++ b/structs.go @@ -37,6 +37,16 @@ func BytesToAddress(b []byte) Address { return a } +// Address implements the ethgo.Key interface Address method. +func (a Address) Address() Address { + return a +} + +// Sign implements the ethgo.Key interface Sign method. +func (a Address) Sign(hash []byte) ([]byte, error) { + panic("an address cannot sign messages") +} + // UnmarshalText implements the unmarshal interface func (a *Address) UnmarshalText(b []byte) error { return unmarshalTextByte(a[:], b, 20) diff --git a/wallet/signer.go b/wallet/signer.go index 974ba4d1..323f21a7 100644 --- a/wallet/signer.go +++ b/wallet/signer.go @@ -12,7 +12,7 @@ type Signer interface { RecoverSender(tx *ethgo.Transaction) (ethgo.Address, error) // SignTx signs a transaction - SignTx(tx *ethgo.Transaction, key *Key) (*ethgo.Transaction, error) + SignTx(tx *ethgo.Transaction, key ethgo.Key) (*ethgo.Transaction, error) } type EIP1155Signer struct { @@ -40,7 +40,7 @@ func (e *EIP1155Signer) RecoverSender(tx *ethgo.Transaction) (ethgo.Address, err return addr, nil } -func (e *EIP1155Signer) SignTx(tx *ethgo.Transaction, key *Key) (*ethgo.Transaction, error) { +func (e *EIP1155Signer) SignTx(tx *ethgo.Transaction, key ethgo.Key) (*ethgo.Transaction, error) { hash := signHash(tx, e.chainID) sig, err := key.Sign(hash) diff --git a/website/components/primitives.jsx b/website/components/primitives.jsx index f41f2f08..46261656 100644 --- a/website/components/primitives.jsx +++ b/website/components/primitives.jsx @@ -18,6 +18,10 @@ function Blocktag() { return {'(BlockTag)'} } +function ABI() { + return {'(ABI)'} +} + function Transaction() { return {'(Transaction)'} } @@ -33,4 +37,5 @@ export { Blocktag, Transaction, Receipt, + ABI, } diff --git a/website/pages/contract.mdx b/website/pages/contract.mdx new file mode 100644 index 00000000..5e6c631d --- /dev/null +++ b/website/pages/contract.mdx @@ -0,0 +1,83 @@ + +import GoDocLink from '../components/godoc' +import {Address, ABI} from '../components/primitives' + +# Contract + +The Contract struct represents a deployed Solidity contract. + +To instantiate a `Contract` object use: + +```go +contract.NewContract(addr, abi) +``` + +with: + +- `addr`
: Address of the contract. +- `abi` : ABI of the contract. + +By default, it connects to the `https://localhost:8545` JsonRPC endpoint. + +## Options + +Besides `addr` and `abi`, you can use the option pattern to parametrize the contract, the available options are: + +- WithJsonRPCEndpoint: JsonRPC url of the endpoint to connect with. +- WithJsonRPCClient: [`JsonRPC`](/jsonrpc) object to make rpc calls. It takes preference over an address from `WithAddress`. +- WithSigner: [`Signer`](/signers/signer) object to send transactions or use a custom `from` address. +- WithProvider: Custom NodeProvider implementation to resolve calls and transactions. + +## Examples + +Check [examples](https://github.com/umbracle/ethgo/tree/master/examples) for a list of examples on how to interact with a smart contract. + +### Call a contract + +```go +package main + +import ( + "fmt" + "math/big" + + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/abi" + "github.com/umbracle/ethgo/contract" + "github.com/umbracle/ethgo/jsonrpc" +) + +func handleErr(err error) { + if err != nil { + panic(err) + } +} + +// call a contract +func main() { + var functions = []string{ + "function totalSupply() view returns (uint256)", + } + + abiContract, err := abi.NewABIFromList(functions) + handleErr(err) + + // Matic token + addr := ethgo.HexToAddress("0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0") + + client, err := jsonrpc.NewClient("https://mainnet.infura.io") + handleErr(err) + + c := contract.NewContract(addr, abiContract, contract.WithJsonRPC(client.Eth())) + res, err := c.Call("totalSupply", ethgo.Latest) + handleErr(err) + + fmt.Printf("TotalSupply: %s", res["totalSupply"].(*big.Int)) +} +``` + +## Abigen + +One small limitation of `Contract` is that works with `interface` objects since the input and outputs of a smart contract are arbitrary. As an alternative, you can use [Abigen](./cli/abigen) to generate Go bindings that wrap the `Contract` object and provide native and typed Go functions to interact with the contracts. + +By default, `ethgo` includes builtin `abigen` contracts for `ens` and `erc20` tokens. diff --git a/website/pages/integrations/ens.mdx b/website/pages/integrations/ens.mdx index 274b1492..091b62d0 100644 --- a/website/pages/integrations/ens.mdx +++ b/website/pages/integrations/ens.mdx @@ -6,7 +6,7 @@ import {Address, Hash, Blocktag, Block, Transaction, Receipt} from '../../compon The Ethereum Name Service ([ENS](https://ens.domains/)) is a distributed, open, and extensible naming system based on the Ethereum blockchain. -The ENS object on the `ens` package is a module that abstracts the interaction with the ENS registry. +The ENS object on the `ens` package is a module that abstracts the interaction with the ENS registry. ```go package main diff --git a/website/pages/meta.json b/website/pages/meta.json index a89ead85..ef077c0a 100644 --- a/website/pages/meta.json +++ b/website/pages/meta.json @@ -3,6 +3,7 @@ "jsonrpc": "JsonRPC", "abi": "Application Binary Interface", "signers": "Signers", + "contract": "Contract", "integrations": "Integrations", "cli": "Command Line Interface" } \ No newline at end of file