From 2945a0510e8f63660f09f788b2d42b48ba9c544c Mon Sep 17 00:00:00 2001 From: Karoly Albert Szabo Date: Sat, 8 Jun 2019 16:04:45 +0200 Subject: [PATCH] Contract test [GET] (#19) Closes: #17 Depends on: cosmos/cosmos-sdk#4470 --- .circleci/config.yml | 21 ++ .gitignore | 1 + Makefile | 35 ++- cmd/contract_tests/main.go | 43 +++ contrib/get_node.sh | 14 + dredd.yml | 33 +++ go.mod | 4 +- go.sum | 2 + lcd_test/helpers.go | 478 +++++++++++++++++++++++++++++++++ lcd_test/helpers_test.go | 401 +-------------------------- lcd_test/lcd_test.go | 173 +++++++----- lcd_test/testdata/setup.sh | 27 ++ lcd_test/testdata/state.tar.gz | Bin 0 -> 20266 bytes 13 files changed, 765 insertions(+), 467 deletions(-) create mode 100644 cmd/contract_tests/main.go create mode 100755 contrib/get_node.sh create mode 100644 dredd.yml create mode 100644 lcd_test/helpers.go create mode 100755 lcd_test/testdata/setup.sh create mode 100644 lcd_test/testdata/state.tar.gz diff --git a/.circleci/config.yml b/.circleci/config.yml index 0b910d44b6..a5668784c1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -391,6 +391,24 @@ jobs: - store_artifacts: path: /go/src/github.com/cosmos/gaia/gaia-windows-res.yml + contract_tests: + <<: *linux_defaults + steps: + - attach_workspace: + at: /tmp/workspace + - checkout + - setup_remote_docker: + docker_layer_caching: true + - run: + name: Get Node.js and test REST implementation against swagger documentation at https://cosmos.network/rpc/ + command: | + go get github.com/snikch/goodman/cmd/goodman + make build + make build-contract-tests-hooks + make setup-contract-tests-data + export PATH=~/.local/bin:$PATH + ./contrib/get_node.sh && make contract-tests + workflows: version: 2 test-suite: @@ -469,4 +487,7 @@ workflows: - master requires: - setup_dependencies + - contract_tests: + requires: + - setup_dependencies diff --git a/.gitignore b/.gitignore index d09b381271..96722f977c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ dependency-graph.png *.aux *.out *.synctex.gz +contract_tests/* diff --git a/Makefile b/Makefile index ce294bf82b..830765424b 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ PACKAGES_SIMTEST=$(shell go list ./... | grep '/simulation') VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//') COMMIT := $(shell git log -1 --format='%H') LEDGER_ENABLED ?= true +SDK_PACK := $(shell go list -m github.com/cosmos/cosmos-sdk | sed 's/ /\@/g') export GO111MODULE = on @@ -78,6 +79,13 @@ endif build-linux: go.sum LEDGER_ENABLED=false GOOS=linux GOARCH=amd64 $(MAKE) build +build-contract-tests-hooks: +ifeq ($(OS),Windows_NT) + go build -mod=readonly $(BUILD_FLAGS) -o build/contract_tests.exe ./cmd/contract_tests +else + go build -mod=readonly $(BUILD_FLAGS) -o build/contract_tests ./cmd/contract_tests +endif + install: go.sum check-ledger go install -mod=readonly $(BUILD_FLAGS) ./cmd/gaiad go install -mod=readonly $(BUILD_FLAGS) ./cmd/gaiacli @@ -86,6 +94,7 @@ install-debug: go.sum go install -mod=readonly $(BUILD_FLAGS) ./cmd/gaiadebug + ######################################## ### Tools & dependencies @@ -158,10 +167,34 @@ localnet-start: localnet-stop localnet-stop: docker-compose down +setup-contract-tests-data: + echo 'Prepare data for the contract tests' + rm -rf /tmp/contract_tests ; \ + mkdir /tmp/contract_tests ; \ + cp "${GOPATH}/pkg/mod/${SDK_PACK}/client/lcd/swagger-ui/swagger.yaml" /tmp/contract_tests/swagger.yaml ; \ + ./build/gaiad init --home /tmp/contract_tests/.gaiad --chain-id lcd contract-tests ; \ + tar -xzf lcd_test/testdata/state.tar.gz -C /tmp/contract_tests/ + +start-gaia: setup-contract-tests-data + ./build/gaiad --home /tmp/contract_tests/.gaiad start & + @sleep 2s + +setup-transactions: start-gaia + @bash ./lcd_test/testdata/setup.sh + +run-lcd-contract-tests: + @echo "Running Gaia LCD for contract tests" + ./build/gaiacli rest-server --laddr tcp://0.0.0.0:8080 --home /tmp/contract_tests/.gaiacli --node http://localhost:26657 --chain-id lcd --trust-node true + +contract-tests: setup-transactions + @echo "Running Gaia LCD for contract tests" + dredd && pkill gaiad + # include simulations include sims.mk .PHONY: all build-linux install install-debug \ - go-mod-cache draw-deps clean \ + go-mod-cache draw-deps clean build \ + setup-transactions setup-contract-tests-data start-gaia run-lcd-contract-tests contract-tests \ check check-all check-build check-cover check-ledger check-unit check-race diff --git a/cmd/contract_tests/main.go b/cmd/contract_tests/main.go new file mode 100644 index 0000000000..740c771f34 --- /dev/null +++ b/cmd/contract_tests/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "github.com/snikch/goodman/hooks" + "github.com/snikch/goodman/transaction" +) + +func main() { + // This must be compiled beforehand and given to dredd as parameter, in the meantime the server should be running + h := hooks.NewHooks() + server := hooks.NewServer(hooks.NewHooksRunner(h)) + h.BeforeAll(func(t []*transaction.Transaction) { + fmt.Println("Sleep 5 seconds before all modification") + }) + h.BeforeEach(func(t *transaction.Transaction) { + fmt.Println("before each modification") + }) + h.Before("/version > GET", func(t *transaction.Transaction) { + fmt.Println("before version TEST") + }) + h.Before("/node_version > GET", func(t *transaction.Transaction) { + fmt.Println("before node_version TEST") + }) + h.BeforeEachValidation(func(t *transaction.Transaction) { + fmt.Println("before each validation modification") + }) + h.BeforeValidation("/node_version > GET", func(t *transaction.Transaction) { + fmt.Println("before validation node_version TEST") + }) + h.After("/node_version > GET", func(t *transaction.Transaction) { + fmt.Println("after node_version TEST") + }) + h.AfterEach(func(t *transaction.Transaction) { + fmt.Println("after each modification") + }) + h.AfterAll(func(t []*transaction.Transaction) { + fmt.Println("after all modification") + }) + server.Serve() + defer server.Listener.Close() + fmt.Print(h) +} diff --git a/contrib/get_node.sh b/contrib/get_node.sh new file mode 100755 index 0000000000..7f0dd6e38d --- /dev/null +++ b/contrib/get_node.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +VERSION=v11.15.0 +NODE_FULL=node-${VERSION}-linux-x64 + +mkdir -p ~/.local/bin +mkdir -p ~/.local/node +wget http://nodejs.org/dist/${VERSION}/${NODE_FULL}.tar.gz -O ~/.local/node/${NODE_FULL}.tar.gz +tar -xzf ~/.local/node/${NODE_FULL}.tar.gz -C ~/.local/node/ +ln -s ~/.local/node/${NODE_FULL}/bin/node ~/.local/bin/node +ln -s ~/.local/node/${NODE_FULL}/bin/npm ~/.local/bin/npm +export PATH=~/.local/bin:$PATH +npm i -g dredd@11.0.1 +ln -s ~/.local/node/${NODE_FULL}/bin/dredd ~/.local/bin/dredd diff --git a/dredd.yml b/dredd.yml new file mode 100644 index 0000000000..3c7e68e2ca --- /dev/null +++ b/dredd.yml @@ -0,0 +1,33 @@ +color: true +dry-run: null +hookfiles: build/contract_tests +language: go +require: null +server: make run-lcd-contract-tests +server-wait: 5 +init: false +custom: {} +names: false +only: [] +reporter: [] +output: [] +header: [] +sorted: false +user: null +inline-errors: false +details: false +method: [GET] +loglevel: warning +path: [] +hooks-worker-timeout: 5000 +hooks-worker-connect-timeout: 1500 +hooks-worker-connect-retry: 500 +hooks-worker-after-connect-wait: 100 +hooks-worker-term-timeout: 5000 +hooks-worker-term-retry: 500 +hooks-worker-handler-host: 127.0.0.1 +hooks-worker-handler-port: 61321 +config: ./dredd.yml +# This path accepts no variables +blueprint: /tmp/contract_tests/swagger.yaml +endpoint: 'http://127.0.0.1:8080' diff --git a/go.mod b/go.mod index eb2dbbcf91..7bf3b98df3 100644 --- a/go.mod +++ b/go.mod @@ -15,10 +15,12 @@ require ( github.com/otiai10/copy v1.0.1 github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776 // indirect github.com/pelletier/go-toml v1.4.0 // indirect + github.com/pkg/errors v0.8.1 github.com/prometheus/common v0.4.1 // indirect github.com/prometheus/procfs v0.0.0-20190523193104-a7aeb8df3389 // indirect - github.com/rakyll/statik v0.1.6 // indirect + github.com/rakyll/statik v0.1.6 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect + github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa github.com/spf13/afero v1.2.2 // indirect github.com/spf13/cobra v0.0.4 github.com/spf13/viper v1.4.0 diff --git a/go.sum b/go.sum index dc27c36d81..7233933289 100644 --- a/go.sum +++ b/go.sum @@ -182,6 +182,8 @@ github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa h1:YJfZp12Z3AFhSBeXOlv4BO55RMwPn2NoQeDsrdWnBtY= +github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= diff --git a/lcd_test/helpers.go b/lcd_test/helpers.go new file mode 100644 index 0000000000..0487051a43 --- /dev/null +++ b/lcd_test/helpers.go @@ -0,0 +1,478 @@ +package lcdtest + +import ( + "bytes" + "fmt" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/lcd" + "github.com/cosmos/cosmos-sdk/codec" + crkeys "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/tests" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/genaccounts" + "github.com/cosmos/cosmos-sdk/x/crisis" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/genutil" + "github.com/cosmos/cosmos-sdk/x/mint" + "github.com/cosmos/cosmos-sdk/x/staking" + gapp "github.com/cosmos/gaia/app" + "github.com/pkg/errors" + "github.com/spf13/viper" + "github.com/tendermint/go-amino" + tmcfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/cli" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + "io/ioutil" + "net" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" + nm "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/p2p" + pvm "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/proxy" + tmrpc "github.com/tendermint/tendermint/rpc/lib/server" + tmtypes "github.com/tendermint/tendermint/types" +) + +// TODO: Make InitializeTestLCD safe to call in multiple tests at the same time +// InitializeLCD starts Tendermint and the LCD in process, listening on +// their respective sockets where nValidators is the total number of validators +// and initAddrs are the accounts to initialize with some stake tokens. It +// returns a cleanup function, a set of validator public keys, and a port. +func InitializeLCD(nValidators int, initAddrs []sdk.AccAddress, minting bool, portExt ...string) ( + cleanup func(), valConsPubKeys []crypto.PubKey, valOperAddrs []sdk.ValAddress, port string, err error) { + + config, err := GetConfig() + if err != nil { + return + } + config.Consensus.TimeoutCommit = 100 + config.Consensus.SkipTimeoutCommit = false + config.TxIndex.IndexAllTags = true + + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + logger = log.NewFilter(logger, log.AllowError()) + + db := dbm.NewMemDB() + app := gapp.NewGaiaApp(logger, db, nil, true, 0) + cdc = gapp.MakeCodec() + + genDoc, valConsPubKeys, valOperAddrs, privVal, err := defaultGenesis(config, nValidators, initAddrs, minting) + if err != nil { + return + } + + var listenAddr string + + if len(portExt) == 0 { + listenAddr, port, err = server.FreeTCPAddr() + if err != nil { + return + } + } else { + listenAddr = fmt.Sprintf("tcp://0.0.0.0:%s", portExt[0]) + port = portExt[0] + } + + // XXX: Need to set this so LCD knows the tendermint node address! + viper.Set(client.FlagNode, config.RPC.ListenAddress) + viper.Set(client.FlagChainID, genDoc.ChainID) + // TODO Set to false once the upstream Tendermint proof verification issue is fixed. + viper.Set(client.FlagTrustNode, true) + + node, err := startTM(config, logger, genDoc, privVal, app) + if err != nil { + return + } + + tests.WaitForNextHeightTM(tests.ExtractPortFromAddress(config.RPC.ListenAddress)) + lcdInstance, err := startLCD(logger, listenAddr, cdc) + if err != nil { + return + } + + tests.WaitForLCDStart(port) + tests.WaitForHeight(1, port) + + cleanup = func() { + logger.Debug("cleaning up LCD initialization") + err = node.Stop() + if err != nil { + logger.Error(err.Error()) + } + + node.Wait() + err = lcdInstance.Close() + if err != nil { + logger.Error(err.Error()) + } + } + + return cleanup, valConsPubKeys, valOperAddrs, port, err +} + +func defaultGenesis(config *tmcfg.Config, nValidators int, initAddrs []sdk.AccAddress, minting bool) ( + genDoc *tmtypes.GenesisDoc, valConsPubKeys []crypto.PubKey, valOperAddrs []sdk.ValAddress, privVal *pvm.FilePV, err error) { + privVal = pvm.LoadOrGenFilePV(config.PrivValidatorKeyFile(), + config.PrivValidatorStateFile()) + privVal.Reset() + + if nValidators < 1 { + err = errors.New("InitializeLCD must use at least one validator") + return + } + + genesisFile := config.GenesisFile() + genDoc, err = tmtypes.GenesisDocFromFile(genesisFile) + if err != nil { + return + } + genDoc.Validators = nil + err = genDoc.SaveAs(genesisFile) + if err != nil { + return + } + + // append any additional (non-proposing) validators + var genTxs []auth.StdTx + var accs []genaccounts.GenesisAccount + + for i := 0; i < nValidators; i++ { + operPrivKey := secp256k1.GenPrivKey() + operAddr := operPrivKey.PubKey().Address() + pubKey := privVal.GetPubKey() + + power := int64(100) + if i > 0 { + pubKey = ed25519.GenPrivKey().PubKey() + power = 1 + } + startTokens := sdk.TokensFromTendermintPower(power) + + msg := staking.NewMsgCreateValidator( + sdk.ValAddress(operAddr), + pubKey, + sdk.NewCoin(sdk.DefaultBondDenom, startTokens), + staking.NewDescription(fmt.Sprintf("validator-%d", i+1), "", "", ""), + staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.OneInt(), + ) + stdSignMsg := auth.StdSignMsg{ + ChainID: genDoc.ChainID, + Msgs: []sdk.Msg{msg}, + } + var sig []byte + sig, err = operPrivKey.Sign(stdSignMsg.Bytes()) + if err != nil { + return + } + transaction := auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, []auth.StdSignature{{Signature: sig, PubKey: operPrivKey.PubKey()}}, "") + genTxs = append(genTxs, transaction) + valConsPubKeys = append(valConsPubKeys, pubKey) + valOperAddrs = append(valOperAddrs, sdk.ValAddress(operAddr)) + + accAuth := auth.NewBaseAccountWithAddress(sdk.AccAddress(operAddr)) + accTokens := sdk.TokensFromTendermintPower(150) + accAuth.Coins = sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, accTokens)} + accs = append(accs, genaccounts.NewGenesisAccount(&accAuth)) + } + + genesisState := gapp.NewDefaultGenesisState() + genDoc.AppState, err = cdc.MarshalJSON(genesisState) + if err != nil { + return + } + + genesisState, err = genutil.SetGenTxsInAppGenesisState(cdc, genesisState, genTxs) + if err != nil { + return + } + + // add some tokens to init accounts + stakingDataBz := genesisState[staking.ModuleName] + var stakingData staking.GenesisState + cdc.MustUnmarshalJSON(stakingDataBz, &stakingData) + + // add some tokens to init accounts + for _, addr := range initAddrs { + accAuth := auth.NewBaseAccountWithAddress(addr) + accTokens := sdk.TokensFromTendermintPower(100) + accAuth.Coins = sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, accTokens)} + acc := genaccounts.NewGenesisAccount(&accAuth) + accs = append(accs, acc) + } + + // distr data + distrDataBz := genesisState[distr.ModuleName] + var distrData distr.GenesisState + cdc.MustUnmarshalJSON(distrDataBz, &distrData) + distrData.FeePool.CommunityPool = sdk.DecCoins{sdk.DecCoin{Denom: "test", Amount: sdk.NewDecFromInt(sdk.NewInt(10))}} + distrDataBz = cdc.MustMarshalJSON(distrData) + genesisState[distr.ModuleName] = distrDataBz + + // now add the account tokens to the non-bonded pool + for _, acc := range accs { + accTokens := acc.Coins.AmountOf(sdk.DefaultBondDenom) + stakingData.Pool.NotBondedTokens = stakingData.Pool.NotBondedTokens.Add(accTokens) + } + genesisState[staking.ModuleName] = cdc.MustMarshalJSON(stakingData) + genesisState[genaccounts.ModuleName] = cdc.MustMarshalJSON(accs) + + // mint genesis (none set within genesisState) + mintData := mint.DefaultGenesisState() + inflationMin := sdk.ZeroDec() + if minting { + inflationMin = sdk.MustNewDecFromStr("10000.0") + mintData.Params.InflationMax = sdk.MustNewDecFromStr("15000.0") + } else { + mintData.Params.InflationMax = inflationMin + } + mintData.Minter.Inflation = inflationMin + mintData.Params.InflationMin = inflationMin + mintDataBz := cdc.MustMarshalJSON(mintData) + genesisState[mint.ModuleName] = mintDataBz + + // initialize crisis data + crisisDataBz := genesisState[crisis.ModuleName] + var crisisData crisis.GenesisState + cdc.MustUnmarshalJSON(crisisDataBz, &crisisData) + crisisData.ConstantFee = sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000) + crisisDataBz = cdc.MustMarshalJSON(crisisData) + genesisState[crisis.ModuleName] = crisisDataBz + + //// double check inflation is set according to the minting boolean flag + if minting { + if !(mintData.Params.InflationMax.Equal(sdk.MustNewDecFromStr("15000.0")) && + mintData.Minter.Inflation.Equal(sdk.MustNewDecFromStr("10000.0")) && + mintData.Params.InflationMin.Equal(sdk.MustNewDecFromStr("10000.0"))) { + err = errors.New("Mint parameters does not correspond to their defaults") + return + } + } else { + if !(mintData.Params.InflationMax.Equal(sdk.ZeroDec()) && + mintData.Minter.Inflation.Equal(sdk.ZeroDec()) && + mintData.Params.InflationMin.Equal(sdk.ZeroDec())) { + err = errors.New("Mint parameters not equal to decimal 0") + return + } + } + + appState, err := codec.MarshalJSONIndent(cdc, genesisState) + if err != nil { + return + } + genDoc.AppState = appState + return +} + +// startTM creates and starts an in-process Tendermint node with memDB and +// in-process ABCI application. It returns the new node or any error that +// occurred. +// +// TODO: Clean up the WAL dir or enable it to be not persistent! +func startTM( + tmcfg *tmcfg.Config, logger log.Logger, genDoc *tmtypes.GenesisDoc, + privVal tmtypes.PrivValidator, app *gapp.GaiaApp, +) (*nm.Node, error) { + + genDocProvider := func() (*tmtypes.GenesisDoc, error) { return genDoc, nil } + dbProvider := func(*nm.DBContext) (dbm.DB, error) { return dbm.NewMemDB(), nil } + nodeKey, err := p2p.LoadOrGenNodeKey(tmcfg.NodeKeyFile()) + if err != nil { + return nil, err + } + node, err := nm.NewNode( + tmcfg, + privVal, + nodeKey, + proxy.NewLocalClientCreator(app), + genDocProvider, + dbProvider, + nm.DefaultMetricsProvider(tmcfg.Instrumentation), + logger.With("module", "node"), + ) + if err != nil { + return nil, err + } + + err = node.Start() + if err != nil { + return nil, err + } + + tests.WaitForRPC(tmcfg.RPC.ListenAddress) + logger.Info("Tendermint running!") + + return node, err +} + +// startLCD starts the LCD. +func startLCD(logger log.Logger, listenAddr string, cdc *codec.Codec) (net.Listener, error) { + rs := lcd.NewRestServer(cdc) + registerRoutes(rs) + listener, err := tmrpc.Listen(listenAddr, tmrpc.DefaultConfig()) + if err != nil { + return nil, err + } + go tmrpc.StartHTTPServer(listener, rs.Mux, logger, tmrpc.DefaultConfig()) //nolint:errcheck + return listener, nil +} + +// NOTE: If making updates here also update cmd/gaia/cmd/gaiacli/main.go +func registerRoutes(rs *lcd.RestServer) { + client.RegisterRoutes(rs.CliCtx, rs.Mux) + gapp.ModuleBasics.RegisterRESTRoutes(rs.CliCtx, rs.Mux) +} + +var cdc = amino.NewCodec() + +func init() { + ctypes.RegisterAmino(cdc) +} + +// CreateAddr adds an address to the key store and returns an address and seed. +// It also requires that the key could be created. +func CreateAddr(name, password string, kb crkeys.Keybase) (sdk.AccAddress, string, error) { + var ( + err error + info crkeys.Info + seed string + ) + info, seed, err = kb.CreateMnemonic(name, crkeys.English, password, crkeys.Secp256k1) + return sdk.AccAddress(info.GetPubKey().Address()), seed, err +} + +// CreateAddr adds multiple address to the key store and returns the addresses and associated seeds in lexographical order by address. +// It also requires that the keys could be created. +func CreateAddrs(kb crkeys.Keybase, numAddrs int) (addrs []sdk.AccAddress, seeds, names, passwords []string, errs []error) { + var ( + err error + info crkeys.Info + seed string + ) + + addrSeeds := AddrSeedSlice{} + + for i := 0; i < numAddrs; i++ { + name := fmt.Sprintf("test%d", i) + password := "1234567890" + info, seed, err = kb.CreateMnemonic(name, crkeys.English, password, crkeys.Secp256k1) + if err != nil { + errs = append(errs, err) + } + addrSeeds = append(addrSeeds, AddrSeed{Address: sdk.AccAddress(info.GetPubKey().Address()), Seed: seed, Name: name, Password: password}) + } + if len(errs) > 0 { + return + } + + sort.Sort(addrSeeds) + + for i := range addrSeeds { + addrs = append(addrs, addrSeeds[i].Address) + seeds = append(seeds, addrSeeds[i].Seed) + names = append(names, addrSeeds[i].Name) + passwords = append(passwords, addrSeeds[i].Password) + } + + return +} + +// AddrSeed combines an Address with the mnemonic of the private key to that address +type AddrSeed struct { + Address sdk.AccAddress + Seed string + Name string + Password string +} + +// AddrSeedSlice implements `Interface` in sort package. +type AddrSeedSlice []AddrSeed + +func (b AddrSeedSlice) Len() int { + return len(b) +} + +// Less sorts lexicographically by Address +func (b AddrSeedSlice) Less(i, j int) bool { + // bytes package already implements Comparable for []byte. + switch bytes.Compare(b[i].Address.Bytes(), b[j].Address.Bytes()) { + case -1: + return true + case 0, 1: + return false + default: + panic("not fail-able with `bytes.Comparable` bounded [-1, 1].") + } +} + +func (b AddrSeedSlice) Swap(i, j int) { + b[j], b[i] = b[i], b[j] +} + +// InitClientHome initialises client home dir. +func InitClientHome(dir string) string { + var err error + if dir == "" { + dir, err = ioutil.TempDir("", "lcd_test") + if err != nil { + panic(err) + } + } + // TODO: this should be set in NewRestServer + // and pass down the CLIContext to achieve + // parallelism. + viper.Set(cli.HomeFlag, dir) + return dir +} + +// makePathname creates a unique pathname for each test. +func makePathname() (string, error) { + p, err := os.Getwd() + if err != nil { + return "", err + } + + sep := string(filepath.Separator) + return strings.Replace(p, sep, "_", -1), nil +} + +// GetConfig returns a Tendermint config for the test cases. +func GetConfig() (*tmcfg.Config, error) { + pathname, err := makePathname() + if err != nil { + return nil, err + } + config := tmcfg.ResetTestRoot(pathname) + + tmAddr, _, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + + rcpAddr, _, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + + grpcAddr, _, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + + config.P2P.ListenAddress = tmAddr + config.RPC.ListenAddress = rcpAddr + config.RPC.GRPCListenAddress = grpcAddr + + return config, nil +} diff --git a/lcd_test/helpers_test.go b/lcd_test/helpers_test.go index e33195ffce..ea3f4cfb8b 100644 --- a/lcd_test/helpers_test.go +++ b/lcd_test/helpers_test.go @@ -1,442 +1,43 @@ -package lcd_test +package lcdtest import ( "bytes" "fmt" "io/ioutil" - "net" "net/http" - "os" - "path/filepath" "regexp" - "sort" "strings" "testing" "github.com/spf13/viper" "github.com/stretchr/testify/require" - gapp "github.com/cosmos/gaia/app" - "github.com/cosmos/cosmos-sdk/client" clientkeys "github.com/cosmos/cosmos-sdk/client/keys" - "github.com/cosmos/cosmos-sdk/client/lcd" "github.com/cosmos/cosmos-sdk/client/rpc" clienttx "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keys" crkeys "github.com/cosmos/cosmos-sdk/crypto/keys" - "github.com/cosmos/cosmos-sdk/server" - "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/rest" "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/auth/genaccounts" bankrest "github.com/cosmos/cosmos-sdk/x/bank/client/rest" - "github.com/cosmos/cosmos-sdk/x/crisis" - distr "github.com/cosmos/cosmos-sdk/x/distribution" distrrest "github.com/cosmos/cosmos-sdk/x/distribution/client/rest" - "github.com/cosmos/cosmos-sdk/x/genutil" "github.com/cosmos/cosmos-sdk/x/gov" govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" gcutils "github.com/cosmos/cosmos-sdk/x/gov/client/utils" - "github.com/cosmos/cosmos-sdk/x/mint" paramscutils "github.com/cosmos/cosmos-sdk/x/params/client/utils" "github.com/cosmos/cosmos-sdk/x/slashing" slashingrest "github.com/cosmos/cosmos-sdk/x/slashing/client/rest" "github.com/cosmos/cosmos-sdk/x/staking" stakingrest "github.com/cosmos/cosmos-sdk/x/staking/client/rest" - abci "github.com/tendermint/tendermint/abci/types" - tmcfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/crypto" - "github.com/tendermint/tendermint/crypto/ed25519" - "github.com/tendermint/tendermint/crypto/secp256k1" - "github.com/tendermint/tendermint/libs/cli" - dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/log" - nm "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/p2p" - pvm "github.com/tendermint/tendermint/privval" - "github.com/tendermint/tendermint/proxy" ctypes "github.com/tendermint/tendermint/rpc/core/types" - tmrpc "github.com/tendermint/tendermint/rpc/lib/server" - tmtypes "github.com/tendermint/tendermint/types" ) -var cdc = codec.New() - -func init() { - codec.RegisterCrypto(cdc) -} - -// makePathname creates a unique pathname for each test. It will panic if it -// cannot get the current working directory. -func makePathname() string { - p, err := os.Getwd() - if err != nil { - panic(err) - } - - sep := string(filepath.Separator) - return strings.Replace(p, sep, "_", -1) -} - -// GetConfig returns a Tendermint config for the test cases. -func GetConfig() *tmcfg.Config { - pathname := makePathname() - config := tmcfg.ResetTestRoot(pathname) - - tmAddr, _, err := server.FreeTCPAddr() - if err != nil { - panic(err) - } - - rcpAddr, _, err := server.FreeTCPAddr() - if err != nil { - panic(err) - } - - grpcAddr, _, err := server.FreeTCPAddr() - if err != nil { - panic(err) - } - - config.P2P.ListenAddress = tmAddr - config.RPC.ListenAddress = rcpAddr - config.RPC.GRPCListenAddress = grpcAddr - - return config -} - -// CreateAddr adds an address to the key store and returns an address and seed. -// It also requires that the key could be created. -func CreateAddr(t *testing.T, name, password string, kb crkeys.Keybase) (sdk.AccAddress, string) { - var ( - err error - info crkeys.Info - seed string - ) - - info, seed, err = kb.CreateMnemonic(name, crkeys.English, password, crkeys.Secp256k1) - require.NoError(t, err) - - return sdk.AccAddress(info.GetPubKey().Address()), seed -} - -// CreateAddr adds multiple address to the key store and returns the addresses and associated seeds in lexographical order by address. -// It also requires that the keys could be created. -func CreateAddrs(t *testing.T, kb crkeys.Keybase, numAddrs int) (addrs []sdk.AccAddress, seeds, names, passwords []string) { - var ( - err error - info crkeys.Info - seed string - ) - - addrSeeds := AddrSeedSlice{} - - for i := 0; i < numAddrs; i++ { - name := fmt.Sprintf("test%d", i) - password := "1234567890" - info, seed, err = kb.CreateMnemonic(name, crkeys.English, password, crkeys.Secp256k1) - require.NoError(t, err) - addrSeeds = append(addrSeeds, AddrSeed{Address: sdk.AccAddress(info.GetPubKey().Address()), Seed: seed, Name: name, Password: password}) - } - - sort.Sort(addrSeeds) - - for i := range addrSeeds { - addrs = append(addrs, addrSeeds[i].Address) - seeds = append(seeds, addrSeeds[i].Seed) - names = append(names, addrSeeds[i].Name) - passwords = append(passwords, addrSeeds[i].Password) - } - - return addrs, seeds, names, passwords -} - -// AddrSeed combines an Address with the mnemonic of the private key to that address -type AddrSeed struct { - Address sdk.AccAddress - Seed string - Name string - Password string -} - -// AddrSeedSlice implements `Interface` in sort package. -type AddrSeedSlice []AddrSeed - -func (b AddrSeedSlice) Len() int { - return len(b) -} - -// Less sorts lexicographically by Address -func (b AddrSeedSlice) Less(i, j int) bool { - // bytes package already implements Comparable for []byte. - switch bytes.Compare(b[i].Address.Bytes(), b[j].Address.Bytes()) { - case -1: - return true - case 0, 1: - return false - default: - panic("not fail-able with `bytes.Comparable` bounded [-1, 1].") - } -} - -func (b AddrSeedSlice) Swap(i, j int) { - b[j], b[i] = b[i], b[j] -} - -// InitClientHome initialises client home dir. -func InitClientHome(t *testing.T, dir string) string { - var err error - if dir == "" { - dir, err = ioutil.TempDir("", "lcd_test") - require.NoError(t, err) - } - // TODO: this should be set in NewRestServer - // and pass down the CLIContext to achieve - // parallelism. - viper.Set(cli.HomeFlag, dir) - return dir -} - -// TODO: Make InitializeTestLCD safe to call in multiple tests at the same time -// InitializeTestLCD starts Tendermint and the LCD in process, listening on -// their respective sockets where nValidators is the total number of validators -// and initAddrs are the accounts to initialize with some stake tokens. It -// returns a cleanup function, a set of validator public keys, and a port. -func InitializeTestLCD(t *testing.T, nValidators int, initAddrs []sdk.AccAddress, minting bool) ( - cleanup func(), valConsPubKeys []crypto.PubKey, valOperAddrs []sdk.ValAddress, port string) { - - if nValidators < 1 { - panic("InitializeTestLCD must use at least one validator") - } - - config := GetConfig() - config.Consensus.TimeoutCommit = 100 - config.Consensus.SkipTimeoutCommit = false - config.TxIndex.IndexAllTags = true - - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - logger = log.NewFilter(logger, log.AllowError()) - - privVal := pvm.LoadOrGenFilePV(config.PrivValidatorKeyFile(), - config.PrivValidatorStateFile()) - privVal.Reset() - - db := dbm.NewMemDB() - app := gapp.NewGaiaApp(logger, db, nil, true, 0) - cdc = gapp.MakeCodec() - - genesisFile := config.GenesisFile() - genDoc, err := tmtypes.GenesisDocFromFile(genesisFile) - require.Nil(t, err) - genDoc.Validators = nil - require.NoError(t, genDoc.SaveAs(genesisFile)) - - // append any additional (non-proposing) validators - var genTxs []auth.StdTx - var accs []genaccounts.GenesisAccount - for i := 0; i < nValidators; i++ { - operPrivKey := secp256k1.GenPrivKey() - operAddr := operPrivKey.PubKey().Address() - pubKey := privVal.GetPubKey() - - power := int64(100) - if i > 0 { - pubKey = ed25519.GenPrivKey().PubKey() - power = 1 - } - startTokens := sdk.TokensFromTendermintPower(power) - - msg := staking.NewMsgCreateValidator( - sdk.ValAddress(operAddr), - pubKey, - sdk.NewCoin(sdk.DefaultBondDenom, startTokens), - staking.NewDescription(fmt.Sprintf("validator-%d", i+1), "", "", ""), - staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), - sdk.OneInt(), - ) - stdSignMsg := auth.StdSignMsg{ - ChainID: genDoc.ChainID, - Msgs: []sdk.Msg{msg}, - } - sig, err := operPrivKey.Sign(stdSignMsg.Bytes()) - require.Nil(t, err) - - tx := auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, []auth.StdSignature{{Signature: sig, PubKey: operPrivKey.PubKey()}}, "") - genTxs = append(genTxs, tx) - - valConsPubKeys = append(valConsPubKeys, pubKey) - valOperAddrs = append(valOperAddrs, sdk.ValAddress(operAddr)) - - accAuth := auth.NewBaseAccountWithAddress(sdk.AccAddress(operAddr)) - accTokens := sdk.TokensFromTendermintPower(150) - accAuth.Coins = sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, accTokens)} - accs = append(accs, genaccounts.NewGenesisAccount(&accAuth)) - } - - genesisState := gapp.NewDefaultGenesisState() - genDoc.AppState, err = cdc.MarshalJSON(genesisState) - require.NoError(t, err) - genesisState, err = genutil.SetGenTxsInAppGenesisState(cdc, genesisState, genTxs) - require.NoError(t, err) - - // add some tokens to init accounts - stakingDataBz := genesisState[staking.ModuleName] - var stakingData staking.GenesisState - cdc.MustUnmarshalJSON(stakingDataBz, &stakingData) - for _, addr := range initAddrs { - accAuth := auth.NewBaseAccountWithAddress(addr) - accTokens := sdk.TokensFromTendermintPower(100) - accAuth.Coins = sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, accTokens)} - acc := genaccounts.NewGenesisAccount(&accAuth) - accs = append(accs, acc) - } - - // distr data - distrDataBz := genesisState[distr.ModuleName] - var distrData distr.GenesisState - cdc.MustUnmarshalJSON(distrDataBz, &distrData) - distrData.FeePool.CommunityPool = sdk.DecCoins{sdk.DecCoin{"test", sdk.NewDecFromInt(sdk.NewInt(10))}} - distrDataBz = cdc.MustMarshalJSON(distrData) - genesisState[distr.ModuleName] = distrDataBz - - // now add the account tokens to the non-bonded pool - for _, acc := range accs { - accTokens := acc.Coins.AmountOf(sdk.DefaultBondDenom) - stakingData.Pool.NotBondedTokens = stakingData.Pool.NotBondedTokens.Add(accTokens) - } - stakingDataBz = cdc.MustMarshalJSON(stakingData) - genesisState[staking.ModuleName] = stakingDataBz - - genaccountsData := genaccounts.GenesisState(accs) - genaccountsDataBz := cdc.MustMarshalJSON(genaccountsData) - genesisState[genaccounts.ModuleName] = genaccountsDataBz - - // mint genesis (none set within genesisState) - mintData := mint.DefaultGenesisState() - inflationMin := sdk.ZeroDec() - if minting { - inflationMin = sdk.MustNewDecFromStr("10000.0") - mintData.Params.InflationMax = sdk.MustNewDecFromStr("15000.0") - } else { - mintData.Params.InflationMax = inflationMin - } - mintData.Minter.Inflation = inflationMin - mintData.Params.InflationMin = inflationMin - mintDataBz := cdc.MustMarshalJSON(mintData) - genesisState[mint.ModuleName] = mintDataBz - - // initialize crisis data - crisisDataBz := genesisState[crisis.ModuleName] - var crisisData crisis.GenesisState - cdc.MustUnmarshalJSON(crisisDataBz, &crisisData) - crisisData.ConstantFee = sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000) - crisisDataBz = cdc.MustMarshalJSON(crisisData) - genesisState[crisis.ModuleName] = crisisDataBz - - // double check inflation is set according to the minting boolean flag - if minting { - require.Equal(t, sdk.MustNewDecFromStr("15000.0"), mintData.Params.InflationMax) - require.Equal(t, sdk.MustNewDecFromStr("10000.0"), mintData.Minter.Inflation) - require.Equal(t, sdk.MustNewDecFromStr("10000.0"), mintData.Params.InflationMin) - } else { - require.Equal(t, sdk.ZeroDec(), mintData.Params.InflationMax) - require.Equal(t, sdk.ZeroDec(), mintData.Minter.Inflation) - require.Equal(t, sdk.ZeroDec(), mintData.Params.InflationMin) - } - - appState, err := codec.MarshalJSONIndent(cdc, genesisState) - require.NoError(t, err) - genDoc.AppState = appState - - listenAddr, port, err := server.FreeTCPAddr() - require.NoError(t, err) - - // NOTE: Need to set this so LCD knows the tendermint node address! - viper.Set(client.FlagNode, config.RPC.ListenAddress) - viper.Set(client.FlagChainID, genDoc.ChainID) - // TODO Set to false once the upstream Tendermint proof verification issue is fixed. - viper.Set(client.FlagTrustNode, true) - - node := startTM(t, config, logger, genDoc, privVal, app) - require.NoError(t, err) - - tests.WaitForNextHeightTM(tests.ExtractPortFromAddress(config.RPC.ListenAddress)) - lcd, err := startLCD(logger, listenAddr, cdc, t) - require.NoError(t, err) - - tests.WaitForLCDStart(port) - tests.WaitForHeight(1, port) - - cleanup = func() { - logger.Debug("cleaning up LCD initialization") - node.Stop() //nolint:errcheck - node.Wait() - lcd.Close() - os.RemoveAll(config.RootDir) - } - - return cleanup, valConsPubKeys, valOperAddrs, port -} - -// startTM creates and starts an in-process Tendermint node with memDB and -// in-process ABCI application. It returns the new node or any error that -// occurred. -// -// TODO: Clean up the WAL dir or enable it to be not persistent! -func startTM( - t *testing.T, tmcfg *tmcfg.Config, logger log.Logger, genDoc *tmtypes.GenesisDoc, - privVal tmtypes.PrivValidator, app abci.Application, -) *nm.Node { - - genDocProvider := func() (*tmtypes.GenesisDoc, error) { return genDoc, nil } - dbProvider := func(*nm.DBContext) (dbm.DB, error) { return dbm.NewMemDB(), nil } - nodeKey, err := p2p.LoadOrGenNodeKey(tmcfg.NodeKeyFile()) - require.NoError(t, err) - - node, err := nm.NewNode( - tmcfg, - privVal, - nodeKey, - proxy.NewLocalClientCreator(app), - genDocProvider, - dbProvider, - nm.DefaultMetricsProvider(tmcfg.Instrumentation), - logger.With("module", "node"), - ) - require.NoError(t, err) - - err = node.Start() - require.NoError(t, err) - - tests.WaitForRPC(tmcfg.RPC.ListenAddress) - logger.Info("Tendermint running!") - - return node -} - -// startLCD starts the LCD. -func startLCD(logger log.Logger, listenAddr string, cdc *codec.Codec, t *testing.T) (net.Listener, error) { - rs := lcd.NewRestServer(cdc) - registerRoutes(rs) - listener, err := tmrpc.Listen(listenAddr, tmrpc.DefaultConfig()) - if err != nil { - return nil, err - } - go tmrpc.StartHTTPServer(listener, rs.Mux, logger, tmrpc.DefaultConfig()) //nolint:errcheck - return listener, nil -} - -// NOTE: If making updates here also update cmd/gaia/cmd/gaiacli/main.go -func registerRoutes(rs *lcd.RestServer) { - client.RegisterRoutes(rs.CliCtx, rs.Mux) - gapp.ModuleBasics.RegisterRESTRoutes(rs.CliCtx, rs.Mux) -} - // Request makes a test LCD test request. It returns a response object and a // stringified response body. func Request(t *testing.T, port, method, path string, payload []byte) (*http.Response, string) { diff --git a/lcd_test/lcd_test.go b/lcd_test/lcd_test.go index af5105b633..252fa48950 100644 --- a/lcd_test/lcd_test.go +++ b/lcd_test/lcd_test.go @@ -1,4 +1,4 @@ -package lcd_test +package lcdtest import ( "encoding/base64" @@ -52,7 +52,8 @@ func TestVersion(t *testing.T) { t.SkipNow() } - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{}, true) + require.NoError(t, err) defer cleanup() // node info @@ -75,14 +76,16 @@ func TestVersion(t *testing.T) { } func TestNodeStatus(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{}, true) + require.NoError(t, err) defer cleanup() getNodeInfo(t, port) getSyncStatus(t, port, false) } func TestBlock(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{}, true) + require.NoError(t, err) defer cleanup() getBlock(t, port, -1, false) getBlock(t, port, 2, false) @@ -90,7 +93,8 @@ func TestBlock(t *testing.T) { } func TestValidators(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{}, true) + require.NoError(t, err) defer cleanup() resultVals := getValidatorSets(t, port, -1, false) require.Contains(t, resultVals.Validators[0].Address.String(), "cosmosvalcons") @@ -100,10 +104,12 @@ func TestValidators(t *testing.T) { } func TestCoinSend(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() bz, err := hex.DecodeString("8FA6AB57AD6870F6B5B2E57735F38F2F30E73CB6") @@ -191,10 +197,12 @@ func TestCoinSend(t *testing.T) { } func TestCoinSendAccAuto(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) @@ -216,10 +224,12 @@ func TestCoinSendAccAuto(t *testing.T) { } func TestCoinMultiSendGenerateOnly(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() // generate only @@ -239,11 +249,12 @@ func TestCoinMultiSendGenerateOnly(t *testing.T) { } func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) - defer cleanup() acc := getAccount(t, port, addr) @@ -282,10 +293,12 @@ func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { } func TestEncodeTx(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() res, body, _ := doTransferWithGas(t, port, seed, name1, memo, "", addr, "2", 1, false, false, fees) @@ -315,10 +328,12 @@ func TestEncodeTx(t *testing.T) { } func TestTxs(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() var emptyTxs []sdk.TxResponse @@ -367,10 +382,12 @@ func TestTxs(t *testing.T) { } func TestPoolParamsQuery(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, _, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, _ := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() defaultParams := staking.DefaultParams() @@ -395,7 +412,8 @@ func TestPoolParamsQuery(t *testing.T) { } func TestValidatorsQuery(t *testing.T) { - cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + cleanup, valPubKeys, operAddrs, port, err := InitializeLCD(1, []sdk.AccAddress{}, true) + require.NoError(t, err) defer cleanup() require.Equal(t, 1, len(valPubKeys)) @@ -415,7 +433,8 @@ func TestValidatorsQuery(t *testing.T) { } func TestValidatorQuery(t *testing.T) { - cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + cleanup, valPubKeys, operAddrs, port, err := InitializeLCD(1, []sdk.AccAddress{}, true) + require.NoError(t, err) defer cleanup() require.Equal(t, 1, len(valPubKeys)) require.Equal(t, 1, len(operAddrs)) @@ -425,11 +444,13 @@ func TestValidatorQuery(t *testing.T) { } func TestBonding(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, _, err := CreateAddr(name1, pw, kb) require.NoError(t, err) - addr, _ := CreateAddr(t, name1, pw, kb) - cleanup, valPubKeys, operAddrs, port := InitializeTestLCD(t, 2, []sdk.AccAddress{addr}, false) + cleanup, valPubKeys, operAddrs, port, err := InitializeLCD(2, []sdk.AccAddress{addr}, false) + require.NoError(t, err) tests.WaitForHeight(1, port) defer cleanup() @@ -589,10 +610,12 @@ func TestBonding(t *testing.T) { } func TestSubmitProposal(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) @@ -626,10 +649,12 @@ func TestSubmitProposal(t *testing.T) { } func TestSubmitCommunityPoolSpendProposal(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) @@ -663,10 +688,12 @@ func TestSubmitCommunityPoolSpendProposal(t *testing.T) { } func TestSubmitParamChangeProposal(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) @@ -700,10 +727,12 @@ func TestSubmitParamChangeProposal(t *testing.T) { } func TestDeposit(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) @@ -759,10 +788,12 @@ func TestDeposit(t *testing.T) { } func TestVote(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, operAddrs, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, operAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() acc := getAccount(t, port, addr) @@ -846,10 +877,12 @@ func TestVote(t *testing.T) { } func TestUnjail(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, _, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, valPubKeys, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, _ := CreateAddr(t, name1, pw, kb) - cleanup, valPubKeys, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() // NOTE: any less than this and it fails @@ -865,11 +898,13 @@ func TestUnjail(t *testing.T) { } func TestProposalsQuery(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) require.NoError(t, err) - addrs, seeds, names, passwords := CreateAddrs(t, kb, 2) + addrs, seeds, names, passwords, errors := CreateAddrs(kb, 2) + require.Empty(t, errors) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addrs[0], addrs[1]}, true) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addrs[0], addrs[1]}, true) + require.NoError(t, err) defer cleanup() depositParam := getDepositParam(t, port) @@ -995,19 +1030,21 @@ func TestProposalsQuery(t *testing.T) { } func TestSlashingGetParams(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{}, true) + require.NoError(t, err) defer cleanup() res, body := Request(t, port, "GET", "/slashing/parameters", nil) require.Equal(t, http.StatusOK, res.StatusCode, body) var params slashing.Params - err := cdc.UnmarshalJSON([]byte(body), ¶ms) + err = cdc.UnmarshalJSON([]byte(body), ¶ms) require.NoError(t, err) } func TestDistributionGetParams(t *testing.T) { - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}, true) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{}, true) + require.NoError(t, err) defer cleanup() res, body := Request(t, port, "GET", "/distribution/parameters", nil) @@ -1016,10 +1053,12 @@ func TestDistributionGetParams(t *testing.T) { } func TestDistributionFlow(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, seed, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, valAddrs, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, seed := CreateAddr(t, name1, pw, kb) - cleanup, _, valAddrs, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() valAddr := valAddrs[0] @@ -1092,10 +1131,12 @@ func TestDistributionFlow(t *testing.T) { } func TestMintingQueries(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, _, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, _ := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() res, body := Request(t, port, "GET", "/minting/parameters", nil) @@ -1118,10 +1159,12 @@ func TestMintingQueries(t *testing.T) { } func TestAccountBalanceQuery(t *testing.T) { - kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + kb, err := keys.NewKeyBaseFromDir(InitClientHome("")) + require.NoError(t, err) + addr, _, err := CreateAddr(name1, pw, kb) + require.NoError(t, err) + cleanup, _, _, port, err := InitializeLCD(1, []sdk.AccAddress{addr}, true) require.NoError(t, err) - addr, _ := CreateAddr(t, name1, pw, kb) - cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) defer cleanup() bz, err := hex.DecodeString("8FA6AB57AD6870F6B5B2E57735F38F2F30E73CB6") diff --git a/lcd_test/testdata/setup.sh b/lcd_test/testdata/setup.sh new file mode 100755 index 0000000000..05ee799976 --- /dev/null +++ b/lcd_test/testdata/setup.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +PASSWORD="1234567890" +ADDR="cosmos16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv" +RECEIVER="cosmos17gx5vwpm0y2k59tw0x00ccug234n56cgltx2w2" +VALIDATOR="cosmosvaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l" +AMOUNT="1000000stake" +CHAIN="lcd" +PROPOSALID="2" +HOME="/tmp/contract_tests/.gaiacli" +SWAGGER='/tmp/contract_tests/swagger.yaml' + +# sleeping a whole second between each step is a conservative precaution +# check lcd_test/testdata/state.tar.gz -> .gaiad/config/config.toml precommit_timeout = 500ms +sleep 1s +echo ${PASSWORD} | ./build/gaiacli tx gov submit-proposal --home ${HOME} --from ${ADDR} --chain-id ${CHAIN} --type text --title test --description test_description --deposit 10000stake --yes +sleep 1s +echo ${PASSWORD} | ./build/gaiacli tx gov deposit --home ${HOME} --from ${ADDR} --chain-id ${CHAIN} ${PROPOSALID} 1000000000stake --yes +sleep 1s +echo ${PASSWORD} | ./build/gaiacli tx gov vote --home ${HOME} --from ${ADDR} --yes --chain-id ${CHAIN} ${PROPOSALID} Yes +sleep 1s +HASH=$(echo ${PASSWORD} | ./build/gaiacli tx send --home ${HOME} ${ADDR} ${RECEIVER} ${AMOUNT} --yes --chain-id ${CHAIN} | awk '/txhash.*/{print $2}') +sed -i.bak -e "s/BCBE20E8D46758B96AE5883B792858296AC06E51435490FBDCAE25A72B3CC76B/${HASH}/g" "${SWAGGER}" +echo "Replaced dummy with actual transaction hash ${HASH}" +sleep 1s +echo ${PASSWORD} | ./build/gaiacli tx staking unbond --home ${HOME} --from ${ADDR} ${VALIDATOR} 100stake --yes --chain-id ${CHAIN} + diff --git a/lcd_test/testdata/state.tar.gz b/lcd_test/testdata/state.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..30f0942309381cb7f978a195a01ed66fb1cba9e4 GIT binary patch literal 20266 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 T|173?+X