diff --git a/.gitignore b/.gitignore index 03dff11ba..185bb48ff 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,8 @@ mockdata/ docs/static dist coverage.txt +coverage.html temp +temp* +txout.json +vote.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 840764d63..86d6aba82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Non-breaking/Compatible Improvements - [#1818](https://github.com/NibiruChain/nibiru/pull/1818) - feat: add pebbledb support +- [#1859](https://github.com/NibiruChain/nibiru/pull/1859) - refactor(oracle): add oracle slashing events +- [#1893](https://github.com/NibiruChain/nibiru/pull/1893) - feat(gosdk): migrate Go-sdk into the Nibiru blockchain repo. ### Dependencies diff --git a/app/server/start.go b/app/server/start.go index 274f31fa6..0447b85c7 100644 --- a/app/server/start.go +++ b/app/server/start.go @@ -232,7 +232,6 @@ func startStandAlone(ctx *server.Context, opts StartOptions) error { traceWriterFile := ctx.Viper.GetString(TraceStore) traceWriter, err := openTraceWriter(traceWriterFile) - if err != nil { return err } diff --git a/app/server/util.go b/app/server/util.go index a604e1d13..633396e16 100644 --- a/app/server/util.go +++ b/app/server/util.go @@ -51,7 +51,7 @@ func AddCommands( sdkserver.NewRollbackCmd(opts.AppCreator, opts.DefaultNodeHome), // custom tx indexer command - //NewIndexTxCmd(), TODO: check indexer tx command + // NewIndexTxCmd(), TODO: check indexer tx command ) } diff --git a/gosdk/README.md b/gosdk/README.md new file mode 100644 index 000000000..0b9e67583 --- /dev/null +++ b/gosdk/README.md @@ -0,0 +1,61 @@ +# Nibiru Go SDK - NibiruChain/nibiru/gosdk + +A Golang client for interacting with the Nibiru blockchain. + +The Nibiru Go SDK extends the core blockchain logic with extensions to build +external clients for the Nibiru blockchain and easily access its query and +transaction types. + +--- + +## Dev Notes - Nibiru Go SDK + +### Finalizing "v1" + +- [ ] Migrate to the [Nibiru repo](https://github.com/NibiruChain/nibiru) and archive this one. +- [ ] feat: add in transaction broadcasting + - [x] initialize keyring obj with mnemonic + - [x] initialize keyring obj with priv key + - [x] write a test that sends a bank transfer. + - [ ] write a test that submits a text gov proposal. +- [x] docs: for grpc.go +- [ ] docs: for clients.go + +### Usage Guides & Dev Ex + +- [ ] Create a quick start guide: Connecting to Nibiru, querying Nibiru, sending +transactions on Nibiru +- [ ] Write usage examples + - [ ] Creating an account and keyring + - [ ] Querying balanaces + - [ ] Broadcasting txs to transfer funds + - [ ] Querying Wasm smart contracts + - [ ] Broadcasting txs to transfer funds +- [x] impl Tendermint RPC client +- [x] refactor: DRY improvements on the QueryClient initialization +- [x] ci: Add go tests to CI +- [x] ci: Add code coverage to CI +- [x] ci: Add linting to CI + +### Feature Backlog + +- [ ] impl wallet abstraction for the keyring +- [ ] epic: storing transaction history storage + +### Question Brain-dump + +Q: Should gosdk run as a binary? + +No, or at least, not initially. Since the software required to operate a full +node has more cumbersome dependencies like RocksDB that involve C-Go and compled +build steps, we may benefit from splitting "start" command from the bulk of the +subcommands available on teh Nibiru CLI. This would make it much easier to have a +command line tool that builds on Linux, Windows, Mac. + +Q: Should there be a way to run queries with JSON-RPC 2 instead of GRPC? + +We [implemented this in +python](https://github.com/NibiruChain/py-sdk/tree/v0.21.12/nibiru/jsonrpc) +without too much trouble, and it's not taxing to maintain. If we're going to +prioritize adding APIs for the CometBFT JSON-RPC methods, it should be in the +[Nibiru TypeScript SDK](https://github.com/NibiruChain/ts-sdk) first. diff --git a/gosdk/broadcast.go b/gosdk/broadcast.go new file mode 100644 index 000000000..4d1d3e63b --- /dev/null +++ b/gosdk/broadcast.go @@ -0,0 +1,198 @@ +package gosdk + +import ( + "context" + + cmtrpc "github.com/cometbft/cometbft/rpc/client" + sdkclient "github.com/cosmos/cosmos-sdk/client" + sdkclienttx "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + sdktypestx "github.com/cosmos/cosmos-sdk/types/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "google.golang.org/grpc" + + "github.com/NibiruChain/nibiru/x/common" + "github.com/NibiruChain/nibiru/x/common/denoms" +) + +func BroadcastMsgsWithSeq( + args BroadcastArgs, + from sdk.AccAddress, + seq uint64, + msgs ...sdk.Msg, +) (*sdk.TxResponse, error) { + broadcaster := args.Broadcaster + + info, err := args.kring.KeyByAddress(from) + if err != nil { + return nil, err + } + + txBuilder := args.txCfg.NewTxBuilder() + err = txBuilder.SetMsgs(msgs...) + if err != nil { + return nil, err + } + + bondDenom := denoms.NIBI + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(bondDenom, sdk.NewInt(1000)))) + txBuilder.SetGasLimit(uint64(2 * common.TO_MICRO)) + + nums, err := args.gosdk.GetAccountNumbers(from.String()) + if err != nil { + return nil, err + } + + var accRetriever sdkclient.AccountRetriever = authtypes.AccountRetriever{} + txFactory := sdkclienttx.Factory{}. + WithChainID(args.chainID). + WithKeybase(args.kring). + WithTxConfig(args.txCfg). + WithAccountRetriever(accRetriever). + WithAccountNumber(nums.Number). + WithSequence(seq) + + overwriteSig := true + err = sdkclienttx.Sign(txFactory, info.Name, txBuilder, overwriteSig) + if err != nil { + return nil, err + } + + txBytes, err := args.txCfg.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return nil, err + } + + return broadcaster.BroadcastTxSync(txBytes) +} + +func BroadcastMsgs( + args BroadcastArgs, + from sdk.AccAddress, + msgs ...sdk.Msg, +) (*sdk.TxResponse, error) { + nums, err := args.gosdk.GetAccountNumbers(from.String()) + if err != nil { + return nil, err + } + return BroadcastMsgsWithSeq(args, from, nums.Sequence, msgs...) +} + +type Broadcaster interface { + BroadcastTxSync(txBytes []byte) (*sdk.TxResponse, error) +} + +var ( + _ Broadcaster = (*BroadcasterTmRpc)(nil) + _ Broadcaster = (*BroadcasterGrpc)(nil) +) + +type BroadcasterTmRpc struct { + RPC cmtrpc.Client +} + +func (b BroadcasterTmRpc) BroadcastTxSync( + txBytes []byte, +) (*sdk.TxResponse, error) { + respRaw, err := b.RPC.BroadcastTxSync(context.Background(), txBytes) + if err != nil { + return nil, err + } + + return sdk.NewResponseFormatBroadcastTx(respRaw), err +} + +type BroadcasterGrpc struct { + GRPC *grpc.ClientConn +} + +func (b BroadcasterGrpc) BroadcastTx( + txBytes []byte, mode sdktypestx.BroadcastMode, +) (*sdk.TxResponse, error) { + txClient := sdktypestx.NewServiceClient(b.GRPC) + respRaw, err := txClient.BroadcastTx( + context.Background(), &sdktypestx.BroadcastTxRequest{ + TxBytes: txBytes, + Mode: mode, + }) + return respRaw.TxResponse, err +} + +func (b BroadcasterGrpc) BroadcastTxSync( + txBytes []byte, +) (*sdk.TxResponse, error) { + return b.BroadcastTx(txBytes, sdktypestx.BroadcastMode_BROADCAST_MODE_SYNC) +} + +func (b BroadcasterGrpc) BroadcastTxAsync( + txBytes []byte, +) (*sdk.TxResponse, error) { + return b.BroadcastTx(txBytes, sdktypestx.BroadcastMode_BROADCAST_MODE_ASYNC) +} + +// func GetTxBytes() ([]byte, error) { +// return txBytes, err +// } + +type BroadcastArgs struct { + kring keyring.Keyring + txCfg sdkclient.TxConfig + gosdk NibiruSDK + // clientCtx sdkclient.Context // TODO: implement + Broadcaster Broadcaster + rpc cmtrpc.Client + chainID string +} + +func initBroadcastArgs( + nc *NibiruSDK, broadcaster Broadcaster, +) (args BroadcastArgs) { + txConfig := nc.EncCfg.TxConfig + return BroadcastArgs{ + kring: nc.Keyring, + txCfg: txConfig, + gosdk: *nc, + Broadcaster: broadcaster, + rpc: nc.CometRPC, + chainID: nc.ChainId, + } +} + +func (nc *NibiruSDK) BroadcastMsgs( + from sdk.AccAddress, + msgs ...sdk.Msg, +) (*sdk.TxResponse, error) { + broadcaster := BroadcasterTmRpc{RPC: nc.CometRPC} + args := initBroadcastArgs(nc, broadcaster) + return BroadcastMsgs(args, from, msgs...) +} + +func (nc *NibiruSDK) BroadcastMsgsWithSeq( + from sdk.AccAddress, + seq uint64, + msgs ...sdk.Msg, +) (*sdk.TxResponse, error) { + broadcaster := BroadcasterTmRpc{RPC: nc.CometRPC} + args := initBroadcastArgs(nc, broadcaster) + return BroadcastMsgsWithSeq(args, from, seq, msgs...) +} + +func (nc *NibiruSDK) BroadcastMsgsGrpc( + from sdk.AccAddress, + msgs ...sdk.Msg, +) (*sdk.TxResponse, error) { + broadcaster := BroadcasterGrpc{GRPC: nc.Querier.ClientConn} + args := initBroadcastArgs(nc, broadcaster) + return BroadcastMsgs(args, from, msgs...) +} + +func (nc *NibiruSDK) BroadcastMsgsGrpcWithSeq( + from sdk.AccAddress, + seq uint64, + msgs ...sdk.Msg, +) (*sdk.TxResponse, error) { + broadcaster := BroadcasterGrpc{GRPC: nc.Querier.ClientConn} + args := initBroadcastArgs(nc, broadcaster) + return BroadcastMsgsWithSeq(args, from, seq, msgs...) +} diff --git a/gosdk/clientctx.go b/gosdk/clientctx.go new file mode 100644 index 000000000..27c3c007d --- /dev/null +++ b/gosdk/clientctx.go @@ -0,0 +1,42 @@ +package gosdk + +// import ( +// "github.com/NibiruChain/nibiru/app" +// sdkclient "github.com/cosmos/cosmos-sdk/client" +// authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +// ) + +// TODO: https://github.com/NibiruChain/nibiru/issues/1894 +// Make a way to instantiate a NibiruSDK from a (*cli.Network, *cli.Validator) + +// ClientCtx: Docs for args +// +// - tmCfgRootDir: /node0/simd +// - Validator.Dir: /node0 +// - Validator.ClientCtx.KeyringDir: /node0/simcli + +// func NewNibiruSDKFromClientCtx( +// clientCtx sdkclient.Context, grpcUrl, cometRpcUrl string, +// ) (gosdk NibiruSDK, err error) { +// grpcConn, err := GetGRPCConnection(grpcUrl, true, 5) +// if err != nil { +// return +// } +// cometRpc, err := NewRPCClient(cometRpcUrl, "/websocket") +// if err != nil { +// return +// } +// querier, err := NewQuerier(grpcConn) +// if err != nil { +// return +// } +// return NibiruSDK{ +// ChainId: clientCtx.ChainID, +// Keyring: clientCtx.Keyring, +// EncCfg: app.MakeEncodingConfig(), +// Querier: querier, +// CometRPC: cometRpc, +// AccountRetriever: authtypes.AccountRetriever{}, +// GrpcClient: grpcConn, +// }, err +// } diff --git a/gosdk/export_test.go b/gosdk/export_test.go new file mode 100644 index 000000000..533afab68 --- /dev/null +++ b/gosdk/export_test.go @@ -0,0 +1,84 @@ +package gosdk + +import ( + "testing" + + "google.golang.org/grpc" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/common/testutil/cli" + "github.com/NibiruChain/nibiru/x/common/testutil/genesis" + + tmconfig "github.com/cometbft/cometbft/config" + serverconfig "github.com/cosmos/cosmos-sdk/server/config" +) + +type Blockchain struct { + GrpcConn *grpc.ClientConn + Cfg *cli.Config + Network *cli.Network + Val *cli.Validator +} + +func CreateBlockchain(t *testing.T) (nibiru Blockchain, err error) { + EnsureNibiruPrefix() + encConfig := app.MakeEncodingConfig() + genState := genesis.NewTestGenesisState(encConfig) + cliCfg := cli.BuildNetworkConfig(genState) + cfg := &cliCfg + cfg.NumValidators = 1 + + network, err := cli.New( + t, + t.TempDir(), + *cfg, + ) + if err != nil { + return nibiru, err + } + err = network.WaitForNextBlock() + if err != nil { + return nibiru, err + } + + val := network.Validators[0] + AbsorbServerConfig(cfg, val.AppConfig) + AbsorbTmConfig(cfg, val.Ctx.Config) + + grpcConn, err := ConnectGrpcToVal(val) + if err != nil { + return nibiru, err + } + return Blockchain{ + GrpcConn: grpcConn, + Cfg: cfg, + Network: network, + Val: val, + }, err +} + +func ConnectGrpcToVal(val *cli.Validator) (*grpc.ClientConn, error) { + grpcUrl := val.AppConfig.GRPC.Address + return GetGRPCConnection( + grpcUrl, true, 5, + ) +} + +func AbsorbServerConfig( + cfg *cli.Config, srvCfg *serverconfig.Config, +) *cli.Config { + cfg.GRPCAddress = srvCfg.GRPC.Address + cfg.APIAddress = srvCfg.API.Address + return cfg +} + +func AbsorbTmConfig( + cfg *cli.Config, tmCfg *tmconfig.Config, +) *cli.Config { + cfg.RPCAddress = tmCfg.RPC.ListenAddress + return cfg +} + +func (chain *Blockchain) TmRpcEndpoint() string { + return chain.Val.RPCAddress +} diff --git a/gosdk/gosdk.go b/gosdk/gosdk.go new file mode 100644 index 000000000..8014afb50 --- /dev/null +++ b/gosdk/gosdk.go @@ -0,0 +1,117 @@ +package gosdk + +import ( + "context" + "encoding/hex" + + cmtrpcclient "github.com/cometbft/cometbft/rpc/client" + cmtcoretypes "github.com/cometbft/cometbft/rpc/core/types" + "google.golang.org/grpc" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/app/appconst" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + csdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +type NibiruSDK struct { + ChainId string + Keyring keyring.Keyring + EncCfg app.EncodingConfig + Querier Querier + CometRPC cmtrpcclient.Client + AccountRetriever authtypes.AccountRetriever + GrpcClient *grpc.ClientConn +} + +func NewNibiruSdk( + chainId string, + grpcConn *grpc.ClientConn, + rpcEndpt string, +) (NibiruSDK, error) { + EnsureNibiruPrefix() + encCfg := app.MakeEncodingConfig() + keyring := keyring.NewInMemory(encCfg.Codec) + queryClient, err := NewQuerier(grpcConn) + if err != nil { + return NibiruSDK{}, err + } + cometRpc, err := NewRPCClient(rpcEndpt, "/websocket") + if err != nil { + return NibiruSDK{}, err + } + return NibiruSDK{ + ChainId: chainId, + Keyring: keyring, + EncCfg: encCfg, + Querier: queryClient, + CometRPC: cometRpc, + AccountRetriever: authtypes.AccountRetriever{}, + GrpcClient: grpcConn, + }, err +} + +func EnsureNibiruPrefix() { + csdkConfig := csdk.GetConfig() + nibiruPrefix := appconst.AccountAddressPrefix + if csdkConfig.GetBech32AccountAddrPrefix() != nibiruPrefix { + app.SetPrefixes(nibiruPrefix) + } +} + +func (nc *NibiruSDK) TxByHash(txHashHex string) (*cmtcoretypes.ResultTx, error) { + goCtx := context.Background() + txHashBz, err := TxHashHexToBytes(txHashHex) + if err != nil { + return nil, err + } + prove := true + res, err := nc.CometRPC.Tx(goCtx, txHashBz, prove) + return res, err +} + +func TxHashHexToBytes(txHashHex string) ([]byte, error) { + return hex.DecodeString(txHashHex) +} + +func TxHashBytesToHex(txHashBz []byte) (txHashHex string) { + return hex.EncodeToString(txHashBz) +} + +type AccountNumbers struct { + Number uint64 + Sequence uint64 +} + +func GetAccountNumbers( + address string, + grpcConn *grpc.ClientConn, + encCfg app.EncodingConfig, +) (nums AccountNumbers, err error) { + queryClient := authtypes.NewQueryClient(grpcConn) + resp, err := queryClient.Account(context.Background(), &authtypes.QueryAccountRequest{ + Address: address, + }) + if err != nil { + return nums, err + } + + // register auth interface + var acc authtypes.AccountI + if err := encCfg.InterfaceRegistry.UnpackAny(resp.Account, &acc); err != nil { + return nums, err + } + + return AccountNumbers{ + Number: acc.GetAccountNumber(), + Sequence: acc.GetSequence(), + }, err +} + +func (nc *NibiruSDK) GetAccountNumbers( + address string, +) (nums AccountNumbers, err error) { + return GetAccountNumbers(address, nc.Querier.ClientConn, nc.EncCfg) +} diff --git a/gosdk/gosdk_test.go b/gosdk/gosdk_test.go new file mode 100644 index 000000000..d14cc44b2 --- /dev/null +++ b/gosdk/gosdk_test.go @@ -0,0 +1,164 @@ +package gosdk_test + +import ( + "fmt" + "strconv" + "testing" + + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" + + "github.com/NibiruChain/nibiru/gosdk" + "github.com/NibiruChain/nibiru/x/common/denoms" + "github.com/NibiruChain/nibiru/x/common/testutil" + "github.com/NibiruChain/nibiru/x/common/testutil/cli" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +// -------------------------------------------------- +// NibiruClientSuite +// -------------------------------------------------- + +var _ suite.SetupAllSuite = (*TestSuite)(nil) + +type TestSuite struct { + suite.Suite + + nibiruSdk *gosdk.NibiruSDK + grpcConn *grpc.ClientConn + cfg *cli.Config + network *cli.Network + val *cli.Validator +} + +func TestNibiruClientTestSuite_RunAll(t *testing.T) { + suite.Run(t, new(TestSuite)) +} + +func (s *TestSuite) RPCEndpoint() string { + return s.val.RPCAddress +} + +// SetupSuite implements the suite.SetupAllSuite interface. This function runs +// prior to all of the other tests in the suite. +func (s *TestSuite) SetupSuite() { + // testutil.BeforeIntegrationSuite(s.T()) + + nibiru, err := gosdk.CreateBlockchain(s.T()) + s.NoError(err) + s.network = nibiru.Network + s.cfg = nibiru.Cfg + s.val = nibiru.Val + s.grpcConn = nibiru.GrpcConn +} + +func ConnectGrpcToVal(val *cli.Validator) (*grpc.ClientConn, error) { + grpcUrl := val.AppConfig.GRPC.Address + return gosdk.GetGRPCConnection( + grpcUrl, true, 5, + ) +} + +func (s *TestSuite) ConnectGrpc() { + grpcConn, err := ConnectGrpcToVal(s.val) + s.NoError(err) + s.NotNil(grpcConn) + s.grpcConn = grpcConn +} + +func (s *TestSuite) TestNewQueryClient() { + _, err := gosdk.NewQuerier(s.grpcConn) + s.NoError(err) +} + +func (s *TestSuite) TestNewNibiruSdk() { + rpcEndpt := s.val.RPCAddress + nibiruSdk, err := gosdk.NewNibiruSdk(s.cfg.ChainID, s.grpcConn, rpcEndpt) + s.NoError(err) + s.nibiruSdk = &nibiruSdk + + s.nibiruSdk.Keyring = s.val.ClientCtx.Keyring + s.T().Run("DoTestBroadcastMsgs", func(t *testing.T) { + s.DoTestBroadcastMsgs() + }) + s.T().Run("DoTestBroadcastMsgsGrpc", func(t *testing.T) { + s.NoError(s.network.WaitForNextBlock()) + s.DoTestBroadcastMsgsGrpc() + }) +} + +// FIXME: Q: What is the node home for a local validator? +func (s *TestSuite) UsefulPrints() { + tmCfgRootDir := s.val.Ctx.Config.RootDir + fmt.Printf("tmCfgRootDir: %v\n", tmCfgRootDir) + fmt.Printf("s.val.Dir: %v\n", s.val.Dir) + fmt.Printf("s.val.ClientCtx.KeyringDir: %v\n", s.val.ClientCtx.KeyringDir) +} + +func (s *TestSuite) AssertTxResponseSuccess(txResp *sdk.TxResponse) (txHashHex string) { + s.NotNil(txResp) + s.EqualValues(txResp.Code, 0) + return txResp.TxHash +} + +func (s *TestSuite) msgSendVars() (from, to sdk.AccAddress, amt sdk.Coins, msgSend sdk.Msg) { + from = s.val.Address + to = testutil.AccAddress() + amt = sdk.NewCoins(sdk.NewInt64Coin(denoms.NIBI, 420)) + msgSend = banktypes.NewMsgSend(from, to, amt) + return from, to, amt, msgSend +} + +func (s *TestSuite) DoTestBroadcastMsgs() (txHashHex string) { + from, _, _, msgSend := s.msgSendVars() + txResp, err := s.nibiruSdk.BroadcastMsgs( + from, msgSend, + ) + s.NoError(err) + return s.AssertTxResponseSuccess(txResp) +} + +func (s *TestSuite) DoTestBroadcastMsgsGrpc() (txHashHex string) { + from, _, _, msgSend := s.msgSendVars() + txResp, err := s.nibiruSdk.BroadcastMsgsGrpc( + from, msgSend, + ) + s.NoError(err) + txHashHex = s.AssertTxResponseSuccess(txResp) + + base := 10 + var txRespCode string = strconv.FormatUint(uint64(txResp.Code), base) + s.EqualValuesf(txResp.Code, 0, + "code: %v\nraw log: %s", txRespCode, txResp.RawLog) + return txHashHex +} + +func (s *TestSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + s.network.Cleanup() +} + +// -------------------------------------------------- +// NibiruClientSuite_NoNetwork +// -------------------------------------------------- + +type NibiruClientSuite_NoNetwork struct { + suite.Suite +} + +func TestNibiruClientSuite_NoNetwork_RunAll(t *testing.T) { + suite.Run(t, new(NibiruClientSuite_NoNetwork)) +} + +func (s *NibiruClientSuite_NoNetwork) TestGetGrpcConnection_NoNetwork() { + grpcConn, err := gosdk.GetGRPCConnection( + gosdk.DefaultNetworkInfo.GrpcEndpoint, true, 2, + ) + s.Error(err) + s.Nil(grpcConn) + + _, err = gosdk.NewQuerier(grpcConn) + s.Error(err) +} diff --git a/gosdk/grpc.go b/gosdk/grpc.go new file mode 100644 index 000000000..84a6c9871 --- /dev/null +++ b/gosdk/grpc.go @@ -0,0 +1,44 @@ +package gosdk + +import ( + "context" + "crypto/tls" + "fmt" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// GetGRPCConnection establishes a connection to a gRPC server using either +// secure (TLS) or insecure credentials. The function blocks until the connection +// is established or the specified timeout is reached. +func GetGRPCConnection( + grpcUrl string, grpcInsecure bool, timeoutSeconds int64, +) (*grpc.ClientConn, error) { + var creds credentials.TransportCredentials + if grpcInsecure { + creds = insecure.NewCredentials() + } else { + creds = credentials.NewTLS(&tls.Config{}) + } + + options := []grpc.DialOption{ + grpc.WithBlock(), + grpc.WithTransportCredentials(creds), + } + timeout := time.Duration(timeoutSeconds) * time.Second + ctx, cancel := context.WithTimeout( + context.Background(), timeout, + ) + defer cancel() + + conn, err := grpc.DialContext(ctx, grpcUrl, options...) + if err != nil { + return nil, fmt.Errorf( + "%w: Cannot connect to gRPC endpoint %s\n", err, grpcUrl) + } + + return conn, nil +} diff --git a/gosdk/keys.go b/gosdk/keys.go new file mode 100644 index 000000000..7d5daa167 --- /dev/null +++ b/gosdk/keys.go @@ -0,0 +1,83 @@ +package gosdk + +import ( + "github.com/cosmos/cosmos-sdk/crypto" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdktestutil "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/app/codec" + + "github.com/cosmos/cosmos-sdk/crypto/hd" +) + +func EncodingConfig() codec.EncodingConfig { return app.MakeEncodingConfig() } + +// NewKeyring: Creates an empty, in-memory keyring +func NewKeyring() keyring.Keyring { + return keyring.NewInMemory(EncodingConfig().Codec) +} + +// TODO: Is this needed? +// import ( +// "bufio" +// "os" +// "path/filepath" +// ) +// func NewKeyringLocal(nodeDir string) (keyring.Keyring) { +// clientDir := filepath.Join(nodeDir, "keyring") +// var cdc codec.Codec = EncodingConfig.Marshaler +// buf := bufio.NewReader(os.Stdin) +// return keyring.New( +// sdk.KeyringServiceName(), +// keyring.BackendTest, +// clientDir, +// buf, +// cdc, +// ) +// } + +func PrivKeyFromMnemonic( + kring keyring.Keyring, mnemonic string, keyName string, +) (cryptotypes.PrivKey, sdk.AccAddress, error) { + algo := hd.Secp256k1 + overwrite := true + addr, secret, err := sdktestutil.GenerateSaveCoinKey( + kring, keyName, mnemonic, overwrite, algo, + ) + if err != nil { + return &secp256k1.PrivKey{}, sdk.AccAddress{}, err + } + privKey := secp256k1.GenPrivKeyFromSecret([]byte(secret)) + return privKey, addr, err +} + +func CreateSigner( + mnemonic string, + kring keyring.Keyring, + keyName string, +) (kringRecord *keyring.Record, privKey cryptotypes.PrivKey, err error) { + privKey, _, err = PrivKeyFromMnemonic(kring, mnemonic, keyName) + if err != nil { + return kringRecord, privKey, err + } + kringRecord, err = CreateSignerFromPrivKey(privKey, keyName) + return kringRecord, privKey, err +} + +func CreateSignerFromPrivKey( + privKey cryptotypes.PrivKey, keyName string, +) (*keyring.Record, error) { + return keyring.NewLocalRecord(keyName, privKey, privKey.PubKey()) +} + +func AddSignerToKeyring( + kring keyring.Keyring, privKey cryptotypes.PrivKey, keyName string, +) error { + passphrase := "password" + armor := crypto.EncryptArmorPrivKey(privKey, passphrase, privKey.Type()) + return kring.ImportPrivKey(keyName, armor, passphrase) +} diff --git a/gosdk/keys_test.go b/gosdk/keys_test.go new file mode 100644 index 000000000..b790cfbb8 --- /dev/null +++ b/gosdk/keys_test.go @@ -0,0 +1,53 @@ +package gosdk_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/NibiruChain/nibiru/gosdk" +) + +const LOCALNET_VALIDATOR_MNEMONIC = "guard cream sadness conduct invite crumble clock pudding hole grit liar hotel maid produce squeeze return argue turtle know drive eight casino maze host" + +func TestCreateSigner(t *testing.T) { + testCases := []struct { + testName string + mnemonic string + expectErr bool + }{ + { + testName: "bad input", + mnemonic: "not a mnemonic", + expectErr: true, + }, + { + testName: "good input (localnet genesis)", + mnemonic: LOCALNET_VALIDATOR_MNEMONIC, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + kring := gosdk.NewKeyring() + keyName := "" + signer, privKey, err := gosdk.CreateSigner(tc.mnemonic, kring, keyName) + if tc.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, signer.PubKey) + + err = gosdk.AddSignerToKeyring(kring, privKey, privKey.PubKey().String()) + require.NoError(t, err) + }) + } +} + +func TestKeyring(t *testing.T) { + require.NotPanics(t, func() { + _ = gosdk.NewKeyring() + }) +} diff --git a/gosdk/netinfo.go b/gosdk/netinfo.go new file mode 100644 index 000000000..91c7e0095 --- /dev/null +++ b/gosdk/netinfo.go @@ -0,0 +1,17 @@ +package gosdk + +type NetworkInfo struct { + GrpcEndpoint string + LcdEndpoint string + TmRpcEndpoint string + WebsocketEndpoint string + ChainID string +} + +var DefaultNetworkInfo = NetworkInfo{ + GrpcEndpoint: "localhost:9090", + LcdEndpoint: "http://localhost:1317", + TmRpcEndpoint: "http://localhost:26657", + WebsocketEndpoint: "ws://localhost:26657/websocket", + ChainID: "nibiru-localnet-0", +} diff --git a/gosdk/querier.go b/gosdk/querier.go new file mode 100644 index 000000000..a9da79c4f --- /dev/null +++ b/gosdk/querier.go @@ -0,0 +1,52 @@ +package gosdk + +import ( + "errors" + + wasm "github.com/CosmWasm/wasmd/x/wasm/types" + "google.golang.org/grpc" + + devgas "github.com/NibiruChain/nibiru/x/devgas/v1/types" + epochs "github.com/NibiruChain/nibiru/x/epochs/types" + "github.com/NibiruChain/nibiru/x/evm" + inflation "github.com/NibiruChain/nibiru/x/inflation/types" + xoracle "github.com/NibiruChain/nibiru/x/oracle/types" + tokenfactory "github.com/NibiruChain/nibiru/x/tokenfactory/types" +) + +type Querier struct { + ClientConn *grpc.ClientConn + + // Smart Contracts + EVM evm.QueryClient + Wasm wasm.QueryClient + + // Other Modules + Devgas devgas.QueryClient + Epoch epochs.QueryClient + Inflation inflation.QueryClient + Oracle xoracle.QueryClient + TokenFactory tokenfactory.QueryClient +} + +func NewQuerier( + grpcConn *grpc.ClientConn, +) (Querier, error) { + if grpcConn == nil { + return Querier{}, errors.New( + "cannot create NibiruQueryClient with nil grpc.ClientConn") + } + + return Querier{ + ClientConn: grpcConn, + + EVM: evm.NewQueryClient(grpcConn), + Wasm: wasm.NewQueryClient(grpcConn), + + Devgas: devgas.NewQueryClient(grpcConn), + Epoch: epochs.NewQueryClient(grpcConn), + Inflation: inflation.NewQueryClient(grpcConn), + Oracle: xoracle.NewQueryClient(grpcConn), + TokenFactory: tokenfactory.NewQueryClient(grpcConn), + }, nil +} diff --git a/gosdk/rpc.go b/gosdk/rpc.go new file mode 100644 index 000000000..62cc549e9 --- /dev/null +++ b/gosdk/rpc.go @@ -0,0 +1,18 @@ +package gosdk + +import ( + cmtrpc "github.com/cometbft/cometbft/rpc/client" + cmtrpchttp "github.com/cometbft/cometbft/rpc/client/http" +) + +var _ cmtrpc.Client = (*cmtrpchttp.HTTP)(nil) + +// NewRPCClient: A remote Comet-BFT RPC client. An error is returned on +// invalid remote. The function panics when remote is nil. +// +// Args: +// - rpcEndpt: endpoint in the form ://: +// - websocket: websocket path (which always seems to be "/websocket") +func NewRPCClient(rpcEndpt string, websocket string) (*cmtrpchttp.HTTP, error) { + return cmtrpchttp.New(rpcEndpt, websocket) +} diff --git a/gosdk/sequence_test.go b/gosdk/sequence_test.go new file mode 100644 index 000000000..904256032 --- /dev/null +++ b/gosdk/sequence_test.go @@ -0,0 +1,78 @@ +package gosdk_test + +import ( + "encoding/json" + + "github.com/MakeNowJust/heredoc/v2" + cmtcoretypes "github.com/cometbft/cometbft/rpc/core/types" + + "github.com/NibiruChain/nibiru/gosdk" +) + +// TestSequenceExpectations validates the behavior of account sequence numbers +// and transaction finalization in a blockchain network. It ensures that sequence +// numbers increment correctly with each transaction and that transactions can be +// queried successfully after the blocks are completed. +func (s *TestSuite) TestSequenceExpectations() { + t := s.T() + t.Log("Get sequence and block") + // Go to next block + _, err := s.network.WaitForNextBlockVerbose() + s.Require().NoError(err) + + accAddr := s.val.Address + getLatestAccNums := func() gosdk.AccountNumbers { + accNums, err := s.nibiruSdk.GetAccountNumbers(accAddr.String()) + s.NoError(err) + return accNums + } + seq := getLatestAccNums().Sequence + + t.Logf("starting sequence %v should not change from waiting a block", seq) + s.NoError(s.network.WaitForNextBlock()) + newSeq := getLatestAccNums().Sequence + s.EqualValues(seq, newSeq) + + t.Log("broadcast msg n times, expect sequence += n") + numTxs := uint64(5) + seqs := []uint64{} + txResults := make(map[string]*cmtcoretypes.ResultTx) + for broadcastCount := uint64(0); broadcastCount < numTxs; broadcastCount++ { + s.NoError(s.network.WaitForNextBlock()) // Ensure block increment + + from, _, _, msgSend := s.msgSendVars() + txResp, err := s.nibiruSdk.BroadcastMsgsGrpcWithSeq( + from, + seq+broadcastCount, + msgSend, + ) + s.NoError(err) + txHashHex := s.AssertTxResponseSuccess(txResp) + + s.T().Log(heredoc.Docf( + `Query for tx %v should fail b/c it's the same block and finalization + cannot have possibly occurred yet.`, broadcastCount)) + txResult, err := s.nibiruSdk.TxByHash(txHashHex) + jsonBz, _ := json.MarshalIndent(txResp, "", " ") + s.Assert().Errorf(err, "txResp: %s", jsonBz) + + txResults[txHashHex] = txResult + seqs = append(seqs, getLatestAccNums().Sequence) + } + + s.T().Log("expect sequence += n") + newNewSeq := getLatestAccNums().Sequence + txResultsJson, _ := json.MarshalIndent(txResults, "", " ") + s.EqualValuesf(int(seq+numTxs-1), int(newNewSeq), "seqs: %v\ntxResults: %s", seqs, txResultsJson) + + s.T().Log("After the blocks are completed, tx queries by hash should work.") + for times := 0; times < 2; times++ { + s.NoError(s.network.WaitForNextBlock()) + } + + s.T().Log("Query each tx by hash (successfully)") + for txHashHex := range txResults { + _, err := s.nibiruSdk.TxByHash(txHashHex) + s.NoError(err) + } +} diff --git a/justfile b/justfile index 746366afc..af0ff7d44 100644 --- a/justfile +++ b/justfile @@ -67,7 +67,7 @@ stop: # Runs golang formatter (gofumpt) fmt: - gofumpt -w x app + gofumpt -w x app gosdk eth # Format and lint tidy: diff --git a/x/common/testutil/cli/network.go b/x/common/testutil/cli/network.go index cd2b5333f..4987ec944 100644 --- a/x/common/testutil/cli/network.go +++ b/x/common/testutil/cli/network.go @@ -118,7 +118,7 @@ type ( // listen for events, test if it also implements events.EventSwitch. // // RPCClient implementations in "github.com/cometbft/cometbft/rpc" v0.37.2: - // - rcp.HTTP + // - rpc.HTTP // - rpc.Local RPCClient tmclient.Client @@ -543,17 +543,23 @@ func (n *Network) WaitForHeightWithTimeout(h int64, t time.Duration) (int64, err // WaitForNextBlock waits for the next block to be committed, returning an error // upon failure. -func (n *Network) WaitForNextBlock() error { +func (n *Network) WaitForNextBlockVerbose() (int64, error) { lastBlock, err := n.LatestHeight() if err != nil { - return err + return -1, err } - _, err = n.WaitForHeight(lastBlock + 1) + newBlock := lastBlock + 1 + _, err = n.WaitForHeight(newBlock) if err != nil { - return err + return lastBlock, err } + return newBlock, err +} + +func (n *Network) WaitForNextBlock() error { + _, err := n.WaitForNextBlockVerbose() return err }