From 5553cbde87ecf974ee7529fef6f4d041a37d217a Mon Sep 17 00:00:00 2001 From: Matt Witkowski Date: Mon, 27 Nov 2023 19:16:22 -0500 Subject: [PATCH 1/3] Add auto complete. (#1762) * Add auto complete. * Add tests for auto complete. * Change some text. * Update changelog. * Update test to remove branch since it's already included. --- CHANGELOG.md | 1 + cmd/provenanced/cmd/cmd_test.go | 54 +++++++++++++++++++++++++++++++++ cmd/provenanced/cmd/root.go | 36 ++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69a269994e..8e84d77e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * Add CLI commands for the exchange module endpoints and queries [#1701](https://github.com/provenance-io/provenance/issues/1701). +* Add CLI command to generate autocomplete shell scripts [#1762](https://github.com/provenance-io/provenance/pull/1762). ### Improvements diff --git a/cmd/provenanced/cmd/cmd_test.go b/cmd/provenanced/cmd/cmd_test.go index c15c9024d1..923e821394 100644 --- a/cmd/provenanced/cmd/cmd_test.go +++ b/cmd/provenanced/cmd/cmd_test.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" "github.com/provenance-io/provenance/cmd/provenanced/cmd" + "github.com/provenance-io/provenance/testutil/assertions" ) func TestInitCmd(t *testing.T) { @@ -24,3 +25,56 @@ func TestInitCmd(t *testing.T) { err := cmd.Execute(rootCmd) require.NoError(t, err) } + +func TestGenAutoCompleteCmd(t *testing.T) { + home := t.TempDir() + + tests := []struct { + name string + args []string + err string + }{ + { + name: "failure - missing arg", + err: "accepts 1 arg(s), received 0", + }, + { + name: "failure - too many args", + args: []string{"bash", "fish"}, + err: "accepts 1 arg(s), received 2", + }, + { + name: "failure - invalid shell type", + args: []string{"badshellname"}, + err: "shell badshellname is not supported", + }, + { + name: "success - works with bash", + args: []string{"bash"}, + }, + { + name: "success - works with zsh", + args: []string{"zsh"}, + }, + { + name: "success - works with fish", + args: []string{"fish"}, + }, + { + name: "success - works with powershell", + args: []string{"powershell"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + args := []string{"--home", home, "enable-cli-autocomplete"} + args = append(args, tc.args...) + + rootCmd, _ := cmd.NewRootCmd(false) + rootCmd.SetArgs(args) + err := cmd.Execute(rootCmd) + assertions.AssertErrorValue(t, err, tc.err, "should have the correct output value") + }) + } +} diff --git a/cmd/provenanced/cmd/root.go b/cmd/provenanced/cmd/root.go index da030bcbdb..91d1d3c50b 100644 --- a/cmd/provenanced/cmd/root.go +++ b/cmd/provenanced/cmd/root.go @@ -101,6 +101,7 @@ func NewRootCmd(sealConfig bool) (*cobra.Command, params.EncodingConfig) { return nil }, } + genAutoCompleteCmd(rootCmd) initRootCmd(rootCmd, encodingConfig) overwriteFlagDefaults(rootCmd, map[string]string{ flags.FlagChainID: "", @@ -180,6 +181,41 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { startCmd.SilenceUsage = true } +// genAutoCompleteCmd creates the command for autocomplete. +func genAutoCompleteCmd(rootCmd *cobra.Command) { + rootCmd.AddCommand(&cobra.Command{ + Use: "enable-cli-autocomplete [bash|zsh|fish|powershell]", + Short: "Generates autocomplete scripts for the provenanced binary", + Long: `To configure your shell to load completions for each session, add to your profile: + +# bash example +echo '. <(provenanced enable-cli-autocomplete bash)' >> ~/.bash_profile +source ~/.bash_profile + +# zsh example +echo '. <(provenanced enable-cli-autocomplete zsh)' >> ~/.zshrc +source ~/.zshrc +`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + return cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + return cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } + + return fmt.Errorf("shell %s is not supported", args[0]) + }, + }) +} + func addModuleInitFlags(startCmd *cobra.Command) { crisis.AddModuleInitFlags(startCmd) } From 0bfc193a02f92db616eab95ef8b151f9eded5e93 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Tue, 28 Nov 2023 06:42:29 -0700 Subject: [PATCH 2/3] Fix the module links in x/README.md. (#1765) --- x/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/x/README.md b/x/README.md index af055f45b4..0d077d2fb6 100644 --- a/x/README.md +++ b/x/README.md @@ -9,14 +9,14 @@ Modules are the code components of the Provenance Blockchain that execute the ma Provenance uses inherited modules from the Cosmos SDK, and has also developed modules that are specific to Provenance. * [Inherited Cosmos modules](https://docs.cosmos.network/v0.47/build/modules) -* [Attribute](./attribute/README.md) - Functions as a blockchain registry for storing \ pairs. -* [Exchange](./exchange/README.md) - Facilitates the trading of on-chain assets. -* [Hold](./hold/README.md) - Keeps track of funds in an account that have a hold placed on them. +* [Attribute](./attribute/spec/README.md) - Functions as a blockchain registry for storing \ pairs. +* [Exchange](./exchange/spec/README.md) - Facilitates the trading of on-chain assets. +* [Hold](./hold/spec/README.md) - Keeps track of funds in an account that have a hold placed on them. * [ibchooks](./ibchooks/README.md) - Forked from https://github.com/osmosis-labs/osmosis/tree/main/x/ibchooks -* [Marker](./marker/README.md) - Allows for the creation of fungible tokens. -* [Metadata](./metadata/README.md) - Provides a system for referencing off-chain information. -* [msgfees](./msgfees/README.md) - Manages additional fees that can be applied to tx msgs. -* [Name](./name/README.md) - Provides a system for providing human-readable names as aliases for addresses. -* [Oracle](./oracle/README.md) - Provides the capability to dynamically expose query endpoints. -* [Reward](./reward/README.md) - Provides a system for distributing rewards to accounts. -* [Trigger](./trigger/README.md) - Provides a system for triggering transactions based on predeterminded events. +* [Marker](./marker/spec/README.md) - Allows for the creation of fungible tokens. +* [Metadata](./metadata/spec/README.md) - Provides a system for referencing off-chain information. +* [msgfees](./msgfees/spec/README.md) - Manages additional fees that can be applied to tx msgs. +* [Name](./name/spec/README.md) - Provides a system for providing human-readable names as aliases for addresses. +* [Oracle](./oracle/spec/README.md) - Provides the capability to dynamically expose query endpoints. +* [Reward](./reward/spec/README.md) - Provides a system for distributing rewards to accounts. +* [Trigger](./trigger/spec/README.md) - Provides a system for triggering transactions based on predeterminded events. From b16a8ba385d84433dd30189e8c132038b1617de8 Mon Sep 17 00:00:00 2001 From: Daniel Wedul Date: Tue, 28 Nov 2023 09:58:27 -0700 Subject: [PATCH 3/3] Add a market to our test/dev chains. (#1759) * [1757]: Create the add-genesis-default-market and add-genesis-custom-market commands. * [1757]: Add a call to add-genesis-default-market to the initialize.sh script. * [1757]: Add a market to the testnet command. * [1757]: Add a market to the devenet setup. * [1757]: Make the updateGenesisFileRunE stuff private. * [1757]: Unit tests on the new cmd helpers and add a check in the testnet unit test. * [1757]: Unit tests on the two new commands. * [1757]: Don't tear down the network if it's nil. * [1757]: remove a call to app.SetConfig in TestPreUpgradeCmd since I'm not sure it's needed and might be causing problems in other tests. * [1757]: Add changelog entry. --------- Co-authored-by: Ira Miller <72319+iramiller@users.noreply.github.com> --- CHANGELOG.md | 4 +- cmd/provenanced/cmd/export_test.go | 13 + cmd/provenanced/cmd/genaccounts.go | 235 +++++++ cmd/provenanced/cmd/genaccounts_test.go | 749 ++++++++++++++++++++++ cmd/provenanced/cmd/pre_upgrade_test.go | 2 - cmd/provenanced/cmd/root.go | 2 + cmd/provenanced/cmd/testnet.go | 17 +- cmd/provenanced/cmd/testnet_test.go | 13 +- networks/dev/blockchain-dev/entrypoint.sh | 19 +- scripts/initialize.sh | 1 + testutil/mocks/codec.go | 143 +++++ testutil/network.go | 4 + 12 files changed, 1184 insertions(+), 18 deletions(-) create mode 100644 cmd/provenanced/cmd/export_test.go create mode 100644 testutil/mocks/codec.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e84d77e40..3406dbd5d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,11 +41,13 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Add CLI commands for the exchange module endpoints and queries [#1701](https://github.com/provenance-io/provenance/issues/1701). * Add CLI command to generate autocomplete shell scripts [#1762](https://github.com/provenance-io/provenance/pull/1762). +* Create CLI commands for adding a market to a genesis file [#1757](https://github.com/provenance-io/provenance/issues/1757). ### Improvements * Add upgrade handler for 1.18 [#1756](https://github.com/provenance-io/provenance/pull/1756). -* Updated documentation for each module to work with docusaurus [PR 1763](https://github.com/provenance-io/provenance/pull/1763) +* Updated documentation for each module to work with docusaurus [PR 1763](https://github.com/provenance-io/provenance/pull/1763). +* Create a default market in `make run`, `localnet`, `devnet` and the `provenanced testnet` command [#1757](https://github.com/provenance-io/provenance/issues/1757). ### Dependencies diff --git a/cmd/provenanced/cmd/export_test.go b/cmd/provenanced/cmd/export_test.go new file mode 100644 index 0000000000..71c56d6fb5 --- /dev/null +++ b/cmd/provenanced/cmd/export_test.go @@ -0,0 +1,13 @@ +package cmd + +// This file is in the cmd package (not cmd_test) so that it can expose +// some private keeper stuff for unit testing. + +var ( + // MakeDefaultMarket is a test-only exposure of makeDefaultMarket. + MakeDefaultMarket = makeDefaultMarket + // AddMarketsToAppState is a test-only exposure of addMarketsToAppState. + AddMarketsToAppState = addMarketsToAppState + // GetNextAvailableMarketID is a test-only exposure of getNextAvailableMarketID. + GetNextAvailableMarketID = getNextAvailableMarketID +) diff --git a/cmd/provenanced/cmd/genaccounts.go b/cmd/provenanced/cmd/genaccounts.go index 1814c85f72..d136033dd7 100644 --- a/cmd/provenanced/cmd/genaccounts.go +++ b/cmd/provenanced/cmd/genaccounts.go @@ -21,7 +21,10 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/provenance-io/provenance/x/exchange" + exchangecli "github.com/provenance-io/provenance/x/exchange/client/cli" markercli "github.com/provenance-io/provenance/x/marker/client/cli" markertypes "github.com/provenance-io/provenance/x/marker/types" msgfeetypes "github.com/provenance-io/provenance/x/msgfees/types" @@ -40,8 +43,55 @@ const ( flagEscrow = "escrow" flagActivate = "activate" flagFinalize = "finalize" + + flagDenom = "denom" ) +// appStateUpdater is a function that makes modifications to an app-state. +// Use one in conjunction with updateGenesisFileRunE if your command only needs to parse +// some inputs to make additions or changes to app-state, +type appStateUpdater func(clientCtx client.Context, cmd *cobra.Command, args []string, appState map[string]json.RawMessage) error + +// updateGenesisFile reads the existing genesis file, runs the app-state through the +// provided updater, then saves the updated genesis state over the existing genesis file. +func updateGenesisFile(cmd *cobra.Command, args []string, updater appStateUpdater) error { + clientCtx := client.GetClientContextFromCmd(cmd) + serverCtx := server.GetServerContextFromCmd(cmd) + config := serverCtx.Config + config.SetRoot(clientCtx.HomeDir) + genFile := config.GenesisFile() + + // Get existing gen state + appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile) + if err != nil { + cmd.SilenceUsage = true + return fmt.Errorf("failed to read genesis file: %w", err) + } + + err = updater(clientCtx, cmd, args, appState) + if err != nil { + return err + } + + // None of the possible errors that might come after this will be helped by printing usage with them. + cmd.SilenceUsage = true + + appStateJSON, err := json.Marshal(appState) + if err != nil { + return fmt.Errorf("failed to marshal application genesis state: %w", err) + } + + genDoc.AppState = appStateJSON + return genutil.ExportGenesisFile(genDoc, genFile) +} + +// updateGenesisFileRunE returns a cobra.Command.RunE function that runs updateGenesisFile using the provided updater. +func updateGenesisFileRunE(updater appStateUpdater) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + return updateGenesisFile(cmd, args, updater) + } +} + // AddGenesisAccountCmd returns add-genesis-account cobra Command. func AddGenesisAccountCmd(defaultNodeHome string) *cobra.Command { cmd := &cobra.Command{ @@ -640,3 +690,188 @@ func checkMsgTypeValid(registry types.InterfaceRegistry, msgTypeURL string) erro } return err } + +// AddGenesisDefaultMarketCmd returns add-genesis-default-market cobra command. +func AddGenesisDefaultMarketCmd(defaultNodeHome string) *cobra.Command { + cmd := &cobra.Command{ + Use: `add-genesis-default-market [--denom ] + +All base accounts already in the genesis file will be given all permissions on this market. +An error is returned if no such accounts are in the existing genesis file. + +If --denom is not provided, the staking bond denom is used. +If no staking bond denom is defined either, then nhash is used. + +This command is equivalent to the following command: +$ ` + version.AppName + ` add-genesis-custom-market \ + --name 'Default Market' \ + --create-ask 100 --create-bid 100 \ + --seller-flat 500 --buyer-flat 500 \ + --seller-ratios 20:1 --buyer-ratios 20:1 \ + --accepting-orders --allow-user-settle \ + --access-grants + +`, + Short: "Add a default market to the genesis file", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + RunE: updateGenesisFileRunE(func(clientCtx client.Context, cmd *cobra.Command, _ []string, appState map[string]json.RawMessage) error { + // Printing usage with the errors from here won't actually help anyone. + cmd.SilenceUsage = true + + // Identify the accounts that will get all the permissions. + var authGen authtypes.GenesisState + err := clientCtx.Codec.UnmarshalJSON(appState[authtypes.ModuleName], &authGen) + if err != nil { + return fmt.Errorf("could not extract auth genesis state: %w", err) + } + genAccts, err := authtypes.UnpackAccounts(authGen.Accounts) + if err != nil { + return fmt.Errorf("could not unpack genesis acounts: %w", err) + } + addrs := make([]string, 0, len(genAccts)) + for _, acct := range genAccts { + // We specifically only want accounts that are base accounts (i.e. no markers or others). + // This should also include the validator accounts (that have already been added). + baseAcct, ok := acct.(*authtypes.BaseAccount) + if ok { + addrs = append(addrs, baseAcct.Address) + } + } + if len(addrs) == 0 { + return errors.New("genesis file must have one or more BaseAccount before a default market can be added") + } + + // Identify the denom that we'll use for the fees. + feeDenom, err := cmd.Flags().GetString(flagDenom) + if err != nil { + return fmt.Errorf("error reading --%s value: %w", flagDenom, err) + } + if len(feeDenom) == 0 { + var stGen stakingtypes.GenesisState + err = clientCtx.Codec.UnmarshalJSON(appState[stakingtypes.ModuleName], &stGen) + if err == nil { + feeDenom = stGen.Params.BondDenom + } + } + if len(feeDenom) == 0 { + feeDenom = "nhash" + } + if err = sdk.ValidateDenom(feeDenom); err != nil { + return err + } + + // Create the market and add it to the app state. + market := makeDefaultMarket(feeDenom, addrs) + return addMarketsToAppState(clientCtx, appState, market) + }), + } + + cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory") + cmd.Flags().String(flagDenom, "", "The fee denom for the market") + return cmd +} + +// makeDefaultMarket creates the default market that uses the provided fee denom +// and gives all permissions to each of the provided addrs. +func makeDefaultMarket(feeDenom string, addrs []string) exchange.Market { + market := exchange.Market{ + MarketDetails: exchange.MarketDetails{Name: "Default Market"}, + AcceptingOrders: true, + AllowUserSettlement: true, + } + + if len(feeDenom) > 0 { + creationFee := sdk.NewCoins(sdk.NewInt64Coin(feeDenom, 100)) + settlementFlat := sdk.NewCoins(sdk.NewInt64Coin(feeDenom, 500)) + settlementRatio := []exchange.FeeRatio{{Price: sdk.NewInt64Coin(feeDenom, 20), Fee: sdk.NewInt64Coin(feeDenom, 1)}} + market.MarketDetails.Name = fmt.Sprintf("Default %s Market", feeDenom) + market.FeeCreateAskFlat = creationFee + market.FeeCreateBidFlat = creationFee + market.FeeSellerSettlementFlat = settlementFlat + market.FeeSellerSettlementRatios = settlementRatio + market.FeeBuyerSettlementFlat = settlementFlat + market.FeeBuyerSettlementRatios = settlementRatio + } + + for _, addr := range addrs { + market.AccessGrants = append(market.AccessGrants, + exchange.AccessGrant{Address: addr, Permissions: exchange.AllPermissions()}) + } + return market +} + +// AddGenesisCustomMarketCmd returns add-genesis-custom-market cobra command. +func AddGenesisCustomMarketCmd(defaultNodeHome string) *cobra.Command { + cmd := &cobra.Command{ + Use: "add-genesis-custom-market", + Short: "Add a market to the genesis file", + RunE: updateGenesisFileRunE(func(clientCtx client.Context, cmd *cobra.Command, args []string, appState map[string]json.RawMessage) error { + msg, err := exchangecli.MakeMsgGovCreateMarket(clientCtx, cmd.Flags(), args) + if err != nil { + return err + } + + // Now that we've read all the flags and stuff, no need to show usage with any errors anymore in here. + cmd.SilenceUsage = true + return addMarketsToAppState(clientCtx, appState, msg.Market) + }), + } + + cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory") + exchangecli.SetupCmdTxGovCreateMarket(cmd) + exchangecli.AddUseDetails(cmd, "If no is provided, the next available one will be used.") + return cmd +} + +// addMarketsToAppState adds the given markets to the app state in the exchange module. +// If a provided market's MarketId is 0, the next available one will be identified and used. +func addMarketsToAppState(clientCtx client.Context, appState map[string]json.RawMessage, markets ...exchange.Market) error { + cdc := clientCtx.Codec + var exGenState exchange.GenesisState + if len(appState[exchange.ModuleName]) > 0 { + if err := cdc.UnmarshalJSON(appState[exchange.ModuleName], &exGenState); err != nil { + return fmt.Errorf("could not extract exchange genesis state: %w", err) + } + } + + for _, market := range markets { + if err := market.Validate(); err != nil { + return err + } + + if market.MarketId == 0 { + market.MarketId = getNextAvailableMarketID(exGenState) + if exGenState.LastMarketId < market.MarketId { + exGenState.LastMarketId = market.MarketId + } + } + + exGenState.Markets = append(exGenState.Markets, market) + } + + exGenStateBz, err := cdc.MarshalJSON(&exGenState) + if err != nil { + return fmt.Errorf("failed to marshal exchange genesis state: %w", err) + } + appState[exchange.ModuleName] = exGenStateBz + return nil +} + +// getNextAvailableMarketID returns the next available market id given all the markets in the provided genesis state. +func getNextAvailableMarketID(exGenState exchange.GenesisState) uint32 { + if len(exGenState.Markets) == 0 { + return 1 + } + + marketIDsMap := make(map[uint32]bool, len(exGenState.Markets)) + for _, market := range exGenState.Markets { + marketIDsMap[market.MarketId] = true + } + + rv := uint32(1) + for marketIDsMap[rv] { + rv++ + } + return rv +} diff --git a/cmd/provenanced/cmd/genaccounts_test.go b/cmd/provenanced/cmd/genaccounts_test.go index 39c154ecc3..5b7ed7e3f3 100644 --- a/cmd/provenanced/cmd/genaccounts_test.go +++ b/cmd/provenanced/cmd/genaccounts_test.go @@ -2,24 +2,38 @@ package cmd_test import ( "context" + "encoding/json" "fmt" "testing" + "time" "github.com/spf13/viper" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/server" sdksim "github.com/cosmos/cosmos-sdk/simapp" "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltest "github.com/cosmos/cosmos-sdk/x/genutil/client/testutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" "github.com/provenance-io/provenance/app" provenancecmd "github.com/provenance-io/provenance/cmd/provenanced/cmd" + "github.com/provenance-io/provenance/testutil/assertions" + "github.com/provenance-io/provenance/testutil/mocks" + "github.com/provenance-io/provenance/x/exchange" ) var testMbm = module.NewBasicManager(genutil.AppModuleBasic{}) @@ -164,3 +178,738 @@ func TestAddGenesisMsgFeeCmd(t *testing.T) { }) } } + +// fixEmptiesInExchangeGenState updates all nil slices to be empty slices. +// The UnmarshalJSON function uses empty slices instead of nil, but it's cleaner and +// easier to define test cases by setting stuff to nil (or omitting the field completely). +func fixEmptiesInExchangeGenState(exGenState *exchange.GenesisState) { + if exGenState.Params != nil && exGenState.Params.DenomSplits == nil { + exGenState.Params.DenomSplits = make([]exchange.DenomSplit, 0) + } + + if exGenState.Markets == nil { + exGenState.Markets = make([]exchange.Market, 0) + } + for i, market := range exGenState.Markets { + if market.FeeCreateAskFlat == nil { + exGenState.Markets[i].FeeCreateAskFlat = make([]sdk.Coin, 0) + } + if market.FeeCreateBidFlat == nil { + exGenState.Markets[i].FeeCreateBidFlat = make([]sdk.Coin, 0) + } + if market.FeeSellerSettlementFlat == nil { + exGenState.Markets[i].FeeSellerSettlementFlat = make([]sdk.Coin, 0) + } + if market.FeeSellerSettlementRatios == nil { + exGenState.Markets[i].FeeSellerSettlementRatios = make([]exchange.FeeRatio, 0) + } + if market.FeeBuyerSettlementFlat == nil { + exGenState.Markets[i].FeeBuyerSettlementFlat = make([]sdk.Coin, 0) + } + if market.FeeBuyerSettlementRatios == nil { + exGenState.Markets[i].FeeBuyerSettlementRatios = make([]exchange.FeeRatio, 0) + } + if market.ReqAttrCreateAsk == nil { + exGenState.Markets[i].ReqAttrCreateAsk = make([]string, 0) + } + if market.ReqAttrCreateBid == nil { + exGenState.Markets[i].ReqAttrCreateBid = make([]string, 0) + } + if market.AccessGrants == nil { + exGenState.Markets[i].AccessGrants = make([]exchange.AccessGrant, 0) + } + for j, ag := range market.AccessGrants { + if ag.Permissions == nil { + exGenState.Markets[i].AccessGrants[j].Permissions = make([]exchange.Permission, 0) + } + } + } + + if exGenState.Orders == nil { + exGenState.Orders = make([]exchange.Order, 0) + } + for _, order := range exGenState.Orders { + if bid := order.GetBidOrder(); bid != nil && bid.BuyerSettlementFees == nil { + bid.BuyerSettlementFees = make(sdk.Coins, 0) + } + } +} + +func TestAddGenesisDefaultMarketCmd(t *testing.T) { + expDefaultMarket := func(marketID uint32, denom string, addrs ...string) exchange.Market { + rv := provenancecmd.MakeDefaultMarket(denom, addrs) + rv.MarketId = marketID + return rv + } + addrs := []string{ + sdk.AccAddress("one_________________").String(), + sdk.AccAddress("two_________________").String(), + sdk.AccAddress("three_______________").String(), + } + + tests := []struct { + name string + iniAddrs []string + iniExGenState *exchange.GenesisState + args []string + expErr string + expExGenState exchange.GenesisState + }{ + { + name: "Incorrect usage", + iniExGenState: exchange.DefaultGenesisState(), + args: []string{"--denom"}, + expErr: "flag needs an argument: --denom", + }, + { + name: "no exchange gen state in file, default denom", + args: []string{}, + iniAddrs: addrs[1:2], + expExGenState: exchange.GenesisState{ + Markets: []exchange.Market{ + expDefaultMarket(1, "nhash", addrs[1]), + }, + LastMarketId: 1, + }, + }, + { + name: "default exchange gen state in file, denom provided", + iniAddrs: addrs, + iniExGenState: exchange.DefaultGenesisState(), + args: []string{"--denom", "vspn"}, + expExGenState: exchange.GenesisState{ + Params: exchange.DefaultParams(), + Markets: []exchange.Market{ + expDefaultMarket(1, "vspn", addrs...), + }, + LastMarketId: 1, + }, + }, + { + name: "file already has two markets", + iniAddrs: addrs[0:2], + iniExGenState: &exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 432}, + Markets: []exchange.Market{ + { + MarketId: 1, + MarketDetails: exchange.MarketDetails{Name: "Market One"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + { + MarketId: 53, + MarketDetails: exchange.MarketDetails{Name: "Market One"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + }, + LastMarketId: 4, + LastOrderId: 88, + }, + args: []string{"--denom", "fruit"}, + expExGenState: exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 432}, + Markets: []exchange.Market{ + { + MarketId: 1, + MarketDetails: exchange.MarketDetails{Name: "Market One"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + { + MarketId: 53, + MarketDetails: exchange.MarketDetails{Name: "Market One"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + expDefaultMarket(2, "fruit", addrs[0], addrs[1]), + }, + LastMarketId: 4, + LastOrderId: 88, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fixEmptiesInExchangeGenState(&tc.expExGenState) + + // Create a new home dir and initialize it. + home := t.TempDir() + cfg, err := genutiltest.CreateDefaultTendermintConfig(home) + require.NoError(t, err, "setup: CreateDefaultTendermintConfig(%q)", home) + cdc := sdksim.MakeTestEncodingConfig().Codec + err = genutiltest.ExecInitCmd(testMbm, home, cdc) + require.NoError(t, err, "setup: ExecInitCmd") + + // Update the new genesis file to have the exchange genesis + // state and just the accounts defined by this test case. + genFile := cfg.GenesisFile() + appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile) + require.NoError(t, err, "setup: GenesisStateFromGenFile") + + genDoc.GenesisTime = time.Unix(1618935600, 0) // 2021-04-20 16:20:00 +0000 + + if tc.iniExGenState != nil { + appState[exchange.ModuleName], err = cdc.MarshalJSON(tc.iniExGenState) + require.NoError(t, err, "setup: MarshalJSON exchange genesis state") + } else { + if _, found := appState[exchange.ModuleName]; found { + delete(appState, exchange.ModuleName) + } + } + + var authState authtypes.GenesisState + if len(appState[authtypes.ModuleName]) > 0 { + err = cdc.UnmarshalJSON(appState[authtypes.ModuleName], &authState) + require.NoError(t, err, "setup: UnmarshalJSON auth genesis state") + } + + oldAccs := authState.Accounts + authState.Accounts = make([]*codectypes.Any, 0, len(oldAccs)+len(tc.iniAddrs)) + for _, acc := range oldAccs { + _, isBase := acc.GetCachedValue().(*authtypes.BaseAccount) + if !isBase { + authState.Accounts = append(authState.Accounts, acc) + } + } + + newAccs := make([]authtypes.GenesisAccount, len(tc.iniAddrs)) + for i, addr := range tc.iniAddrs { + accAddr, addrErr := sdk.AccAddressFromBech32(addr) + require.NoError(t, addrErr, "setup: [%d]: AccAddressFromBech32(%q)", i, addr) + newAccs[i] = authtypes.NewBaseAccountWithAddress(accAddr) + } + newAccAnys, err := authtypes.PackAccounts(newAccs) + require.NoError(t, err, "setup: PackAccounts") + authState.Accounts = append(authState.Accounts, newAccAnys...) + + appState[authtypes.ModuleName], err = cdc.MarshalJSON(&authState) + require.NoError(t, err, "MarshalJSON auth genesis state") + + genDoc.AppState, err = json.Marshal(appState) + require.NoError(t, err, "setup: Marshal app state") + err = genutil.ExportGenesisFile(genDoc, genFile) + require.NoError(t, err, "setup: ExportGenesisFile") + + // Create the contexts and set up the command. + logger := log.NewNopLogger() + serverCtx := server.NewContext(viper.New(), cfg, logger) + clientCtx := client.Context{}.WithCodec(cdc).WithHomeDir(home) + + ctx := context.Background() + ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) + ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx) + + cmd := provenancecmd.AddGenesisDefaultMarketCmd(home) + cmd.SetArgs(tc.args) + + // Run it! + err = cmd.ExecuteContext(ctx) + assertions.AssertErrorValue(t, err, tc.expErr, "%q ExecuteContext", cmd.Name()) + + if len(tc.expErr) > 0 || err != nil { + return + } + + appState, genDoc, err = genutiltypes.GenesisStateFromGenFile(genFile) + require.NoError(t, err, "GenesisStateFromGenFile(%q)", genFile) + var actExGenState exchange.GenesisState + err = cdc.UnmarshalJSON(appState[exchange.ModuleName], &actExGenState) + require.NoError(t, err, "UnmarshalJSON exchange genesis state") + assert.Equal(t, tc.expExGenState, actExGenState, "genesis state after %s", cmd.Name()) + }) + } +} + +func TestMakeDefaultMarket(t *testing.T) { + addrs := []string{ + "one_________________", + "two_________________", + "three_______________", + } + coins := func(amount int64, denom string) []sdk.Coin { + return []sdk.Coin{{Denom: denom, Amount: sdkmath.NewInt(amount)}} + } + ratios := func(denom string, priceAmt, feeAmt int64) []exchange.FeeRatio { + return []exchange.FeeRatio{{ + Price: sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(priceAmt)}, + Fee: sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(feeAmt)}, + }} + } + + tests := []struct { + name string + feeDenom string + addrs []string + expMarket exchange.Market + }{ + { + name: "no denom, nil addrs", + feeDenom: "", + addrs: nil, + expMarket: exchange.Market{ + MarketDetails: exchange.MarketDetails{Name: "Default Market"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + }, + { + name: "no denom, one addr", + feeDenom: "", + addrs: addrs[0:1], + expMarket: exchange.Market{ + MarketDetails: exchange.MarketDetails{Name: "Default Market"}, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{{Address: addrs[0], Permissions: exchange.AllPermissions()}}, + }, + }, + { + name: "no denom, three addrs", + feeDenom: "", + addrs: addrs, + expMarket: exchange.Market{ + MarketDetails: exchange.MarketDetails{Name: "Default Market"}, + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: addrs[0], Permissions: exchange.AllPermissions()}, + {Address: addrs[1], Permissions: exchange.AllPermissions()}, + {Address: addrs[2], Permissions: exchange.AllPermissions()}, + }, + }, + }, + { + name: "empty addrs", + feeDenom: "else", + addrs: []string{}, + expMarket: exchange.Market{ + MarketDetails: exchange.MarketDetails{Name: "Default else Market"}, + FeeCreateAskFlat: coins(100, "else"), + FeeCreateBidFlat: coins(100, "else"), + FeeSellerSettlementFlat: coins(500, "else"), + FeeSellerSettlementRatios: ratios("else", 20, 1), + FeeBuyerSettlementFlat: coins(500, "else"), + FeeBuyerSettlementRatios: ratios("else", 20, 1), + AcceptingOrders: true, + AllowUserSettlement: true, + }, + }, + { + name: "one addr", + feeDenom: "vspn", + addrs: addrs[0:1], + expMarket: exchange.Market{ + MarketDetails: exchange.MarketDetails{Name: "Default vspn Market"}, + FeeCreateAskFlat: coins(100, "vspn"), + FeeCreateBidFlat: coins(100, "vspn"), + FeeSellerSettlementFlat: coins(500, "vspn"), + FeeSellerSettlementRatios: ratios("vspn", 20, 1), + FeeBuyerSettlementFlat: coins(500, "vspn"), + FeeBuyerSettlementRatios: ratios("vspn", 20, 1), + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{{Address: addrs[0], Permissions: exchange.AllPermissions()}}, + }, + }, + { + name: "three addrs", + feeDenom: "nhash", + addrs: addrs, + expMarket: exchange.Market{ + MarketDetails: exchange.MarketDetails{Name: "Default nhash Market"}, + FeeCreateAskFlat: coins(100, "nhash"), + FeeCreateBidFlat: coins(100, "nhash"), + FeeSellerSettlementFlat: coins(500, "nhash"), + FeeSellerSettlementRatios: ratios("nhash", 20, 1), + FeeBuyerSettlementFlat: coins(500, "nhash"), + FeeBuyerSettlementRatios: ratios("nhash", 20, 1), + AcceptingOrders: true, + AllowUserSettlement: true, + AccessGrants: []exchange.AccessGrant{ + {Address: addrs[0], Permissions: exchange.AllPermissions()}, + {Address: addrs[1], Permissions: exchange.AllPermissions()}, + {Address: addrs[2], Permissions: exchange.AllPermissions()}, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actMarket exchange.Market + testFunc := func() { + actMarket = provenancecmd.MakeDefaultMarket(tc.feeDenom, tc.addrs) + } + require.NotPanics(t, testFunc, "MakeDefaultMarket") + assert.Equal(t, tc.expMarket, actMarket, "MakeDefaultMarket result") + }) + } +} + +func TestAddGenesisCustomMarketCmd(t *testing.T) { + tests := []struct { + name string + iniExGenState *exchange.GenesisState + args []string + expErr string + expExGenState exchange.GenesisState + }{ + { + name: "Incorrect usage", + iniExGenState: exchange.DefaultGenesisState(), + args: []string{"--create-ask", "nhash"}, + expErr: "invalid coin expression: \"nhash\"", + }, + { + name: "no exchange gen state in file", + args: []string{"--name", "My New Market", "--accepting-orders"}, + expExGenState: exchange.GenesisState{ + Markets: []exchange.Market{ + { + MarketId: 1, + MarketDetails: exchange.MarketDetails{Name: "My New Market"}, + AcceptingOrders: true, + }, + }, + LastMarketId: 1, + }, + }, + { + name: "default exchange gen state in file", + iniExGenState: exchange.DefaultGenesisState(), + args: []string{"--name", "THE market", "--market", "6"}, + expExGenState: exchange.GenesisState{ + Params: exchange.DefaultParams(), + Markets: []exchange.Market{ + { + MarketId: 6, + MarketDetails: exchange.MarketDetails{Name: "THE market"}, + }, + }, + }, + }, + { + name: "file already has two markets", + iniExGenState: &exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 432}, + Markets: []exchange.Market{ + { + MarketId: 1, + MarketDetails: exchange.MarketDetails{Name: "Market One"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + { + MarketId: 53, + MarketDetails: exchange.MarketDetails{Name: "Market One"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + }, + LastMarketId: 4, + LastOrderId: 88, + }, + args: []string{"--name", "A Better Market", "--create-bid", "50nhash", "--create-ask", "30nhash", + "--accepting-orders", "--access-grants", sdk.AccAddress("addr1_______________").String() + ":all"}, + expExGenState: exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 432}, + Markets: []exchange.Market{ + { + MarketId: 1, + MarketDetails: exchange.MarketDetails{Name: "Market One"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + { + MarketId: 53, + MarketDetails: exchange.MarketDetails{Name: "Market One"}, + AcceptingOrders: true, + AllowUserSettlement: true, + }, + { + MarketId: 2, + MarketDetails: exchange.MarketDetails{Name: "A Better Market"}, + FeeCreateAskFlat: []sdk.Coin{sdk.NewInt64Coin("nhash", 30)}, + FeeCreateBidFlat: []sdk.Coin{sdk.NewInt64Coin("nhash", 50)}, + AcceptingOrders: true, + AccessGrants: []exchange.AccessGrant{ + { + Address: sdk.AccAddress("addr1_______________").String(), + Permissions: exchange.AllPermissions(), + }, + }, + }, + }, + LastMarketId: 4, + LastOrderId: 88, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fixEmptiesInExchangeGenState(&tc.expExGenState) + + // Create a new home dir and initialize it. + home := t.TempDir() + cfg, err := genutiltest.CreateDefaultTendermintConfig(home) + require.NoError(t, err, "setup: CreateDefaultTendermintConfig(%q)", home) + cdc := sdksim.MakeTestEncodingConfig().Codec + err = genutiltest.ExecInitCmd(testMbm, home, cdc) + require.NoError(t, err, "setup: ExecInitCmd") + + // Update the new genesis file to have the exchange genesis state defined by this test case. + genFile := cfg.GenesisFile() + appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile) + require.NoError(t, err, "setup: GenesisStateFromGenFile") + + genDoc.GenesisTime = time.Unix(1618935600, 0) // 2021-04-20 16:20:00 +0000 + + if tc.iniExGenState != nil { + appState[exchange.ModuleName], err = cdc.MarshalJSON(tc.iniExGenState) + require.NoError(t, err, "setup: MarshalJSON exchange genesis state") + } else { + if _, found := appState[exchange.ModuleName]; found { + delete(appState, exchange.ModuleName) + } + } + + genDoc.AppState, err = json.Marshal(appState) + require.NoError(t, err, "setup: Marshal app state") + err = genutil.ExportGenesisFile(genDoc, genFile) + require.NoError(t, err, "setup: ExportGenesisFile") + + // Create the contexts and set up the command. + logger := log.NewNopLogger() + serverCtx := server.NewContext(viper.New(), cfg, logger) + clientCtx := client.Context{}.WithCodec(cdc).WithHomeDir(home) + + ctx := context.Background() + ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) + ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx) + + cmd := provenancecmd.AddGenesisCustomMarketCmd(home) + cmd.SetArgs(tc.args) + + // Run it! + err = cmd.ExecuteContext(ctx) + assertions.AssertErrorValue(t, err, tc.expErr, "%q ExecuteContext", cmd.Name()) + + if len(tc.expErr) > 0 || err != nil { + return + } + + appState, genDoc, err = genutiltypes.GenesisStateFromGenFile(genFile) + require.NoError(t, err, "GenesisStateFromGenFile(%q)", genFile) + var actExGenState exchange.GenesisState + err = cdc.UnmarshalJSON(appState[exchange.ModuleName], &actExGenState) + require.NoError(t, err, "UnmarshalJSON exchange genesis state") + assert.Equal(t, tc.expExGenState, actExGenState, "genesis state after %s", cmd.Name()) + }) + } +} + +func TestAddMarketsToAppState(t *testing.T) { + askOrder := *exchange.NewOrder(1).WithAsk(&exchange.AskOrder{ + MarketId: 1, + Seller: sdk.AccAddress("seller______________").String(), + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("plum", 100), + }) + bidOrder := *exchange.NewOrder(2).WithBid(&exchange.BidOrder{ + MarketId: 1, + Buyer: sdk.AccAddress("buyer_______________").String(), + Assets: sdk.NewInt64Coin("apple", 30), + Price: sdk.NewInt64Coin("plum", 200), + BuyerSettlementFees: make([]sdk.Coin, 0), + }) + + tests := []struct { + name string + codec codec.Codec // Defaults to the test encoding config codec. + exGenState exchange.GenesisState + markets []exchange.Market + expExGenState exchange.GenesisState + expErr string + }{ + { + name: "error unmarshalling exchange gen state", + codec: mocks.NewMockCodec().WithUnmarshalJSONErrs("injected error message"), + expErr: "could not extract exchange genesis state: injected error message", + }, + { + name: "error marshalling exchange gen state", + codec: mocks.NewMockCodec().WithMarshalJSONErrs("another injected error message"), + expErr: "failed to marshal exchange genesis state: another injected error message", + }, + { + name: "no markets: none added", + exGenState: exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 123}, + Markets: nil, + Orders: []exchange.Order{askOrder, bidOrder}, + LastMarketId: 42, + LastOrderId: 300, + }, + expExGenState: exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 123}, + Markets: nil, + Orders: []exchange.Order{askOrder, bidOrder}, + LastMarketId: 42, + LastOrderId: 300, + }, + }, + { + name: "no markets: one added, id 0", + exGenState: exchange.GenesisState{}, + markets: []exchange.Market{{MarketId: 0, MarketDetails: exchange.MarketDetails{Name: "some test"}}}, + expExGenState: exchange.GenesisState{ + Markets: []exchange.Market{{MarketId: 1, MarketDetails: exchange.MarketDetails{Name: "some test"}}}, + LastMarketId: 1, + }, + }, + { + name: "no markets: one added, id 3", + exGenState: exchange.GenesisState{LastMarketId: 2}, + markets: []exchange.Market{{MarketId: 3, MarketDetails: exchange.MarketDetails{Name: "some test"}}}, + expExGenState: exchange.GenesisState{ + Markets: []exchange.Market{{MarketId: 3, MarketDetails: exchange.MarketDetails{Name: "some test"}}}, + LastMarketId: 2, + }, + }, + { + name: "two markets: two added", + exGenState: exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 444, DenomSplits: []exchange.DenomSplit{{Denom: "nhash", Split: 555}}}, + Markets: []exchange.Market{ + {MarketId: 1, MarketDetails: exchange.MarketDetails{Name: "Market One"}}, + {MarketId: 8, MarketDetails: exchange.MarketDetails{Name: "Market Eight", Description: "Dude!"}}, + }, + Orders: []exchange.Order{bidOrder, askOrder}, + LastMarketId: 3, + LastOrderId: 76, + }, + markets: []exchange.Market{ + {MarketId: 12, MarketDetails: exchange.MarketDetails{Name: "Market Twelve"}}, + {MarketId: 0, MarketDetails: exchange.MarketDetails{Name: "Market Four"}}, + }, + expExGenState: exchange.GenesisState{ + Params: &exchange.Params{DefaultSplit: 444, DenomSplits: []exchange.DenomSplit{{Denom: "nhash", Split: 555}}}, + Markets: []exchange.Market{ + {MarketId: 1, MarketDetails: exchange.MarketDetails{Name: "Market One"}}, + {MarketId: 8, MarketDetails: exchange.MarketDetails{Name: "Market Eight", Description: "Dude!"}}, + {MarketId: 12, MarketDetails: exchange.MarketDetails{Name: "Market Twelve"}}, + {MarketId: 2, MarketDetails: exchange.MarketDetails{Name: "Market Four"}}, + }, + Orders: []exchange.Order{bidOrder, askOrder}, + LastMarketId: 3, + LastOrderId: 76, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + fixEmptiesInExchangeGenState(&tc.expExGenState) + + appCdc := sdksim.MakeTestEncodingConfig().Codec + egsBz, err := appCdc.MarshalJSON(&tc.exGenState) + require.NoError(t, err, "MarshalJSON initial exchange genesis state") + appState := map[string]json.RawMessage{exchange.ModuleName: egsBz} + + if tc.codec == nil { + tc.codec = appCdc + } + home := t.TempDir() + clientCtx := client.Context{}.WithCodec(tc.codec).WithHomeDir(home) + + testFunc := func() { + err = provenancecmd.AddMarketsToAppState(clientCtx, appState, tc.markets...) + } + require.NotPanics(t, testFunc, "AddMarketsToAppState") + assertions.AssertErrorValue(t, err, tc.expErr) + + var actExpGenState exchange.GenesisState + err = appCdc.UnmarshalJSON(appState[exchange.ModuleName], &actExpGenState) + assert.Equal(t, tc.expExGenState, actExpGenState, "exchange genesis state after AddMarketsToAppState") + }) + } +} + +func TestGetNextAvailableMarketID(t *testing.T) { + tests := []struct { + name string + exGenState exchange.GenesisState + exp uint32 + }{ + { + name: "nil markets", + exGenState: exchange.GenesisState{Markets: nil}, + exp: 1, + }, + { + name: "empty markets", + exGenState: exchange.GenesisState{Markets: []exchange.Market{}}, + exp: 1, + }, + { + name: "no markets: last market id 100", + exGenState: exchange.GenesisState{LastMarketId: 100}, + exp: 1, + }, + { + name: "one market: 1", + exGenState: exchange.GenesisState{Markets: []exchange.Market{{MarketId: 1}}}, + exp: 2, + }, + { + name: "one market: 2", + exGenState: exchange.GenesisState{Markets: []exchange.Market{{MarketId: 2}}}, + exp: 1, + }, + { + name: "three markets: 1 2 3", + exGenState: exchange.GenesisState{Markets: []exchange.Market{{MarketId: 1}, {MarketId: 2}, {MarketId: 3}}}, + exp: 4, + }, + { + name: "three markets: 3 2 1", + exGenState: exchange.GenesisState{Markets: []exchange.Market{{MarketId: 3}, {MarketId: 2}, {MarketId: 1}}}, + exp: 4, + }, + { + name: "three markets: 1 4 1", + exGenState: exchange.GenesisState{Markets: []exchange.Market{{MarketId: 1}, {MarketId: 4}, {MarketId: 1}}}, + exp: 2, + }, + { + name: "three markets: 1 2 4", + exGenState: exchange.GenesisState{Markets: []exchange.Market{{MarketId: 1}, {MarketId: 2}, {MarketId: 4}}}, + exp: 3, + }, + { + name: "three markets: 1 3 4", + exGenState: exchange.GenesisState{Markets: []exchange.Market{{MarketId: 1}, {MarketId: 3}, {MarketId: 4}}}, + exp: 2, + }, + { + name: "three markets: 2 3 4", + exGenState: exchange.GenesisState{Markets: []exchange.Market{{MarketId: 2}, {MarketId: 3}, {MarketId: 4}}}, + exp: 1, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var act uint32 + testFunc := func() { + act = provenancecmd.GetNextAvailableMarketID(tc.exGenState) + } + require.NotPanics(t, testFunc, "GetNextAvailableMarketID") + assert.Equal(t, tc.exp, act, "GetNextAvailableMarketID result") + }) + } +} diff --git a/cmd/provenanced/cmd/pre_upgrade_test.go b/cmd/provenanced/cmd/pre_upgrade_test.go index e2b3c1b148..202461f70a 100644 --- a/cmd/provenanced/cmd/pre_upgrade_test.go +++ b/cmd/provenanced/cmd/pre_upgrade_test.go @@ -25,7 +25,6 @@ import ( serverconfig "github.com/cosmos/cosmos-sdk/server/config" sdksim "github.com/cosmos/cosmos-sdk/simapp" - "github.com/provenance-io/provenance/app" "github.com/provenance-io/provenance/cmd/provenanced/cmd" "github.com/provenance-io/provenance/cmd/provenanced/config" "github.com/provenance-io/provenance/internal/pioconfig" @@ -211,7 +210,6 @@ func makeDummyCmd(t *testing.T, home string) *cobra.Command { } func TestPreUpgradeCmd(t *testing.T) { - app.SetConfig(true, false) pioconfig.SetProvenanceConfig("", 0) tmpDir := t.TempDir() diff --git a/cmd/provenanced/cmd/root.go b/cmd/provenanced/cmd/root.go index 91d1d3c50b..9addcb322b 100644 --- a/cmd/provenanced/cmd/root.go +++ b/cmd/provenanced/cmd/root.go @@ -148,6 +148,8 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { AddGenesisMarkerCmd(app.DefaultNodeHome), AddGenesisMsgFeeCmd(app.DefaultNodeHome, encodingConfig.InterfaceRegistry), AddGenesisCustomFloorPriceDenomCmd(app.DefaultNodeHome), + AddGenesisDefaultMarketCmd(app.DefaultNodeHome), + AddGenesisCustomMarketCmd(app.DefaultNodeHome), tmcli.NewCompletionCmd(rootCmd, true), testnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{}), debug.Cmd(), diff --git a/cmd/provenanced/cmd/testnet.go b/cmd/provenanced/cmd/testnet.go index 5c9a54e9b0..8fc270055c 100644 --- a/cmd/provenanced/cmd/testnet.go +++ b/cmd/provenanced/cmd/testnet.go @@ -44,6 +44,7 @@ import ( "github.com/provenance-io/provenance/app" "github.com/provenance-io/provenance/internal/pioconfig" + "github.com/provenance-io/provenance/x/exchange" markertypes "github.com/provenance-io/provenance/x/marker/types" nametypes "github.com/provenance-io/provenance/x/name/types" ) @@ -129,6 +130,7 @@ func InitTestnet( nodeIDs := make([]string, numValidators) valPubKeys := make([]cryptotypes.PubKey, numValidators) + valAddrs := make([]string, numValidators) simappConfig := srvconfig.DefaultConfig() simappConfig.MinGasPrices = minGasPrices @@ -143,6 +145,7 @@ func InitTestnet( genBalances []banktypes.Balance genFiles []string genMarkers []markertypes.MarkerAccount + genMarkets []exchange.Market ) inBuf := bufio.NewReader(cmd.InOrStdin()) @@ -216,6 +219,7 @@ func InitTestnet( genBalances = append(genBalances, banktypes.Balance{Address: addr.String(), Coins: coins.Sort()}) genAccounts = append(genAccounts, authtypes.NewBaseAccount(addr, nil, 0, 0)) + valAddrs[i] = addr.String() valTokens := sdk.TokensFromConsensusPower(100, app.DefaultPowerReduction) createValMsg, _ := stakingtypes.NewMsgCreateValidator( @@ -277,7 +281,9 @@ func InitTestnet( genMarkers = append(genMarkers, *markerAcc) - if err := initGenFiles(clientCtx, mbm, chainID, genAccounts, genBalances, genMarkers, genFiles, numValidators, pioconfig.GetProvenanceConfig().BondDenom); err != nil { + genMarkets = append(genMarkets, makeDefaultMarket(pioconfig.GetProvenanceConfig().BondDenom, valAddrs)) + + if err := initGenFiles(clientCtx, mbm, chainID, genAccounts, genBalances, genMarkers, genMarkets, genFiles, numValidators, pioconfig.GetProvenanceConfig().BondDenom); err != nil { return err } @@ -296,8 +302,8 @@ func InitTestnet( func initGenFiles( clientCtx client.Context, mbm module.BasicManager, chainID string, genAccounts []authtypes.GenesisAccount, genBalances []banktypes.Balance, - genMarkers []markertypes.MarkerAccount, genFiles []string, numValidators int, - chainDenom string, + genMarkers []markertypes.MarkerAccount, genMarkets []exchange.Market, + genFiles []string, numValidators int, chainDenom string, ) error { appGenState := mbm.DefaultGenesis(clientCtx.Codec) @@ -392,6 +398,11 @@ func initGenFiles( markerGenState.Markers = genMarkers appGenState[markertypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&markerGenState) + // Add the markets. + if err = addMarketsToAppState(clientCtx, appGenState, genMarkets...); err != nil { + return err + } + // END OF PROVENANCE SPECIFIC CONFIG // -------------------------------------------- diff --git a/cmd/provenanced/cmd/testnet_test.go b/cmd/provenanced/cmd/testnet_test.go index f10c33b735..2578d5130f 100644 --- a/cmd/provenanced/cmd/testnet_test.go +++ b/cmd/provenanced/cmd/testnet_test.go @@ -5,14 +5,17 @@ import ( "fmt" "testing" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/server" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" genutiltest "github.com/cosmos/cosmos-sdk/x/genutil/client/testutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" - "github.com/spf13/viper" - "github.com/stretchr/testify/require" + "github.com/provenance-io/provenance/x/exchange" "github.com/tendermint/tendermint/libs/log" "github.com/provenance-io/provenance/app" @@ -50,4 +53,10 @@ func Test_TestnetCmd(t *testing.T) { bankGenState := banktypes.GetGenesisStateFromAppState(encodingConfig.Marshaler, appState) require.NotEmpty(t, bankGenState.Supply.String()) + + var exGenState exchange.GenesisState + err = clientCtx.Codec.UnmarshalJSON(appState[exchange.ModuleName], &exGenState) + if assert.NoError(t, err, "UnmarshalJSON exchange genesis state") { + assert.Len(t, exGenState.Markets, 1, "markets in exchange genesis state") + } } diff --git a/networks/dev/blockchain-dev/entrypoint.sh b/networks/dev/blockchain-dev/entrypoint.sh index 9d64cb9f91..1d381aa8e7 100755 --- a/networks/dev/blockchain-dev/entrypoint.sh +++ b/networks/dev/blockchain-dev/entrypoint.sh @@ -11,28 +11,27 @@ LOG=${LOG:-provenance.log} ## Generate genesis if not already present ## if ! [ -f ${PIO_HOME}/config/genesis.json ]; then - "${BINARY}" -t --home "${PIO_HOME}" init --chain-id=chain-dev testing ; \ - hash_supply=0 + "${BINARY}" -t --home "${PIO_HOME}" init --chain-id=chain-dev testing num_accounts=0 for f in /mnemonics/*.txt do key_name=$(basename $f .txt) echo "Adding account $key_name from mnemonic file $f with 100000000000000000000nhash" - "${BINARY}" -t --home "${PIO_HOME}" keys add $key_name --recover --keyring-backend test < $f + "${BINARY}" -t --home "${PIO_HOME}" keys add $key_name --recover --keyring-backend test < "$f" "${BINARY}" -t --home "${PIO_HOME}" add-genesis-account $key_name 100000000000000000000nhash --keyring-backend test let num_accounts=num_accounts+1 done "${BINARY}" -t --home "${PIO_HOME}" add-genesis-root-name validator pio --keyring-backend test - "${BINARY}" -t --home "${PIO_HOME}" add-genesis-root-name validator pb --restrict=false --keyring-backend test - "${BINARY}" -t --home "${PIO_HOME}" add-genesis-root-name validator io --restrict --keyring-backend test - "${BINARY}" -t --home "${PIO_HOME}" add-genesis-root-name validator provenance --keyring-backend test - "${BINARY}" -t --home "${PIO_HOME}" add-genesis-marker ${num_accounts}00000000000000000000nhash --manager validator --access mint,burn,admin,withdraw,deposit --activate --keyring-backend test + "${BINARY}" -t --home "${PIO_HOME}" add-genesis-root-name validator pb --restrict=false --keyring-backend test + "${BINARY}" -t --home "${PIO_HOME}" add-genesis-root-name validator io --restrict --keyring-backend test + "${BINARY}" -t --home "${PIO_HOME}" add-genesis-root-name validator provenance --keyring-backend test + "${BINARY}" -t --home "${PIO_HOME}" add-genesis-marker ${num_accounts}00000000000000000000nhash --manager validator --access mint,burn,admin,withdraw,deposit --activate --keyring-backend test "${BINARY}" -t --home "${PIO_HOME}" gentx validator 100000000000000nhash --keyring-backend test --chain-id=chain-dev - "${BINARY}" -t --home "${PIO_HOME}" collect-gentxs + "${BINARY}" -t --home "${PIO_HOME}" add-genesis-default-market + "${BINARY}" -t --home "${PIO_HOME}" collect-gentxs "${BINARY}" -t --home "${PIO_HOME}" config set rpc.laddr tcp://0.0.0.0:26657 "${BINARY}" -t --home "${PIO_HOME}" config set api.enable true "${BINARY}" -t --home "${PIO_HOME}" config set api.swagger true - fi @@ -42,6 +41,6 @@ fi if [ -d "$(dirname "${PIO_HOME}"/"${LOG}")" ]; then "${BINARY}" -t --home "${PIO_HOME}" "$@" | tee "${PIO_HOME}/${LOG}" else - "${BINARY}" -t --home "${PIO_HOME}" "$@" + "${BINARY}" -t --home "${PIO_HOME}" "$@" fi diff --git a/scripts/initialize.sh b/scripts/initialize.sh index c6e5480f86..c353384331 100755 --- a/scripts/initialize.sh +++ b/scripts/initialize.sh @@ -95,6 +95,7 @@ $PROV_CMD add-genesis-msg-fee /provenance.marker.v1.MsgAddMarkerRequest "1000000 $PROV_CMD add-genesis-msg-fee /provenance.attribute.v1.MsgAddAttributeRequest "10000000000$DENOM" $PROV_CMD add-genesis-msg-fee /provenance.metadata.v1.MsgWriteScopeRequest "10000000000$DENOM" $PROV_CMD add-genesis-custom-floor "${MIN_FLOOR_PRICE}${DENOM}" +$PROV_CMD add-genesis-default-market --denom "$DENOM" $PROV_CMD collect-gentxs $PROV_CMD config set minimum-gas-prices "${MIN_FLOOR_PRICE}${DENOM}" set +ex diff --git a/testutil/mocks/codec.go b/testutil/mocks/codec.go new file mode 100644 index 0000000000..57350d363d --- /dev/null +++ b/testutil/mocks/codec.go @@ -0,0 +1,143 @@ +package mocks + +import ( + "errors" + + "github.com/gogo/protobuf/proto" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdksim "github.com/cosmos/cosmos-sdk/simapp" +) + +// This doesn't yet have injectable errors for all the possible things because they weren't needed yet. +// Feel free to add them if you need them. + +// MockCodec is a wrapper on a codec that allows injection of errors on these functions: +// MarshalJSON, MustMarshalJSON, UnmarshalJSON, MustUnmarshalJSON. +type MockCodec struct { + Base codec.Codec + MarshalJSONErrs []string + UnmarshalJSONErrs []string +} + +var _ codec.Codec = (*MockCodec)(nil) + +// NewMockCodec creates a new mock codec based on the standard test encoding config codec. +func NewMockCodec() *MockCodec { + return &MockCodec{Base: sdksim.MakeTestEncodingConfig().Codec} +} + +// WithMarshalJSONErrs adds the given errors to be returned from MarshalJSON or MustMarshalJSON. +// Each entry is used once in the order they are provided. +// An empty string indicates no error (do the normal thing). +// The receiver is both updated and returned. +func (c *MockCodec) WithMarshalJSONErrs(errMsgs ...string) *MockCodec { + c.MarshalJSONErrs = append(c.MarshalJSONErrs, errMsgs...) + return c +} + +// WithUnmarshalJSONErrs adds the given errors to be returned from UnmarshalJSON or MustUnmarshalJSON. +// Each entry is used once in the order they are provided. +// An empty string indicates no error (do the normal thing). +// The receiver is both updated and returned. +func (c *MockCodec) WithUnmarshalJSONErrs(errMsgs ...string) *MockCodec { + c.UnmarshalJSONErrs = append(c.UnmarshalJSONErrs, errMsgs...) + return c +} + +func (c *MockCodec) Marshal(o codec.ProtoMarshaler) ([]byte, error) { + return c.Base.Marshal(o) +} + +func (c *MockCodec) MustMarshal(o codec.ProtoMarshaler) []byte { + return c.Base.MustMarshal(o) +} + +func (c *MockCodec) MarshalLengthPrefixed(o codec.ProtoMarshaler) ([]byte, error) { + return c.Base.MarshalLengthPrefixed(o) +} + +func (c *MockCodec) MustMarshalLengthPrefixed(o codec.ProtoMarshaler) []byte { + return c.Base.MustMarshalLengthPrefixed(o) +} + +func (c *MockCodec) Unmarshal(bz []byte, ptr codec.ProtoMarshaler) error { + return c.Base.Unmarshal(bz, ptr) +} + +func (c *MockCodec) MustUnmarshal(bz []byte, ptr codec.ProtoMarshaler) { + c.Base.MustUnmarshal(bz, ptr) +} + +func (c *MockCodec) UnmarshalLengthPrefixed(bz []byte, ptr codec.ProtoMarshaler) error { + return c.Base.UnmarshalLengthPrefixed(bz, ptr) +} + +func (c *MockCodec) MustUnmarshalLengthPrefixed(bz []byte, ptr codec.ProtoMarshaler) { + c.Base.MustUnmarshalLengthPrefixed(bz, ptr) +} + +func (c *MockCodec) MarshalInterface(i proto.Message) ([]byte, error) { + return c.Base.MarshalInterface(i) +} + +func (c *MockCodec) UnmarshalInterface(bz []byte, ptr interface{}) error { + return c.Base.UnmarshalInterface(bz, ptr) +} + +func (c *MockCodec) UnpackAny(a *codectypes.Any, iface interface{}) error { + return c.Base.UnpackAny(a, iface) +} + +func (c *MockCodec) MarshalJSON(o proto.Message) ([]byte, error) { + if len(c.MarshalJSONErrs) > 0 { + errMsg := c.MarshalJSONErrs[0] + c.MarshalJSONErrs = c.MarshalJSONErrs[1:] + if len(errMsg) > 0 { + return nil, errors.New(errMsg) + } + } + return c.Base.MarshalJSON(o) +} + +func (c *MockCodec) MustMarshalJSON(o proto.Message) []byte { + if len(c.MarshalJSONErrs) > 0 { + errMsg := c.MarshalJSONErrs[0] + c.MarshalJSONErrs = c.MarshalJSONErrs[1:] + if len(errMsg) > 0 { + panic(errors.New(errMsg)) + } + } + return c.Base.MustMarshalJSON(o) +} + +func (c *MockCodec) MarshalInterfaceJSON(i proto.Message) ([]byte, error) { + return c.Base.MarshalInterfaceJSON(i) +} + +func (c *MockCodec) UnmarshalInterfaceJSON(bz []byte, ptr interface{}) error { + return c.Base.UnmarshalInterfaceJSON(bz, ptr) +} + +func (c *MockCodec) UnmarshalJSON(bz []byte, ptr proto.Message) error { + if len(c.UnmarshalJSONErrs) > 0 { + errMsg := c.UnmarshalJSONErrs[0] + c.UnmarshalJSONErrs = c.UnmarshalJSONErrs[1:] + if len(errMsg) > 0 { + return errors.New(errMsg) + } + } + return c.Base.UnmarshalJSON(bz, ptr) +} + +func (c *MockCodec) MustUnmarshalJSON(bz []byte, ptr proto.Message) { + if len(c.UnmarshalJSONErrs) > 0 { + errMsg := c.UnmarshalJSONErrs[0] + c.UnmarshalJSONErrs = c.UnmarshalJSONErrs[1:] + if len(errMsg) > 0 { + panic(errors.New(errMsg)) + } + } + c.Base.MustUnmarshalJSON(bz, ptr) +} diff --git a/testutil/network.go b/testutil/network.go index a0400b4c29..b35fab9bb1 100644 --- a/testutil/network.go +++ b/testutil/network.go @@ -62,6 +62,10 @@ func DefaultTestNetworkConfig() testnet.Config { } func CleanUp(n *testnet.Network, t *testing.T) { + if n == nil { + t.Log("nothing to tear down") + return + } t.Log("teardown waiting for next block") //nolint:errcheck // The test shouldn't fail because cleanup was a problem. So ignoring any error from this. n.WaitForNextBlock()