From 0d235f0894495e1cd5b5c120c37bb2c907a33a04 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:59:20 +0200 Subject: [PATCH 01/24] feat: add gno.land/pkg/gnoclient (Gno.land Go client) Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/README.md | 14 ++++++++++ gno.land/pkg/gnoclient/client.go | 36 ++++++++++++++++++++++++++ gno.land/pkg/gnoclient/client_test.go | 15 +++++++++++ gno.land/pkg/gnoclient/example_test.go | 15 +++++++++++ 4 files changed, 80 insertions(+) create mode 100644 gno.land/pkg/gnoclient/README.md create mode 100644 gno.land/pkg/gnoclient/client.go create mode 100644 gno.land/pkg/gnoclient/client_test.go create mode 100644 gno.land/pkg/gnoclient/example_test.go diff --git a/gno.land/pkg/gnoclient/README.md b/gno.land/pkg/gnoclient/README.md new file mode 100644 index 00000000000..322df2a2742 --- /dev/null +++ b/gno.land/pkg/gnoclient/README.md @@ -0,0 +1,14 @@ +# Gno.land Go Client + +This is a Go client library for interacting with the Gno.land RPC API. +The library provides a convenient way to make requests to the Gno.land API endpoints and process the responses. + +## Installation + +To use this library, you can add it to your Go project using `go get`: + + go get github.com/gnolang/gno/gno.land/pkg/gnoclient + +## Usage + +TODO diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go new file mode 100644 index 00000000000..4b3b9e7a4fb --- /dev/null +++ b/gno.land/pkg/gnoclient/client.go @@ -0,0 +1,36 @@ +package gnoclient + +// Client represents the Gno.land RPC API client. +type Client struct { + Remote string + ChainID string +} + +// TODO: port existing code, i.e. faucet? +// TODO: create right now a tm2 generic go client and a gnovm generic go client? +// TODO: Command: Call +// TODO: Command: Send +// TODO: Command: AddPkg +// TODO: Command: Query +// TODO: Command: Eval +// TODO: Command: Exec +// TODO: Command: Package +// TODO: Command: QFile +// TODO: examples and unit tests +// TODO: Mock +// TODO: alternative configuration (pass existing websocket?) +// TODO: minimal go.mod to make it light to import + +func (c *Client) ApplyDefaults() { + if c.Remote == "" { + c.Remote = "127.0.0.1:26657" + } + if c.ChainID == "" { + c.ChainID = "devnet" + } +} + +// Request performs an API request and returns the response body. +func (c *Client) Request(method, endpoint string, params map[string]interface{}) ([]byte, error) { + panic("not implemented") +} diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go new file mode 100644 index 00000000000..3aa2f41c111 --- /dev/null +++ b/gno.land/pkg/gnoclient/client_test.go @@ -0,0 +1,15 @@ +package gnoclient + +import ( + "testing" +) + +func TestClient_Request(t *testing.T) { + client := Client{ + Remote: "localhost:12345", + ChainID: "test", + } + _ = client + + // TODO: xxx +} diff --git a/gno.land/pkg/gnoclient/example_test.go b/gno.land/pkg/gnoclient/example_test.go new file mode 100644 index 00000000000..4de537fa4f3 --- /dev/null +++ b/gno.land/pkg/gnoclient/example_test.go @@ -0,0 +1,15 @@ +package gnoclient_test + +import ( + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/gnoclient" +) + +func Example() { + client := gnoclient.Client{} + _ = client + fmt.Println("Hello") + // Output: + // Hello +} From f4d7b73ed09c1206a70436f1d1817097144ed305 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:36:33 +0200 Subject: [PATCH 02/24] chore: wip Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/README.md | 7 ++++ gno.land/pkg/gnoclient/client.go | 39 ++++++++++++--------- gno.land/pkg/gnoclient/client_test.go | 4 +-- gno.land/pkg/gnoclient/example_test.go | 33 +++++++++++++++++- gno.land/pkg/gnoclient/networking.go | 42 +++++++++++++++++++++++ gno.land/pkg/gnoclient/networking_test.go | 1 + gno.land/pkg/gnoclient/signer.go | 35 +++++++++++++++++++ gno.land/pkg/gnoclient/signer_test.go | 1 + tm2/pkg/crypto/keys/keybase.go | 3 ++ 9 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 gno.land/pkg/gnoclient/networking.go create mode 100644 gno.land/pkg/gnoclient/networking_test.go create mode 100644 gno.land/pkg/gnoclient/signer.go create mode 100644 gno.land/pkg/gnoclient/signer_test.go diff --git a/gno.land/pkg/gnoclient/README.md b/gno.land/pkg/gnoclient/README.md index 322df2a2742..af3c851964b 100644 --- a/gno.land/pkg/gnoclient/README.md +++ b/gno.land/pkg/gnoclient/README.md @@ -9,6 +9,13 @@ To use this library, you can add it to your Go project using `go get`: go get github.com/gnolang/gno/gno.land/pkg/gnoclient +## Plan + +TODO: improve +* bootstrap the effort here for gno.land, then move the generic functions to tm2/gnovm/gnosdk later. +* start using this library in `gno.land/cmd/*` and external clients such as `gnoblog-client` or the discord community faucet bot. +* as soon as we've a strong generic client, let's try to use codegen to ease creating typesafe contract-specific clients. + ## Usage TODO diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index 4b3b9e7a4fb..2f7612dc1dc 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -1,9 +1,28 @@ package gnoclient -// Client represents the Gno.land RPC API client. +import ( + "errors" + + "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + type Client struct { - Remote string - ChainID string + Keybase keys.Keybase + Networking Networking +} + +func (c Client) validateSigner() error { + if c.Keybase == nil { + return errors.New("missing c.Keybase") + } + return nil +} + +func (c Client) validateRPCClient() error { + if c.Networking == nil { + return errors.New("missing c.Networking") + } + return nil } // TODO: port existing code, i.e. faucet? @@ -20,17 +39,3 @@ type Client struct { // TODO: Mock // TODO: alternative configuration (pass existing websocket?) // TODO: minimal go.mod to make it light to import - -func (c *Client) ApplyDefaults() { - if c.Remote == "" { - c.Remote = "127.0.0.1:26657" - } - if c.ChainID == "" { - c.ChainID = "devnet" - } -} - -// Request performs an API request and returns the response body. -func (c *Client) Request(method, endpoint string, params map[string]interface{}) ([]byte, error) { - panic("not implemented") -} diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go index 3aa2f41c111..17ec73a8fb4 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -6,8 +6,8 @@ import ( func TestClient_Request(t *testing.T) { client := Client{ - Remote: "localhost:12345", - ChainID: "test", + // Remote: "localhost:12345", + // ChainID: "test", } _ = client diff --git a/gno.land/pkg/gnoclient/example_test.go b/gno.land/pkg/gnoclient/example_test.go index 4de537fa4f3..2413140ce3e 100644 --- a/gno.land/pkg/gnoclient/example_test.go +++ b/gno.land/pkg/gnoclient/example_test.go @@ -4,9 +4,40 @@ import ( "fmt" "github.com/gnolang/gno/gno.land/pkg/gnoclient" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) -func Example() { +func Example_withDisk() { + home := "/path/to/dir" + account := "mykey" + passwd := "secure" + + kb, _ := keys.NewKeyBaseFromDir(home) + signer := gnoclient.Signer(kb, account, passwd) + client := gnoclient.Client{ + Signer: signer, + } + _ = client +} + +func Example_withInMemCrypto() { + // create inmem keybase from bip39 + mnemo := "index brass unknown lecture autumn provide royal shrimp elegant wink now zebra discover swarm act ill you bullet entire outdoor tilt usage gap multiply" + bip39Passphrase := "" + account := uint32(0) + index := uint32(0) + kb, _ := gnoclient.InmemKeybaseFromBip39(mnemo, bip39Passphrase, account, index) + client := gnoclient.Client{ + Keybase: kb, + } + _ = client + fmt.Println("Hello") + // Output: + // Hello +} + +func Example_readOnly() { + // read-only client, can only query. client := gnoclient.Client{} _ = client fmt.Println("Hello") diff --git a/gno.land/pkg/gnoclient/networking.go b/gno.land/pkg/gnoclient/networking.go new file mode 100644 index 00000000000..cc2e372d50e --- /dev/null +++ b/gno.land/pkg/gnoclient/networking.go @@ -0,0 +1,42 @@ +package gnoclient + +import "errors" + +// Networking ... +type Networking interface { + // write methods. + + // Broadcast sends an encoded and signed transaction message to the blockchain. + // The default implementation connects on a node's Websocket interface. + Broadcast(txbz []byte) error + + // TODO: asynchronous Broadcast + // Broadcast(txbz []byte) (ch <-Event, error) + + // read methods. + + Query() +} + +type WebsocketClient struct { + // Opts + Remote string // Remote RPC node +} + +var _ Networking = (*WebsocketClient)(nil) // should implement Networking + +func (wsc WebsocketClient) ValidateOpts() error { + if wsc.Remote == "" { + return errors.New("missing remote url") + } + + return nil +} + +func (wsc WebsocketClient) Broadcast(txbz []byte) error { + return errors.New("not implemented") +} + +func (wsc WebsocketClient) Query() { + +} diff --git a/gno.land/pkg/gnoclient/networking_test.go b/gno.land/pkg/gnoclient/networking_test.go new file mode 100644 index 00000000000..3b4cbb757ad --- /dev/null +++ b/gno.land/pkg/gnoclient/networking_test.go @@ -0,0 +1 @@ +package gnoclient diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go new file mode 100644 index 00000000000..f20b1c910b7 --- /dev/null +++ b/gno.land/pkg/gnoclient/signer.go @@ -0,0 +1,35 @@ +package gnoclient + +import "github.com/gnolang/gno/tm2/pkg/crypto/keys" + +// Signer ... +type Signer interface { + Sign() +} + +type SignerFromKeybase struct { + Keybase keys.Keybase + Account string // name or bech32 + Password string // encryption password +} + +var _ Signer = (*SignerFromKeybase)(nil) + +func SignerFromKeybase(kb keys.Keybase, nameOrBech32 string, passwd string) Signer { + +} + +// InmemSignerFromBip39 creates an inmemory keybase which loads a single "default" account. +// It is intended to be used in systems that cannot rely on filesystem to store the private keys. +// +// Warning: It's recommended to use keys.NewKeyBaseFromDir when possible. +func InmemSignerFromBip39(mnemo string, passphrase string, account uint32, index uint32) (keys.Keybase, error) { + kb := keys.NewInMemory() + name := "default" + passwd := "" // not needed in memory + _, err := kb.CreateAccount(name, mnemo, passphrase, passwd, account, index) + if err != nil { + return nil, err + } + return kb, nil +} diff --git a/gno.land/pkg/gnoclient/signer_test.go b/gno.land/pkg/gnoclient/signer_test.go new file mode 100644 index 00000000000..3b4cbb757ad --- /dev/null +++ b/gno.land/pkg/gnoclient/signer_test.go @@ -0,0 +1 @@ +package gnoclient diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index 16b3631d188..88750cb3600 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -42,6 +42,9 @@ const ( French // Italian is currently not supported. Italian +) + +const ( addressSuffix = "address" infoSuffix = "info" ) From 588dfba7c1f0713c3a968e2c804a752217634a5b Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:53:03 +0200 Subject: [PATCH 03/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 8 +++----- gno.land/pkg/gnoclient/example_test.go | 16 ++++++++-------- gno.land/pkg/gnoclient/signer.go | 18 +++++++++++------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index 2f7612dc1dc..e163fb6c58b 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -2,18 +2,16 @@ package gnoclient import ( "errors" - - "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) type Client struct { - Keybase keys.Keybase + Signer Signer Networking Networking } func (c Client) validateSigner() error { - if c.Keybase == nil { - return errors.New("missing c.Keybase") + if c.Signer == nil { + return errors.New("missing c.Signer") } return nil } diff --git a/gno.land/pkg/gnoclient/example_test.go b/gno.land/pkg/gnoclient/example_test.go index 2413140ce3e..9c95a3b1a22 100644 --- a/gno.land/pkg/gnoclient/example_test.go +++ b/gno.land/pkg/gnoclient/example_test.go @@ -8,12 +8,12 @@ import ( ) func Example_withDisk() { - home := "/path/to/dir" - account := "mykey" - passwd := "secure" - - kb, _ := keys.NewKeyBaseFromDir(home) - signer := gnoclient.Signer(kb, account, passwd) + kb, _ := keys.NewKeyBaseFromDir("/path/to/dir") + signer := gnoclient.SignerFromKeybase{ + Keybase: kb, + Account: "mykey", + Password: "secure", + } client := gnoclient.Client{ Signer: signer, } @@ -26,9 +26,9 @@ func Example_withInMemCrypto() { bip39Passphrase := "" account := uint32(0) index := uint32(0) - kb, _ := gnoclient.InmemKeybaseFromBip39(mnemo, bip39Passphrase, account, index) + signer, _ := gnoclient.SignerFromBip39(mnemo, bip39Passphrase, account, index) client := gnoclient.Client{ - Keybase: kb, + Signer: signer, } _ = client fmt.Println("Hello") diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go index f20b1c910b7..d032726a4eb 100644 --- a/gno.land/pkg/gnoclient/signer.go +++ b/gno.land/pkg/gnoclient/signer.go @@ -13,17 +13,15 @@ type SignerFromKeybase struct { Password string // encryption password } -var _ Signer = (*SignerFromKeybase)(nil) - -func SignerFromKeybase(kb keys.Keybase, nameOrBech32 string, passwd string) Signer { +func (s SignerFromKeybase) Sign() { panic("not implemneted") } -} +var _ Signer = (*SignerFromKeybase)(nil) -// InmemSignerFromBip39 creates an inmemory keybase which loads a single "default" account. +// SignerFromBip39 creates an inmemory keybase which loads a single "default" account. // It is intended to be used in systems that cannot rely on filesystem to store the private keys. // // Warning: It's recommended to use keys.NewKeyBaseFromDir when possible. -func InmemSignerFromBip39(mnemo string, passphrase string, account uint32, index uint32) (keys.Keybase, error) { +func SignerFromBip39(mnemo string, passphrase string, account uint32, index uint32) (Signer, error) { kb := keys.NewInMemory() name := "default" passwd := "" // not needed in memory @@ -31,5 +29,11 @@ func InmemSignerFromBip39(mnemo string, passphrase string, account uint32, index if err != nil { return nil, err } - return kb, nil + + signer := SignerFromKeybase{ + Keybase: kb, + Account: name, + Password: passwd, + } + return &signer, nil } From e11919b61df1c72fbdcd7cef1d3d65d1e68ce399 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:00:34 +0200 Subject: [PATCH 04/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/README.md | 19 ++++++++++--------- gno.land/pkg/gnoclient/example_test.go | 5 +++-- gno.land/pkg/gnoclient/signer.go | 25 ++++++++++++++++--------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/gno.land/pkg/gnoclient/README.md b/gno.land/pkg/gnoclient/README.md index af3c851964b..390559fb142 100644 --- a/gno.land/pkg/gnoclient/README.md +++ b/gno.land/pkg/gnoclient/README.md @@ -1,21 +1,22 @@ # Gno.land Go Client -This is a Go client library for interacting with the Gno.land RPC API. -The library provides a convenient way to make requests to the Gno.land API endpoints and process the responses. +The Gno.land Go client is a dedicated library for interacting seamlessly with the Gno.land RPC API. +This library simplifies the process of querying or sending transactions to the Gno.land RPC API and interpreting the responses. ## Installation -To use this library, you can add it to your Go project using `go get`: +Integrate this library into your Go project with the following command: go get github.com/gnolang/gno/gno.land/pkg/gnoclient -## Plan +## Development Plan -TODO: improve -* bootstrap the effort here for gno.land, then move the generic functions to tm2/gnovm/gnosdk later. -* start using this library in `gno.land/cmd/*` and external clients such as `gnoblog-client` or the discord community faucet bot. -* as soon as we've a strong generic client, let's try to use codegen to ease creating typesafe contract-specific clients. +The roadmap for the Gno.land Go client includes: + +- **Initial Development:** Kickstart the development specifically for Gno.land. Subsequently, transition the generic functionalities to other modules like `tm2`, `gnovm`, `gnosdk`. +- **Integration:** Begin incorporating this library within various components such as `gno.land/cmd/*` and other external clients, including `gnoblog-client`, the Discord community faucet bot, and [GnoMobile](https://github.com/gnolang/gnomobile). +- **Enhancements:** Once the generic client establishes a robust foundation, we aim to utilize code generation for contracts. This will streamline the creation of type-safe, contract-specific clients. ## Usage -TODO +TODO: Documentation for usage is currently in development and will be available soon. diff --git a/gno.land/pkg/gnoclient/example_test.go b/gno.land/pkg/gnoclient/example_test.go index 9c95a3b1a22..5031ae83898 100644 --- a/gno.land/pkg/gnoclient/example_test.go +++ b/gno.land/pkg/gnoclient/example_test.go @@ -7,6 +7,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) +// Example_withDisk demonstrates how to initialize a gnoclient with a keybase sourced from a directory. func Example_withDisk() { kb, _ := keys.NewKeyBaseFromDir("/path/to/dir") signer := gnoclient.SignerFromKeybase{ @@ -20,8 +21,8 @@ func Example_withDisk() { _ = client } +// Example_withInMemCrypto demonstrates how to initialize a gnoclient with an in-memory keybase using BIP39 mnemonics. func Example_withInMemCrypto() { - // create inmem keybase from bip39 mnemo := "index brass unknown lecture autumn provide royal shrimp elegant wink now zebra discover swarm act ill you bullet entire outdoor tilt usage gap multiply" bip39Passphrase := "" account := uint32(0) @@ -36,8 +37,8 @@ func Example_withInMemCrypto() { // Hello } +// Example_readOnly demonstrates how to initialize a read-only gnoclient, which can only query. func Example_readOnly() { - // read-only client, can only query. client := gnoclient.Client{} _ = client fmt.Println("Hello") diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go index d032726a4eb..b7985076e83 100644 --- a/gno.land/pkg/gnoclient/signer.go +++ b/gno.land/pkg/gnoclient/signer.go @@ -2,29 +2,35 @@ package gnoclient import "github.com/gnolang/gno/tm2/pkg/crypto/keys" -// Signer ... +// Signer provides an interface for signing. type Signer interface { Sign() } +// SignerFromKeybase represents a signer created from a Keybase. type SignerFromKeybase struct { - Keybase keys.Keybase - Account string // name or bech32 - Password string // encryption password + Keybase keys.Keybase // Holds keys in memory or on disk + Account string // Could be name or bech32 format + Password string // Password for encryption } -func (s SignerFromKeybase) Sign() { panic("not implemneted") } +// Sign implements the Signer interface for SignerFromKeybase. +func (s SignerFromKeybase) Sign() { + panic("not implemented") +} +// Ensure SignerFromKeybase implements Signer interface. var _ Signer = (*SignerFromKeybase)(nil) -// SignerFromBip39 creates an inmemory keybase which loads a single "default" account. -// It is intended to be used in systems that cannot rely on filesystem to store the private keys. +// SignerFromBip39 creates an in-memory keybase with a single default account. +// This can be useful in scenarios where storing private keys in the filesystem isn't feasible. // -// Warning: It's recommended to use keys.NewKeyBaseFromDir when possible. +// Warning: Using keys.NewKeyBaseFromDir is recommended where possible, as it is more secure. func SignerFromBip39(mnemo string, passphrase string, account uint32, index uint32) (Signer, error) { kb := keys.NewInMemory() name := "default" - passwd := "" // not needed in memory + passwd := "" // Password isn't needed for in-memory storage + _, err := kb.CreateAccount(name, mnemo, passphrase, passwd, account, index) if err != nil { return nil, err @@ -35,5 +41,6 @@ func SignerFromBip39(mnemo string, passphrase string, account uint32, index uint Account: name, Password: passwd, } + return &signer, nil } From f28ff5d86e418797a01c55bfcb4d3b3403448cee Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 00:19:37 +0200 Subject: [PATCH 05/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 122 ++++++++++++++++++++++++- gno.land/pkg/gnoclient/example_test.go | 22 ++++- gno.land/pkg/gnoclient/signer.go | 21 ++++- 3 files changed, 156 insertions(+), 9 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index e163fb6c58b..0614c6f909b 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -1,14 +1,18 @@ package gnoclient import ( - "errors" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/errors" ) type Client struct { - Signer Signer - Networking Networking + Signer Signer + RPCClient rpcclient.Client } +// validateSigner checks that the signer is correctly configured. func (c Client) validateSigner() error { if c.Signer == nil { return errors.New("missing c.Signer") @@ -16,13 +20,121 @@ func (c Client) validateSigner() error { return nil } +// validateRPCClient checks that the RPCClient is correctly configured. func (c Client) validateRPCClient() error { - if c.Networking == nil { - return errors.New("missing c.Networking") + if c.RPCClient == nil { + return errors.New("missing c.RPCClient") } return nil } +type QueryCfg struct { + Path string + Data []byte + client.ABCIQueryOptions +} + +func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { + if err := c.validateRPCClient(); err != nil { + return nil, err + } + return c.RPCClient.ABCIQueryWithOptions(cfg.Path, cfg.Data, cfg.ABCIQueryOptions) +} + +/* +func (c *Client) Call( + pkgPath string, + fnc string, + args []string, + gasFee string, + gasWanted int64, + send string, +) error { + if err := c.validateSigner(); err != nil { + return err + } + + caller := c.Signer.Info().GetAddress() + + // Parse send amount. + sendCoins, err := std.ParseCoins(send) + if err != nil { + return errors.Wrap(err, "parsing send coins") + } + + // parse gas wanted & fee. + gasFeeCoins, err := std.ParseCoin(gasFee) + if err != nil { + return errors.Wrap(err, "parsing gas fee coin") + } + + // construct msg & tx and marshal. + msg := vm.MsgCall{ + Caller: caller, + Send: sendCoins, + PkgPath: pkgPath, + Func: fnc, + Args: args, + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(gasWanted, gasFeeCoins), + Signatures: nil, + Memo: "", + } + + qopts := &queryCfg{ + remote: c.remote, + path: fmt.Sprintf("auth/accounts/%s", caller), + } + qres, err := queryHandler(qopts) + if err != nil { + return errors.Wrap(err, "query account") + } + var qret struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(qres.Response.Data, &qret) + if err != nil { + return err + } + + // sign tx + accountNumber := qret.BaseAccount.AccountNumber + sequence := qret.BaseAccount.Sequence + sopts := &signCfg{ + kb: c.keybase, + sequence: sequence, + accountNumber: accountNumber, + chainID: c.chainID, + nameOrBech32: nameOrBech32, + txJSON: amino.MustMarshalJSON(tx), + pass: password, + } + + signedTx, err := SignHandler(sopts) + if err != nil { + return errors.Wrap(err, "sign tx") + } + + // broadcast signed tx + bopts := &broadcastCfg{ + remote: c.remote, + tx: signedTx, + } + bres, err := broadcastHandler(bopts) + if err != nil { + return errors.Wrap(err, "broadcast tx") + } + if bres.CheckTx.IsErr() { + return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + } + if bres.DeliverTx.IsErr() { + return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + } + + return nil +} +*/ + // TODO: port existing code, i.e. faucet? // TODO: create right now a tm2 generic go client and a gnovm generic go client? // TODO: Command: Call diff --git a/gno.land/pkg/gnoclient/example_test.go b/gno.land/pkg/gnoclient/example_test.go index 5031ae83898..65f6b8aafbd 100644 --- a/gno.land/pkg/gnoclient/example_test.go +++ b/gno.land/pkg/gnoclient/example_test.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gnolang/gno/gno.land/pkg/gnoclient" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) @@ -15,8 +16,13 @@ func Example_withDisk() { Account: "mykey", Password: "secure", } + + remote := "127.0.0.1:26657" + rpcClient := client.NewHTTP(remote, "/websocket") + client := gnoclient.Client{ - Signer: signer, + Signer: signer, + RPCClient: rpcClient, } _ = client } @@ -28,8 +34,13 @@ func Example_withInMemCrypto() { account := uint32(0) index := uint32(0) signer, _ := gnoclient.SignerFromBip39(mnemo, bip39Passphrase, account, index) + + remote := "127.0.0.1:26657" + rpcClient := client.NewHTTP(remote, "/websocket") + client := gnoclient.Client{ - Signer: signer, + Signer: signer, + RPCClient: rpcClient, } _ = client fmt.Println("Hello") @@ -39,7 +50,12 @@ func Example_withInMemCrypto() { // Example_readOnly demonstrates how to initialize a read-only gnoclient, which can only query. func Example_readOnly() { - client := gnoclient.Client{} + remote := "127.0.0.1:26657" + rpcClient := client.NewHTTP(remote, "/websocket") + + client := gnoclient.Client{ + RPCClient: rpcClient, + } _ = client fmt.Println("Hello") // Output: diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go index b7985076e83..dbfbf36a383 100644 --- a/gno.land/pkg/gnoclient/signer.go +++ b/gno.land/pkg/gnoclient/signer.go @@ -4,7 +4,9 @@ import "github.com/gnolang/gno/tm2/pkg/crypto/keys" // Signer provides an interface for signing. type Signer interface { - Sign() + Sign() // TBD + Info() keys.Info // returns keys info, containing the address. + Validate() error // checks wether the signer is well configured. } // SignerFromKeybase represents a signer created from a Keybase. @@ -14,6 +16,23 @@ type SignerFromKeybase struct { Password string // Password for encryption } +func (s SignerFromKeybase) Validate() error { + _, err := s.Keybase.GetByNameOrAddress(s.Account) + if err != nil { + return err + } + // XXX: additional checks? + return nil +} + +func (s SignerFromKeybase) Info() keys.Info { + info, err := s.Keybase.GetByNameOrAddress(s.Account) + if err != nil { + panic("should not happen") + } + return info +} + // Sign implements the Signer interface for SignerFromKeybase. func (s SignerFromKeybase) Sign() { panic("not implemented") From 2f6721eccab6706bd2a4c41f214c34ad87d1dbb3 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 02:13:11 +0200 Subject: [PATCH 06/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 158 +++++++++++++++++++------------ gno.land/pkg/gnoclient/signer.go | 79 ++++++++++++++-- 2 files changed, 168 insertions(+), 69 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index 0614c6f909b..64a797feb80 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -1,10 +1,15 @@ package gnoclient import ( + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/std" ) type Client struct { @@ -34,6 +39,7 @@ type QueryCfg struct { client.ABCIQueryOptions } +// XXX: not sure if we should keep this helper or encourage people to use ABCIQueryWithOptions directly. func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { if err := c.validateRPCClient(); err != nil { return nil, err @@ -41,99 +47,127 @@ func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { return c.RPCClient.ABCIQueryWithOptions(cfg.Path, cfg.Data, cfg.ABCIQueryOptions) } -/* -func (c *Client) Call( - pkgPath string, - fnc string, - args []string, - gasFee string, - gasWanted int64, - send string, -) error { - if err := c.validateSigner(); err != nil { - return err +func (c Client) QueryAccount(addr string) (*std.BaseAccount, *ctypes.ResultABCIQuery, error) { + if err := c.validateRPCClient(); err != nil { + return nil, nil, err } - caller := c.Signer.Info().GetAddress() + path := fmt.Sprintf("auth/accounts/%s", addr) + data := []byte{} + + qres, err := c.RPCClient.ABCIQuery(path, data) + if err != nil { + return nil, nil, errors.Wrap(err, "query account") + } + + var qret struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(qres.Response.Data, &qret) + if err != nil { + return nil, nil, err + } + + return &qret.BaseAccount, qres, nil +} + +type CallCfg struct { + PkgPath string + FuncName string + Args []string + + GasFee string + GasWanted int64 + Send string + + AccountNumber uint64 + SequenceNumber uint64 +} + +func (c *Client) Call(cfg CallCfg) error { + // validate config. + if cfg.PkgPath == "" { + return errors.New("missing PkgPath") + } + if cfg.FuncName == "" { + return errors.New("missing FuncName") + } // Parse send amount. - sendCoins, err := std.ParseCoins(send) + sendCoins, err := std.ParseCoins(cfg.Send) if err != nil { return errors.Wrap(err, "parsing send coins") } // parse gas wanted & fee. - gasFeeCoins, err := std.ParseCoin(gasFee) + gasFeeCoins, err := std.ParseCoin(cfg.GasFee) if err != nil { return errors.Wrap(err, "parsing gas fee coin") } + // validate required client fields. + if err := c.validateSigner(); err != nil { + return err + } + if err := c.validateRPCClient(); err != nil { + return err + } + + caller := c.Signer.Info().GetAddress() + // construct msg & tx and marshal. msg := vm.MsgCall{ Caller: caller, Send: sendCoins, - PkgPath: pkgPath, - Func: fnc, - Args: args, + PkgPath: cfg.PkgPath, + Func: cfg.FuncName, + Args: cfg.Args, } tx := std.Tx{ Msgs: []std.Msg{msg}, - Fee: std.NewFee(gasWanted, gasFeeCoins), + Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: "", } - qopts := &queryCfg{ - remote: c.remote, - path: fmt.Sprintf("auth/accounts/%s", caller), - } - qres, err := queryHandler(qopts) - if err != nil { - return errors.Wrap(err, "query account") - } - var qret struct{ BaseAccount std.BaseAccount } - err = amino.UnmarshalJSON(qres.Response.Data, &qret) - if err != nil { - return err + if cfg.SequenceNumber == 0 || cfg.AccountNumber == 0 { + account, _, err := c.QueryAccount(caller.String()) + if err != nil { + return errors.Wrap(err, "query account") + } + cfg.AccountNumber = account.AccountNumber + cfg.SequenceNumber = account.Sequence } - // sign tx - accountNumber := qret.BaseAccount.AccountNumber - sequence := qret.BaseAccount.Sequence - sopts := &signCfg{ - kb: c.keybase, - sequence: sequence, - accountNumber: accountNumber, - chainID: c.chainID, - nameOrBech32: nameOrBech32, - txJSON: amino.MustMarshalJSON(tx), - pass: password, + signCfg := SignCfg{ + UnsignedTX: tx, + SequenceNumber: cfg.SequenceNumber, + AccountNumber: cfg.AccountNumber, } - - signedTx, err := SignHandler(sopts) + signedTx, err := c.Signer.Sign(signCfg) if err != nil { - return errors.Wrap(err, "sign tx") - } - - // broadcast signed tx - bopts := &broadcastCfg{ - remote: c.remote, - tx: signedTx, - } - bres, err := broadcastHandler(bopts) - if err != nil { - return errors.Wrap(err, "broadcast tx") - } - if bres.CheckTx.IsErr() { - return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) - } - if bres.DeliverTx.IsErr() { - return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) - } + return errors.Wrap(err, "sign") + } + + _ = signedTx + /* + // broadcast signed tx + bopts := &broadcastCfg{ + remote: c.remote, + tx: signedTx, + } + bres, err := broadcastHandler(bopts) + if err != nil { + return errors.Wrap(err, "broadcast tx") + } + if bres.CheckTx.IsErr() { + return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + } + if bres.DeliverTx.IsErr() { + return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + } + */ return nil } -*/ // TODO: port existing code, i.e. faucet? // TODO: create right now a tm2 generic go client and a gnovm generic go client? diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go index dbfbf36a383..adab38983d8 100644 --- a/gno.land/pkg/gnoclient/signer.go +++ b/gno.land/pkg/gnoclient/signer.go @@ -1,12 +1,18 @@ package gnoclient -import "github.com/gnolang/gno/tm2/pkg/crypto/keys" +import ( + "fmt" + + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/std" +) // Signer provides an interface for signing. type Signer interface { - Sign() // TBD - Info() keys.Info // returns keys info, containing the address. - Validate() error // checks wether the signer is well configured. + Sign(SignCfg) (*std.Tx, error) // returns a signed tx, ready to be broadcasted. + Info() keys.Info // returns keys info, containing the address. + Validate() error // checks wether the signer is well configured. } // SignerFromKeybase represents a signer created from a Keybase. @@ -14,14 +20,20 @@ type SignerFromKeybase struct { Keybase keys.Keybase // Holds keys in memory or on disk Account string // Could be name or bech32 format Password string // Password for encryption + ChainID string } func (s SignerFromKeybase) Validate() error { + if s.ChainID == "" { + return errors.New("missing ChainID") + } + _, err := s.Keybase.GetByNameOrAddress(s.Account) if err != nil { return err } - // XXX: additional checks? + + // TODO: also verify if the password unlocks the account. return nil } @@ -34,8 +46,61 @@ func (s SignerFromKeybase) Info() keys.Info { } // Sign implements the Signer interface for SignerFromKeybase. -func (s SignerFromKeybase) Sign() { - panic("not implemented") +type SignCfg struct { + UnsignedTX std.Tx + SequenceNumber uint64 + AccountNumber uint64 +} + +func (s SignerFromKeybase) Sign(cfg SignCfg) (*std.Tx, error) { + tx := cfg.UnsignedTX + chainID := s.ChainID + accountNumber := cfg.AccountNumber + sequenceNumber := cfg.SequenceNumber + account := s.Account + password := s.Password + + // fill tx signatures. + signers := tx.GetSigners() + if tx.Signatures == nil { + for range signers { + tx.Signatures = append(tx.Signatures, std.Signature{ + PubKey: nil, // zero signature + Signature: nil, // zero signature + }) + } + } + + // validate document to sign. + err := tx.ValidateBasic() + if err != nil { + return nil, err + } + + // derive sign doc bytes. + signbz := tx.GetSignBytes(chainID, accountNumber, sequenceNumber) + + sig, pub, err := s.Keybase.Sign(account, password, signbz) + if err != nil { + return nil, err + } + addr := pub.Address() + found := false + for i := range tx.Signatures { + if signers[i] == addr { + found = true + tx.Signatures[i] = std.Signature{ + PubKey: pub, + Signature: sig, + } + } + } + + if !found { + return nil, fmt.Errorf("addr %v (%s) not in signer set", addr, account) + } + + return &tx, nil } // Ensure SignerFromKeybase implements Signer interface. From 1cd0b1d71424bcd136b1f1089f1e03728c687f15 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 02:40:38 +0200 Subject: [PATCH 07/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 106 +++++++++++++++++-------------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index 64a797feb80..22bbb5f9bb5 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -74,41 +74,51 @@ type CallCfg struct { FuncName string Args []string - GasFee string - GasWanted int64 - Send string - + GasFee string + GasWanted int64 + Send string AccountNumber uint64 SequenceNumber uint64 + Memo string } -func (c *Client) Call(cfg CallCfg) error { +func (c *Client) CallCommit(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { + pkgPath := cfg.PkgPath + funcName := cfg.FuncName + args := cfg.Args + gasWanted := cfg.GasWanted + gasFee := cfg.GasFee + send := cfg.Send + sequenceNumber := cfg.SequenceNumber + accountNumber := cfg.AccountNumber + memo := cfg.Memo + // validate config. - if cfg.PkgPath == "" { - return errors.New("missing PkgPath") + if pkgPath == "" { + return nil, errors.New("missing PkgPath") } - if cfg.FuncName == "" { - return errors.New("missing FuncName") + if funcName == "" { + return nil, errors.New("missing FuncName") } // Parse send amount. - sendCoins, err := std.ParseCoins(cfg.Send) + sendCoins, err := std.ParseCoins(send) if err != nil { - return errors.Wrap(err, "parsing send coins") + return nil, errors.Wrap(err, "parsing send coins") } // parse gas wanted & fee. - gasFeeCoins, err := std.ParseCoin(cfg.GasFee) + gasFeeCoins, err := std.ParseCoin(gasFee) if err != nil { - return errors.Wrap(err, "parsing gas fee coin") + return nil, errors.Wrap(err, "parsing gas fee coin") } // validate required client fields. if err := c.validateSigner(); err != nil { - return err + return nil, errors.Wrap(err, "validate signer") } if err := c.validateRPCClient(); err != nil { - return err + return nil, errors.Wrap(err, "validate RPC client") } caller := c.Signer.Info().GetAddress() @@ -117,56 +127,60 @@ func (c *Client) Call(cfg CallCfg) error { msg := vm.MsgCall{ Caller: caller, Send: sendCoins, - PkgPath: cfg.PkgPath, - Func: cfg.FuncName, - Args: cfg.Args, + PkgPath: pkgPath, + Func: funcName, + Args: args, } tx := std.Tx{ Msgs: []std.Msg{msg}, - Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), + Fee: std.NewFee(gasWanted, gasFeeCoins), Signatures: nil, - Memo: "", + Memo: memo, } - if cfg.SequenceNumber == 0 || cfg.AccountNumber == 0 { + return c.signAndBroadcastTxCommit(tx, accountNumber, sequenceNumber) +} + +func (c Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { + caller := c.Signer.Info().GetAddress() + + if sequenceNumber == 0 || accountNumber == 0 { account, _, err := c.QueryAccount(caller.String()) if err != nil { - return errors.Wrap(err, "query account") + return nil, errors.Wrap(err, "query account") } - cfg.AccountNumber = account.AccountNumber - cfg.SequenceNumber = account.Sequence + accountNumber = account.AccountNumber + sequenceNumber = account.Sequence } signCfg := SignCfg{ UnsignedTX: tx, - SequenceNumber: cfg.SequenceNumber, - AccountNumber: cfg.AccountNumber, + SequenceNumber: sequenceNumber, + AccountNumber: accountNumber, } signedTx, err := c.Signer.Sign(signCfg) if err != nil { - return errors.Wrap(err, "sign") + return nil, errors.Wrap(err, "sign") } - _ = signedTx - /* - // broadcast signed tx - bopts := &broadcastCfg{ - remote: c.remote, - tx: signedTx, - } - bres, err := broadcastHandler(bopts) - if err != nil { - return errors.Wrap(err, "broadcast tx") - } - if bres.CheckTx.IsErr() { - return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) - } - if bres.DeliverTx.IsErr() { - return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) - } - */ + bz, err := amino.Marshal(signedTx) + if err != nil { + return nil, errors.Wrap(err, "remarshaling tx binary bytes") + } - return nil + bres, err := c.RPCClient.BroadcastTxCommit(bz) + if err != nil { + return nil, errors.Wrap(err, "broadcasting bytes") + } + + if bres.CheckTx.IsErr() { + return nil, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + } + if bres.DeliverTx.IsErr() { + return nil, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + } + + return bres, nil } // TODO: port existing code, i.e. faucet? From 3deec7c66101bfb6702c4c82402e86b3ab0d5c9a Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 02:49:54 +0200 Subject: [PATCH 08/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index 22bbb5f9bb5..4a74e434ef8 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -82,7 +82,15 @@ type CallCfg struct { Memo string } -func (c *Client) CallCommit(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { +func (c *Client) Call(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { + // validate required client fields. + if err := c.validateSigner(); err != nil { + return nil, errors.Wrap(err, "validate signer") + } + if err := c.validateRPCClient(); err != nil { + return nil, errors.Wrap(err, "validate RPC client") + } + pkgPath := cfg.PkgPath funcName := cfg.FuncName args := cfg.Args @@ -113,14 +121,6 @@ func (c *Client) CallCommit(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error return nil, errors.Wrap(err, "parsing gas fee coin") } - // validate required client fields. - if err := c.validateSigner(); err != nil { - return nil, errors.Wrap(err, "validate signer") - } - if err := c.validateRPCClient(); err != nil { - return nil, errors.Wrap(err, "validate RPC client") - } - caller := c.Signer.Info().GetAddress() // construct msg & tx and marshal. From 96c9076d368edf51607288ded8cb841de36f0bcc Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 02:51:16 +0200 Subject: [PATCH 09/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/networking.go | 42 ----------------------- gno.land/pkg/gnoclient/networking_test.go | 1 - 2 files changed, 43 deletions(-) delete mode 100644 gno.land/pkg/gnoclient/networking.go delete mode 100644 gno.land/pkg/gnoclient/networking_test.go diff --git a/gno.land/pkg/gnoclient/networking.go b/gno.land/pkg/gnoclient/networking.go deleted file mode 100644 index cc2e372d50e..00000000000 --- a/gno.land/pkg/gnoclient/networking.go +++ /dev/null @@ -1,42 +0,0 @@ -package gnoclient - -import "errors" - -// Networking ... -type Networking interface { - // write methods. - - // Broadcast sends an encoded and signed transaction message to the blockchain. - // The default implementation connects on a node's Websocket interface. - Broadcast(txbz []byte) error - - // TODO: asynchronous Broadcast - // Broadcast(txbz []byte) (ch <-Event, error) - - // read methods. - - Query() -} - -type WebsocketClient struct { - // Opts - Remote string // Remote RPC node -} - -var _ Networking = (*WebsocketClient)(nil) // should implement Networking - -func (wsc WebsocketClient) ValidateOpts() error { - if wsc.Remote == "" { - return errors.New("missing remote url") - } - - return nil -} - -func (wsc WebsocketClient) Broadcast(txbz []byte) error { - return errors.New("not implemented") -} - -func (wsc WebsocketClient) Query() { - -} diff --git a/gno.land/pkg/gnoclient/networking_test.go b/gno.land/pkg/gnoclient/networking_test.go deleted file mode 100644 index 3b4cbb757ad..00000000000 --- a/gno.land/pkg/gnoclient/networking_test.go +++ /dev/null @@ -1 +0,0 @@ -package gnoclient From 3f63f40e9bcc701b5566bb38e578948912a839eb Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 02:56:58 +0200 Subject: [PATCH 10/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 66 ++++++++++++++------------------ gno.land/pkg/gnoclient/signer.go | 38 +++++++++--------- 2 files changed, 48 insertions(+), 56 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index 4a74e434ef8..0f1cebbab06 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -12,15 +12,16 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) +// Client provides an interface for interacting with the blockchain. type Client struct { - Signer Signer - RPCClient rpcclient.Client + Signer Signer // Signer for transaction authentication + RPCClient rpcclient.Client // RPC client for blockchain communication } // validateSigner checks that the signer is correctly configured. func (c Client) validateSigner() error { if c.Signer == nil { - return errors.New("missing c.Signer") + return errors.New("missing Signer") } return nil } @@ -28,18 +29,19 @@ func (c Client) validateSigner() error { // validateRPCClient checks that the RPCClient is correctly configured. func (c Client) validateRPCClient() error { if c.RPCClient == nil { - return errors.New("missing c.RPCClient") + return errors.New("missing RPCClient") } return nil } +// QueryCfg contains configuration options for performing queries. type QueryCfg struct { - Path string - Data []byte - client.ABCIQueryOptions + Path string // Query path + Data []byte // Query data + client.ABCIQueryOptions // ABCI query options } -// XXX: not sure if we should keep this helper or encourage people to use ABCIQueryWithOptions directly. +// Query performs a generic query on the blockchain. func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { if err := c.validateRPCClient(); err != nil { return nil, err @@ -47,6 +49,7 @@ func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { return c.RPCClient.ABCIQueryWithOptions(cfg.Path, cfg.Data, cfg.ABCIQueryOptions) } +// QueryAccount retrieves account information for a given address. func (c Client) QueryAccount(addr string) (*std.BaseAccount, *ctypes.ResultABCIQuery, error) { if err := c.validateRPCClient(); err != nil { return nil, nil, err @@ -69,21 +72,22 @@ func (c Client) QueryAccount(addr string) (*std.BaseAccount, *ctypes.ResultABCIQ return &qret.BaseAccount, qres, nil } +// CallCfg contains configuration options for executing a contract call. type CallCfg struct { - PkgPath string - FuncName string - Args []string - - GasFee string - GasWanted int64 - Send string - AccountNumber uint64 - SequenceNumber uint64 - Memo string + PkgPath string // Package path + FuncName string // Function name + Args []string // Function arguments + GasFee string // Gas fee + GasWanted int64 // Gas wanted + Send string // Send amount + AccountNumber uint64 // Account number + SequenceNumber uint64 // Sequence number + Memo string // Memo } +// Call executes a contract call on the blockchain. func (c *Client) Call(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { - // validate required client fields. + // Validate required client fields. if err := c.validateSigner(); err != nil { return nil, errors.Wrap(err, "validate signer") } @@ -101,7 +105,7 @@ func (c *Client) Call(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { accountNumber := cfg.AccountNumber memo := cfg.Memo - // validate config. + // Validate config. if pkgPath == "" { return nil, errors.New("missing PkgPath") } @@ -115,7 +119,7 @@ func (c *Client) Call(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { return nil, errors.Wrap(err, "parsing send coins") } - // parse gas wanted & fee. + // Parse gas wanted & fee. gasFeeCoins, err := std.ParseCoin(gasFee) if err != nil { return nil, errors.Wrap(err, "parsing gas fee coin") @@ -123,7 +127,7 @@ func (c *Client) Call(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { caller := c.Signer.Info().GetAddress() - // construct msg & tx and marshal. + // Construct message & transaction and marshal. msg := vm.MsgCall{ Caller: caller, Send: sendCoins, @@ -141,6 +145,7 @@ func (c *Client) Call(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { return c.signAndBroadcastTxCommit(tx, accountNumber, sequenceNumber) } +// signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result. func (c Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { caller := c.Signer.Info().GetAddress() @@ -165,7 +170,7 @@ func (c Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumbe bz, err := amino.Marshal(signedTx) if err != nil { - return nil, errors.Wrap(err, "remarshaling tx binary bytes") + return nil, errors.Wrap(err, "marshaling tx binary bytes") } bres, err := c.RPCClient.BroadcastTxCommit(bz) @@ -183,17 +188,4 @@ func (c Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumbe return bres, nil } -// TODO: port existing code, i.e. faucet? -// TODO: create right now a tm2 generic go client and a gnovm generic go client? -// TODO: Command: Call -// TODO: Command: Send -// TODO: Command: AddPkg -// TODO: Command: Query -// TODO: Command: Eval -// TODO: Command: Exec -// TODO: Command: Package -// TODO: Command: QFile -// TODO: examples and unit tests -// TODO: Mock -// TODO: alternative configuration (pass existing websocket?) -// TODO: minimal go.mod to make it light to import +// TODO: Add more functionality, examples, and unit tests. diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go index adab38983d8..098af742e4d 100644 --- a/gno.land/pkg/gnoclient/signer.go +++ b/gno.land/pkg/gnoclient/signer.go @@ -8,19 +8,19 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) -// Signer provides an interface for signing. +// Signer provides an interface for signing transactions. type Signer interface { - Sign(SignCfg) (*std.Tx, error) // returns a signed tx, ready to be broadcasted. - Info() keys.Info // returns keys info, containing the address. - Validate() error // checks wether the signer is well configured. + Sign(SignCfg) (*std.Tx, error) // Signs a transaction and returns a signed tx ready for broadcasting. + Info() keys.Info // Returns key information, including the address. + Validate() error // Checks whether the signer is properly configured. } // SignerFromKeybase represents a signer created from a Keybase. type SignerFromKeybase struct { - Keybase keys.Keybase // Holds keys in memory or on disk - Account string // Could be name or bech32 format + Keybase keys.Keybase // Stores keys in memory or on disk + Account string // Account name or bech32 format Password string // Password for encryption - ChainID string + ChainID string // Chain ID for transaction signing } func (s SignerFromKeybase) Validate() error { @@ -33,7 +33,7 @@ func (s SignerFromKeybase) Validate() error { return err } - // TODO: also verify if the password unlocks the account. + // TODO: Also verify if the password unlocks the account. return nil } @@ -60,24 +60,24 @@ func (s SignerFromKeybase) Sign(cfg SignCfg) (*std.Tx, error) { account := s.Account password := s.Password - // fill tx signatures. + // Initialize tx signatures. signers := tx.GetSigners() if tx.Signatures == nil { for range signers { tx.Signatures = append(tx.Signatures, std.Signature{ - PubKey: nil, // zero signature - Signature: nil, // zero signature + PubKey: nil, // Zero signature + Signature: nil, // Zero signature }) } } - // validate document to sign. + // Validate the transaction to sign. err := tx.ValidateBasic() if err != nil { return nil, err } - // derive sign doc bytes. + // Derive sign doc bytes. signbz := tx.GetSignBytes(chainID, accountNumber, sequenceNumber) sig, pub, err := s.Keybase.Sign(account, password, signbz) @@ -97,25 +97,25 @@ func (s SignerFromKeybase) Sign(cfg SignCfg) (*std.Tx, error) { } if !found { - return nil, fmt.Errorf("addr %v (%s) not in signer set", addr, account) + return nil, fmt.Errorf("address %v (%s) not in signer set", addr, account) } return &tx, nil } -// Ensure SignerFromKeybase implements Signer interface. +// Ensure SignerFromKeybase implements the Signer interface. var _ Signer = (*SignerFromKeybase)(nil) // SignerFromBip39 creates an in-memory keybase with a single default account. // This can be useful in scenarios where storing private keys in the filesystem isn't feasible. // // Warning: Using keys.NewKeyBaseFromDir is recommended where possible, as it is more secure. -func SignerFromBip39(mnemo string, passphrase string, account uint32, index uint32) (Signer, error) { +func SignerFromBip39(mnemonic string, passphrase string, account uint32, index uint32) (Signer, error) { kb := keys.NewInMemory() name := "default" - passwd := "" // Password isn't needed for in-memory storage + password := "" // Password isn't needed for in-memory storage - _, err := kb.CreateAccount(name, mnemo, passphrase, passwd, account, index) + _, err := kb.CreateAccount(name, mnemonic, passphrase, password, account, index) if err != nil { return nil, err } @@ -123,7 +123,7 @@ func SignerFromBip39(mnemo string, passphrase string, account uint32, index uint signer := SignerFromKeybase{ Keybase: kb, Account: name, - Password: passwd, + Password: password, } return &signer, nil From 89e9b48061d8ee2999008f617551b2f98bd1fb9f Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 02:59:12 +0200 Subject: [PATCH 11/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index 0f1cebbab06..44347253c94 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -14,7 +14,7 @@ import ( // Client provides an interface for interacting with the blockchain. type Client struct { - Signer Signer // Signer for transaction authentication + Signer Signer // Signer for transaction authentication RPCClient rpcclient.Client // RPC client for blockchain communication } @@ -36,9 +36,9 @@ func (c Client) validateRPCClient() error { // QueryCfg contains configuration options for performing queries. type QueryCfg struct { - Path string // Query path - Data []byte // Query data - client.ABCIQueryOptions // ABCI query options + Path string // Query path + Data []byte // Query data + client.ABCIQueryOptions // ABCI query options } // Query performs a generic query on the blockchain. From 5cd1252b0bbdb3b1e9aece3b014a0ee1eaf4162d Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 03:01:15 +0200 Subject: [PATCH 12/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index 44347253c94..c73449fd36a 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -5,7 +5,6 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/errors" @@ -36,9 +35,9 @@ func (c Client) validateRPCClient() error { // QueryCfg contains configuration options for performing queries. type QueryCfg struct { - Path string // Query path - Data []byte // Query data - client.ABCIQueryOptions // ABCI query options + Path string // Query path + Data []byte // Query data + rpcclient.ABCIQueryOptions // ABCI query options } // Query performs a generic query on the blockchain. From e9901a602bd43bee83f46f68e87967d29830594d Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 03:13:58 +0200 Subject: [PATCH 13/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index c73449fd36a..eace5d535be 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -45,7 +45,16 @@ func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { if err := c.validateRPCClient(); err != nil { return nil, err } - return c.RPCClient.ABCIQueryWithOptions(cfg.Path, cfg.Data, cfg.ABCIQueryOptions) + qres, err := c.RPCClient.ABCIQueryWithOptions(cfg.Path, cfg.Data, cfg.ABCIQueryOptions) + if err != nil { + return nil, errors.Wrap(err, "query error") + } + + if qres.Response.Error != nil { + return qres, errors.Wrap(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) + } + + return qres, nil } // QueryAccount retrieves account information for a given address. @@ -178,10 +187,10 @@ func (c Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumbe } if bres.CheckTx.IsErr() { - return nil, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + return bres, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) } if bres.DeliverTx.IsErr() { - return nil, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + return bres, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) } return bres, nil From 795f01769e976b25e3ad690c0f0a79d83120e407 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 03:50:51 +0200 Subject: [PATCH 14/24] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client.go | 171 ----------------------- gno.land/pkg/gnoclient/client_queries.go | 58 ++++++++ gno.land/pkg/gnoclient/client_txs.go | 127 +++++++++++++++++ 3 files changed, 185 insertions(+), 171 deletions(-) create mode 100644 gno.land/pkg/gnoclient/client_queries.go create mode 100644 gno.land/pkg/gnoclient/client_txs.go diff --git a/gno.land/pkg/gnoclient/client.go b/gno.land/pkg/gnoclient/client.go index eace5d535be..2c43a5fa01d 100644 --- a/gno.land/pkg/gnoclient/client.go +++ b/gno.land/pkg/gnoclient/client.go @@ -1,14 +1,8 @@ package gnoclient import ( - "fmt" - - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - "github.com/gnolang/gno/tm2/pkg/amino" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/errors" - "github.com/gnolang/gno/tm2/pkg/std" ) // Client provides an interface for interacting with the blockchain. @@ -32,168 +26,3 @@ func (c Client) validateRPCClient() error { } return nil } - -// QueryCfg contains configuration options for performing queries. -type QueryCfg struct { - Path string // Query path - Data []byte // Query data - rpcclient.ABCIQueryOptions // ABCI query options -} - -// Query performs a generic query on the blockchain. -func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { - if err := c.validateRPCClient(); err != nil { - return nil, err - } - qres, err := c.RPCClient.ABCIQueryWithOptions(cfg.Path, cfg.Data, cfg.ABCIQueryOptions) - if err != nil { - return nil, errors.Wrap(err, "query error") - } - - if qres.Response.Error != nil { - return qres, errors.Wrap(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) - } - - return qres, nil -} - -// QueryAccount retrieves account information for a given address. -func (c Client) QueryAccount(addr string) (*std.BaseAccount, *ctypes.ResultABCIQuery, error) { - if err := c.validateRPCClient(); err != nil { - return nil, nil, err - } - - path := fmt.Sprintf("auth/accounts/%s", addr) - data := []byte{} - - qres, err := c.RPCClient.ABCIQuery(path, data) - if err != nil { - return nil, nil, errors.Wrap(err, "query account") - } - - var qret struct{ BaseAccount std.BaseAccount } - err = amino.UnmarshalJSON(qres.Response.Data, &qret) - if err != nil { - return nil, nil, err - } - - return &qret.BaseAccount, qres, nil -} - -// CallCfg contains configuration options for executing a contract call. -type CallCfg struct { - PkgPath string // Package path - FuncName string // Function name - Args []string // Function arguments - GasFee string // Gas fee - GasWanted int64 // Gas wanted - Send string // Send amount - AccountNumber uint64 // Account number - SequenceNumber uint64 // Sequence number - Memo string // Memo -} - -// Call executes a contract call on the blockchain. -func (c *Client) Call(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { - // Validate required client fields. - if err := c.validateSigner(); err != nil { - return nil, errors.Wrap(err, "validate signer") - } - if err := c.validateRPCClient(); err != nil { - return nil, errors.Wrap(err, "validate RPC client") - } - - pkgPath := cfg.PkgPath - funcName := cfg.FuncName - args := cfg.Args - gasWanted := cfg.GasWanted - gasFee := cfg.GasFee - send := cfg.Send - sequenceNumber := cfg.SequenceNumber - accountNumber := cfg.AccountNumber - memo := cfg.Memo - - // Validate config. - if pkgPath == "" { - return nil, errors.New("missing PkgPath") - } - if funcName == "" { - return nil, errors.New("missing FuncName") - } - - // Parse send amount. - sendCoins, err := std.ParseCoins(send) - if err != nil { - return nil, errors.Wrap(err, "parsing send coins") - } - - // Parse gas wanted & fee. - gasFeeCoins, err := std.ParseCoin(gasFee) - if err != nil { - return nil, errors.Wrap(err, "parsing gas fee coin") - } - - caller := c.Signer.Info().GetAddress() - - // Construct message & transaction and marshal. - msg := vm.MsgCall{ - Caller: caller, - Send: sendCoins, - PkgPath: pkgPath, - Func: funcName, - Args: args, - } - tx := std.Tx{ - Msgs: []std.Msg{msg}, - Fee: std.NewFee(gasWanted, gasFeeCoins), - Signatures: nil, - Memo: memo, - } - - return c.signAndBroadcastTxCommit(tx, accountNumber, sequenceNumber) -} - -// signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result. -func (c Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { - caller := c.Signer.Info().GetAddress() - - if sequenceNumber == 0 || accountNumber == 0 { - account, _, err := c.QueryAccount(caller.String()) - if err != nil { - return nil, errors.Wrap(err, "query account") - } - accountNumber = account.AccountNumber - sequenceNumber = account.Sequence - } - - signCfg := SignCfg{ - UnsignedTX: tx, - SequenceNumber: sequenceNumber, - AccountNumber: accountNumber, - } - signedTx, err := c.Signer.Sign(signCfg) - if err != nil { - return nil, errors.Wrap(err, "sign") - } - - bz, err := amino.Marshal(signedTx) - if err != nil { - return nil, errors.Wrap(err, "marshaling tx binary bytes") - } - - bres, err := c.RPCClient.BroadcastTxCommit(bz) - if err != nil { - return nil, errors.Wrap(err, "broadcasting bytes") - } - - if bres.CheckTx.IsErr() { - return bres, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) - } - if bres.DeliverTx.IsErr() { - return bres, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) - } - - return bres, nil -} - -// TODO: Add more functionality, examples, and unit tests. diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go new file mode 100644 index 00000000000..1f2edf4b3cf --- /dev/null +++ b/gno.land/pkg/gnoclient/client_queries.go @@ -0,0 +1,58 @@ +package gnoclient + +import ( + "fmt" + + "github.com/gnolang/gno/tm2/pkg/amino" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// QueryCfg contains configuration options for performing queries. +type QueryCfg struct { + Path string // Query path + Data []byte // Query data + rpcclient.ABCIQueryOptions // ABCI query options +} + +// Query performs a generic query on the blockchain. +func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { + if err := c.validateRPCClient(); err != nil { + return nil, err + } + qres, err := c.RPCClient.ABCIQueryWithOptions(cfg.Path, cfg.Data, cfg.ABCIQueryOptions) + if err != nil { + return nil, errors.Wrap(err, "query error") + } + + if qres.Response.Error != nil { + return qres, errors.Wrap(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) + } + + return qres, nil +} + +// QueryAccount retrieves account information for a given address. +func (c Client) QueryAccount(addr string) (*std.BaseAccount, *ctypes.ResultABCIQuery, error) { + if err := c.validateRPCClient(); err != nil { + return nil, nil, err + } + + path := fmt.Sprintf("auth/accounts/%s", addr) + data := []byte{} + + qres, err := c.RPCClient.ABCIQuery(path, data) + if err != nil { + return nil, nil, errors.Wrap(err, "query account") + } + + var qret struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(qres.Response.Data, &qret) + if err != nil { + return nil, nil, err + } + + return &qret.BaseAccount, qres, nil +} diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go new file mode 100644 index 00000000000..cb779891e16 --- /dev/null +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -0,0 +1,127 @@ +package gnoclient + +import ( + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// CallCfg contains configuration options for executing a contract call. +type CallCfg struct { + PkgPath string // Package path + FuncName string // Function name + Args []string // Function arguments + GasFee string // Gas fee + GasWanted int64 // Gas wanted + Send string // Send amount + AccountNumber uint64 // Account number + SequenceNumber uint64 // Sequence number + Memo string // Memo +} + +// Call executes a contract call on the blockchain. +func (c *Client) Call(cfg CallCfg) (*ctypes.ResultBroadcastTxCommit, error) { + // Validate required client fields. + if err := c.validateSigner(); err != nil { + return nil, errors.Wrap(err, "validate signer") + } + if err := c.validateRPCClient(); err != nil { + return nil, errors.Wrap(err, "validate RPC client") + } + + pkgPath := cfg.PkgPath + funcName := cfg.FuncName + args := cfg.Args + gasWanted := cfg.GasWanted + gasFee := cfg.GasFee + send := cfg.Send + sequenceNumber := cfg.SequenceNumber + accountNumber := cfg.AccountNumber + memo := cfg.Memo + + // Validate config. + if pkgPath == "" { + return nil, errors.New("missing PkgPath") + } + if funcName == "" { + return nil, errors.New("missing FuncName") + } + + // Parse send amount. + sendCoins, err := std.ParseCoins(send) + if err != nil { + return nil, errors.Wrap(err, "parsing send coins") + } + + // Parse gas wanted & fee. + gasFeeCoins, err := std.ParseCoin(gasFee) + if err != nil { + return nil, errors.Wrap(err, "parsing gas fee coin") + } + + caller := c.Signer.Info().GetAddress() + + // Construct message & transaction and marshal. + msg := vm.MsgCall{ + Caller: caller, + Send: sendCoins, + PkgPath: pkgPath, + Func: funcName, + Args: args, + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(gasWanted, gasFeeCoins), + Signatures: nil, + Memo: memo, + } + + return c.signAndBroadcastTxCommit(tx, accountNumber, sequenceNumber) +} + +// signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result. +func (c Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { + caller := c.Signer.Info().GetAddress() + + if sequenceNumber == 0 || accountNumber == 0 { + account, _, err := c.QueryAccount(caller.String()) + if err != nil { + return nil, errors.Wrap(err, "query account") + } + accountNumber = account.AccountNumber + sequenceNumber = account.Sequence + } + + signCfg := SignCfg{ + UnsignedTX: tx, + SequenceNumber: sequenceNumber, + AccountNumber: accountNumber, + } + signedTx, err := c.Signer.Sign(signCfg) + if err != nil { + return nil, errors.Wrap(err, "sign") + } + + bz, err := amino.Marshal(signedTx) + if err != nil { + return nil, errors.Wrap(err, "marshaling tx binary bytes") + } + + bres, err := c.RPCClient.BroadcastTxCommit(bz) + if err != nil { + return nil, errors.Wrap(err, "broadcasting bytes") + } + + if bres.CheckTx.IsErr() { + return bres, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + } + if bres.DeliverTx.IsErr() { + return bres, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + } + + return bres, nil +} + +// TODO: Add more functionality, examples, and unit tests. From 208c61fbb1c2c91392e40dfa01ba7d1f327e7548 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 14 Sep 2023 03:55:13 +0200 Subject: [PATCH 15/24] chore: wip Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoclient/client_queries.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go index 1f2edf4b3cf..b7c51208238 100644 --- a/gno.land/pkg/gnoclient/client_queries.go +++ b/gno.land/pkg/gnoclient/client_queries.go @@ -56,3 +56,20 @@ func (c Client) QueryAccount(addr string) (*std.BaseAccount, *ctypes.ResultABCIQ return &qret.BaseAccount, qres, nil } + +func (c Client) QueryAppVersion() (string, *ctypes.ResultABCIQuery, error) { + if err := c.validateRPCClient(); err != nil { + return "", nil, err + } + + path := ".app/version" + data := []byte{} + + qres, err := c.RPCClient.ABCIQuery(path, data) + if err != nil { + return "", nil, errors.Wrap(err, "query account") + } + + version := string(qres.Response.Value) + return version, qres, nil +} From 60e05e83f57558843c0808f78500b6a51b2a22c1 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 15 Sep 2023 09:49:52 +0200 Subject: [PATCH 16/24] Update example_test.go --- gno.land/pkg/gnoclient/example_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gno.land/pkg/gnoclient/example_test.go b/gno.land/pkg/gnoclient/example_test.go index 65f6b8aafbd..a1be4481d2b 100644 --- a/gno.land/pkg/gnoclient/example_test.go +++ b/gno.land/pkg/gnoclient/example_test.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/gnolang/gno/gno.land/pkg/gnoclient" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) @@ -18,7 +18,7 @@ func Example_withDisk() { } remote := "127.0.0.1:26657" - rpcClient := client.NewHTTP(remote, "/websocket") + rpcClient := rpcclient.NewHTTP(remote, "/websocket") client := gnoclient.Client{ Signer: signer, @@ -36,7 +36,7 @@ func Example_withInMemCrypto() { signer, _ := gnoclient.SignerFromBip39(mnemo, bip39Passphrase, account, index) remote := "127.0.0.1:26657" - rpcClient := client.NewHTTP(remote, "/websocket") + rpcClient := rpcclient.NewHTTP(remote, "/websocket") client := gnoclient.Client{ Signer: signer, @@ -51,7 +51,7 @@ func Example_withInMemCrypto() { // Example_readOnly demonstrates how to initialize a read-only gnoclient, which can only query. func Example_readOnly() { remote := "127.0.0.1:26657" - rpcClient := client.NewHTTP(remote, "/websocket") + rpcClient := rpcclient.NewHTTP(remote, "/websocket") client := gnoclient.Client{ RPCClient: rpcClient, From 35e86bd1f18b6c4a14f7cce45641f1767b7fe277 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 23 Nov 2023 09:35:38 +0100 Subject: [PATCH 17/24] chore: In gnoclient.Client, add methods Render and QEval. Signed-off-by: Jeff Thompson --- gno.land/pkg/gnoclient/client_queries.go | 41 +++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go index b7c51208238..b20000fdd19 100644 --- a/gno.land/pkg/gnoclient/client_queries.go +++ b/gno.land/pkg/gnoclient/client_queries.go @@ -67,9 +67,48 @@ func (c Client) QueryAppVersion() (string, *ctypes.ResultABCIQuery, error) { qres, err := c.RPCClient.ABCIQuery(path, data) if err != nil { - return "", nil, errors.Wrap(err, "query account") + return "", nil, errors.Wrap(err, "query app version") } version := string(qres.Response.Value) return version, qres, nil } + +// Render calls the Render function for pkgPath with optional args. The pkgPath should +// include the prefix like "gno.land/". This is similar to using a browser URL +// /: where doesn't have the prefix like "gno.land/". +func (c Client) Render(pkgPath string, args string) (string, *ctypes.ResultABCIQuery, error) { + if err := c.validateRPCClient(); err != nil { + return "", nil, err + } + + path := "vm/qrender" + data := []byte(fmt.Sprintf("%s\n%s", pkgPath, args)) + + qres, err := c.RPCClient.ABCIQuery(path, data) + if err != nil { + return "", nil, errors.Wrap(err, "query render") + } + + return string(qres.Response.Data), qres, nil +} + +// QEval evaluates the given expression with the realm code at pkgPath. The pkgPath should +// include the prefix like "gno.land/". The expression is usually a function call like +// "GetBoardIDFromName(\"testboard\")". The return value is a typed expression like +// "(1 gno.land/r/demo/boards.BoardID)\n(true bool)". +func (c Client) QEval(pkgPath string, expression string) (string, *ctypes.ResultABCIQuery, error) { + if err := c.validateRPCClient(); err != nil { + return "", nil, err + } + + path := "vm/qeval" + data := []byte(fmt.Sprintf("%s\n%s", pkgPath, expression)) + + qres, err := c.RPCClient.ABCIQuery(path, data) + if err != nil { + return "", nil, errors.Wrap(err, "query qeval") + } + + return string(qres.Response.Data), qres, nil +} From dcb7d4398f57453f42fd4594e75274338791eacc Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 23 Nov 2023 09:37:39 +0100 Subject: [PATCH 18/24] chore: In gnoclient.Signer.Validate, sign a blank transaction to verify if the password unlocks the account Signed-off-by: Jeff Thompson --- gno.land/pkg/gnoclient/signer.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go index 098af742e4d..5eba124f2a8 100644 --- a/gno.land/pkg/gnoclient/signer.go +++ b/gno.land/pkg/gnoclient/signer.go @@ -3,6 +3,7 @@ package gnoclient import ( "fmt" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/std" @@ -33,7 +34,20 @@ func (s SignerFromKeybase) Validate() error { return err } - // TODO: Also verify if the password unlocks the account. + // To verify if the password unlocks the account, sign a blank transaction. + msg := vm.MsgCall{ + Caller: s.Info().GetAddress(), + } + signCfg := SignCfg{ + UnsignedTX: std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(0, std.NewCoin("ugnot", 1000000)), + }, + } + if _, err = s.Sign(signCfg); err != nil { + return err + } + return nil } From 8abebbe2b20523b1891e9ef872daecb930085080 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 23 Nov 2023 09:39:46 +0100 Subject: [PATCH 19/24] chore: In gnoclient.Client.QueryAccount, param addr should be crypto.Address, not string Signed-off-by: Jeff Thompson --- gno.land/pkg/gnoclient/client_queries.go | 8 ++++++-- gno.land/pkg/gnoclient/client_txs.go | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go index b20000fdd19..ce03305d503 100644 --- a/gno.land/pkg/gnoclient/client_queries.go +++ b/gno.land/pkg/gnoclient/client_queries.go @@ -6,6 +6,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -35,18 +36,21 @@ func (c Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { } // QueryAccount retrieves account information for a given address. -func (c Client) QueryAccount(addr string) (*std.BaseAccount, *ctypes.ResultABCIQuery, error) { +func (c Client) QueryAccount(addr crypto.Address) (*std.BaseAccount, *ctypes.ResultABCIQuery, error) { if err := c.validateRPCClient(); err != nil { return nil, nil, err } - path := fmt.Sprintf("auth/accounts/%s", addr) + path := fmt.Sprintf("auth/accounts/%s", crypto.AddressToBech32(addr)) data := []byte{} qres, err := c.RPCClient.ABCIQuery(path, data) if err != nil { return nil, nil, errors.Wrap(err, "query account") } + if qres.Response.Data == nil || len(qres.Response.Data) == 0 || string(qres.Response.Data) == "null" { + return nil, nil, std.ErrUnknownAddress("unknown address: " + crypto.AddressToBech32(addr)) + } var qret struct{ BaseAccount std.BaseAccount } err = amino.UnmarshalJSON(qres.Response.Data, &qret) diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go index cb779891e16..ab475154fad 100644 --- a/gno.land/pkg/gnoclient/client_txs.go +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -86,7 +86,7 @@ func (c Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumbe caller := c.Signer.Info().GetAddress() if sequenceNumber == 0 || accountNumber == 0 { - account, _, err := c.QueryAccount(caller.String()) + account, _, err := c.QueryAccount(caller) if err != nil { return nil, errors.Wrap(err, "query account") } From e2537eb558239428ba5e963ed89d51983c35447c Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:15:27 +0100 Subject: [PATCH 20/24] feat: add basic integration test to gnoclient Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoclient/client_test.go | 42 +++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go index 17ec73a8fb4..beef253c8dc 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -1,15 +1,51 @@ package gnoclient import ( + "fmt" "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/integration" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/jaekwon/testify/require" ) +func newInMemorySigner(t *testing.T, chainid string) *SignerFromKeybase { + t.Helper() + + mmeonic := integration.DefaultAccount_Seed + name := integration.DefaultAccount_Name + + kb := keys.NewInMemory() + _, err := kb.CreateAccount(name, mmeonic, "", "", uint32(0), uint32(0)) + require.NoError(t, err) + + return &SignerFromKeybase{ + Keybase: kb, // Stores keys in memory or on disk + Account: name, // Account name or bech32 format + Password: "", // Password for encryption + ChainID: chainid, // Chain ID for transaction signing + } +} + func TestClient_Request(t *testing.T) { + config, _ := integration.TestingNodeConfig(t, gnoland.MustGuessGnoRootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) + defer node.Stop() + + signer := newInMemorySigner(t, config.TMConfig.ChainID()) + client := Client{ - // Remote: "localhost:12345", - // ChainID: "test", + Signer: signer, + RPCClient: rpcclient.NewHTTP(remoteAddr, "/websocket"), } - _ = client + + data, res, err := client.Render("gno.land/r/demo/boards", "") + require.NoError(t, err) + fmt.Println("data", data) + fmt.Println("res", res) // TODO: xxx } From 89f112c396899abf626ac7a53947a5bc8bde303e Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:20:38 +0100 Subject: [PATCH 21/24] chore: add test placeholder Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoclient/client_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go index beef253c8dc..21ed089a238 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -44,8 +44,11 @@ func TestClient_Request(t *testing.T) { data, res, err := client.Render("gno.land/r/demo/boards", "") require.NoError(t, err) - fmt.Println("data", data) - fmt.Println("res", res) - // TODO: xxx + // XXX: need more test + + // XXX: need validation + fmt.Println("data:", data) + fmt.Println("res: ", res) + require.FailNow(t, "forcing failure: replace this by a real test") } From f1059c7a31efcd1353e91140f018352a00162776 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 30 Nov 2023 16:19:44 +0100 Subject: [PATCH 22/24] fix: In gnoclient Render and QEval, need to check Response.Error Signed-off-by: Jeff Thompson --- gno.land/pkg/gnoclient/client_queries.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go index ce03305d503..ba63c0d543e 100644 --- a/gno.land/pkg/gnoclient/client_queries.go +++ b/gno.land/pkg/gnoclient/client_queries.go @@ -93,6 +93,9 @@ func (c Client) Render(pkgPath string, args string) (string, *ctypes.ResultABCIQ if err != nil { return "", nil, errors.Wrap(err, "query render") } + if qres.Response.Error != nil { + return "", nil, errors.Wrap(qres.Response.Error, "Render failed: log:%s", qres.Response.Log) + } return string(qres.Response.Data), qres, nil } @@ -113,6 +116,9 @@ func (c Client) QEval(pkgPath string, expression string) (string, *ctypes.Result if err != nil { return "", nil, errors.Wrap(err, "query qeval") } + if qres.Response.Error != nil { + return "", nil, errors.Wrap(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log) + } return string(qres.Response.Data), qres, nil } From 5f0290f6044ce59d7a32d0908f8d93d559aadf2a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:43:18 +0100 Subject: [PATCH 23/24] fix: fix rebase Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoclient/client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go index 21ed089a238..e1b59d2d466 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/log" @@ -31,7 +31,7 @@ func newInMemorySigner(t *testing.T, chainid string) *SignerFromKeybase { } func TestClient_Request(t *testing.T) { - config, _ := integration.TestingNodeConfig(t, gnoland.MustGuessGnoRootDir()) + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) defer node.Stop() From 19f90c152c53f223c421d4546b42ad563ea3ee9c Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:26:37 +0100 Subject: [PATCH 24/24] fix: update simple test Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/pkg/gnoclient/client_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go index e1b59d2d466..418a95aa997 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -1,7 +1,6 @@ package gnoclient import ( - "fmt" "testing" "github.com/gnolang/gno/gno.land/pkg/integration" @@ -44,11 +43,10 @@ func TestClient_Request(t *testing.T) { data, res, err := client.Render("gno.land/r/demo/boards", "") require.NoError(t, err) + require.NotEmpty(t, data) - // XXX: need more test + require.NotNil(t, res) + require.NotEmpty(t, res.Response.Data) - // XXX: need validation - fmt.Println("data:", data) - fmt.Println("res: ", res) - require.FailNow(t, "forcing failure: replace this by a real test") + // XXX: need more test }