diff --git a/.golangci.yml b/.golangci.yml index 93f78604f2..939417dce3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -56,6 +56,9 @@ issues: - text: "SA1019: codec.LegacyAmino is deprecated" linters: - staticcheck + - text: "SA1019: collection." + linters: + - staticcheck max-issues-per-linter: 10000 max-same-issues: 10000 diff --git a/CHANGELOG.md b/CHANGELOG.md index 515a20e631..97c113fbb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (x/foundation) [\#528](https://github.com/line/lbm-sdk/pull/528) add a feature of whitelist for /lbm.foundation.v1.MsgWithdrawFromTreasury * (proto) [\#584](https://github.com/line/lbm-sdk/pull/564) remove `prove` field in the `GetTxsEventRequest` of `tx` proto * (x/collection) [\#571](https://github.com/line/lbm-sdk/pull/571) add x/collection proto +* (x/collection) [\#574](https://github.com/line/lbm-sdk/pull/574) implement x/collection ### Improvements diff --git a/simapp/app.go b/simapp/app.go index dfbaf36114..87452f2a53 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -51,6 +51,9 @@ import ( "github.com/line/lbm-sdk/x/capability" capabilitykeeper "github.com/line/lbm-sdk/x/capability/keeper" capabilitytypes "github.com/line/lbm-sdk/x/capability/types" + "github.com/line/lbm-sdk/x/collection" + collectionkeeper "github.com/line/lbm-sdk/x/collection/keeper" + collectionmodule "github.com/line/lbm-sdk/x/collection/module" "github.com/line/lbm-sdk/x/crisis" crisiskeeper "github.com/line/lbm-sdk/x/crisis/keeper" crisistypes "github.com/line/lbm-sdk/x/crisis/types" @@ -157,6 +160,7 @@ var ( authzmodule.AppModuleBasic{}, vesting.AppModuleBasic{}, tokenmodule.AppModuleBasic{}, + collectionmodule.AppModuleBasic{}, wasm.AppModuleBasic{}, ) @@ -218,6 +222,7 @@ type SimApp struct { TransferKeeper ibctransferkeeper.Keeper FeeGrantKeeper feegrantkeeper.Keeper TokenKeeper tokenkeeper.Keeper + CollectionKeeper collectionkeeper.Keeper WasmKeeper wasm.Keeper // make scoped keepers public for test purposes @@ -279,6 +284,7 @@ func NewSimApp( foundation.StoreKey, class.StoreKey, token.StoreKey, + collection.StoreKey, authzkeeper.StoreKey, wasm.StoreKey, ) @@ -347,6 +353,7 @@ func NewSimApp( classKeeper := classkeeper.NewKeeper(appCodec, keys[class.StoreKey]) app.TokenKeeper = tokenkeeper.NewKeeper(appCodec, keys[token.StoreKey], app.AccountKeeper, classKeeper) + app.CollectionKeeper = collectionkeeper.NewKeeper(appCodec, keys[collection.StoreKey], app.AccountKeeper, classKeeper) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -470,6 +477,7 @@ func NewSimApp( ibc.NewAppModule(app.IBCKeeper), params.NewAppModule(app.ParamsKeeper), tokenmodule.NewAppModule(appCodec, app.TokenKeeper), + collectionmodule.NewAppModule(appCodec, app.CollectionKeeper), authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), transferModule, ) @@ -500,6 +508,7 @@ func NewSimApp( ibchost.ModuleName, ibctransfertypes.ModuleName, token.ModuleName, + collection.ModuleName, wasm.ModuleName, ) app.mm.SetOrderEndBlockers( @@ -523,6 +532,7 @@ func NewSimApp( ibctransfertypes.ModuleName, foundation.ModuleName, token.ModuleName, + collection.ModuleName, wasm.ModuleName, ) @@ -553,6 +563,7 @@ func NewSimApp( vestingtypes.ModuleName, foundation.ModuleName, token.ModuleName, + collection.ModuleName, // wasm after ibc transfer wasm.ModuleName, ) diff --git a/x/collection/client/cli/query.go b/x/collection/client/cli/query.go new file mode 100644 index 0000000000..25ba5b583c --- /dev/null +++ b/x/collection/client/cli/query.go @@ -0,0 +1,870 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/line/lbm-sdk/client" + "github.com/line/lbm-sdk/client/flags" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/version" + "github.com/line/lbm-sdk/x/collection" +) + +const ( + FlagTokenID = "token-id" +) + +// NewQueryCmd returns the cli query commands for this module +func NewQueryCmd() *cobra.Command { + queryCmd := &cobra.Command{ + Use: collection.ModuleName, + Short: fmt.Sprintf("Querying commands for the %s module", collection.ModuleName), + Long: "", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + queryCmd.AddCommand( + NewQueryCmdBalances(), + NewQueryCmdSupply(), + NewQueryCmdMinted(), + NewQueryCmdBurnt(), + NewQueryCmdContract(), + NewQueryCmdContracts(), + NewQueryCmdNFT(), + // NewQueryCmdNFTs(), + NewQueryCmdOwner(), + NewQueryCmdRoot(), + NewQueryCmdParent(), + NewQueryCmdChildren(), + NewQueryCmdGrant(), + NewQueryCmdGranteeGrants(), + NewQueryCmdAuthorization(), + NewQueryCmdOperatorAuthorizations(), + ) + + return queryCmd +} + +func NewQueryCmdBalances() *cobra.Command { + cmd := &cobra.Command{ + Use: "balances [contract-id] [address]", + Args: cobra.ExactArgs(2), + Short: "query for token balances by a given address", + Example: fmt.Sprintf(`$ %s query %s balances [contract-id] [address]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + address := args[1] + if err := sdk.ValidateAccAddress(address); err != nil { + return err + } + + tokenID, err := cmd.Flags().GetString(FlagTokenID) + if err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + if len(tokenID) == 0 { + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + + req := &collection.QueryAllBalancesRequest{ + ContractId: contractID, + Address: address, + Pagination: pageReq, + } + res, err := queryClient.AllBalances(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + } + + if err := collection.ValidateTokenID(tokenID); err != nil { + return err + } + + req := &collection.QueryBalanceRequest{ + ContractId: contractID, + Address: address, + TokenId: tokenID, + } + res, err := queryClient.Balance(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + cmd.Flags().String(FlagTokenID, "", "Token ID to query for") + flags.AddPaginationFlagsToCmd(cmd, "all balances") + + return cmd +} + +func NewQueryCmdSupply() *cobra.Command { + cmd := &cobra.Command{ + Use: "supply [contract-id] [class-id]", + Args: cobra.ExactArgs(2), + Short: "query the supply of tokens of the class", + Example: fmt.Sprintf(`$ %s query %s supply [contract-id] [class-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + classID := args[1] + if err := collection.ValidateClassID(classID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QuerySupplyRequest{ + ContractId: contractID, + ClassId: classID, + } + res, err := queryClient.Supply(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdMinted() *cobra.Command { + cmd := &cobra.Command{ + Use: "minted [contract-id] [class-id]", + Args: cobra.ExactArgs(2), + Short: "query the minted tokens of the class", + Example: fmt.Sprintf(`$ %s query %s minted [contract-id] [class-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + classID := args[1] + if err := collection.ValidateClassID(classID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryMintedRequest{ + ContractId: contractID, + ClassId: classID, + } + res, err := queryClient.Minted(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdBurnt() *cobra.Command { + cmd := &cobra.Command{ + Use: "burnt [contract-id] [class-id]", + Args: cobra.ExactArgs(2), + Short: "query the burnt tokens of the class", + Example: fmt.Sprintf(`$ %s query %s burnt [contract-id] [class-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + classID := args[1] + if err := collection.ValidateClassID(classID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryBurntRequest{ + ContractId: contractID, + ClassId: classID, + } + res, err := queryClient.Burnt(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdContract() *cobra.Command { + cmd := &cobra.Command{ + Use: "contract [contract-id]", + Args: cobra.ExactArgs(1), + Short: "query token metadata based on its id", + Example: fmt.Sprintf(`$ %s query %s contract [contract-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryContractRequest{ + ContractId: contractID, + } + res, err := queryClient.Contract(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdContracts() *cobra.Command { + cmd := &cobra.Command{ + Use: "contracts", + Args: cobra.NoArgs, + Short: "query all contract metadata", + Example: fmt.Sprintf(`$ %s query %s contracts`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + req := &collection.QueryContractsRequest{ + Pagination: pageReq, + } + res, err := queryClient.Contracts(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "contracts") + return cmd +} + +func NewQueryCmdFTClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "ft-class [contract-id] [class-id]", + Args: cobra.ExactArgs(2), + Short: "query ft class metadata based on its id", + Example: fmt.Sprintf(`$ %s query %s ft-class [contract-id] [class-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + classID := args[1] + if err := collection.ValidateClassID(classID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryFTClassRequest{ + ContractId: contractID, + ClassId: classID, + } + res, err := queryClient.FTClass(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdNFTClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "nft-class [contract-id] [class-id]", + Args: cobra.ExactArgs(2), + Short: "query nft class metadata based on its id", + Example: fmt.Sprintf(`$ %s query %s nft-class [contract-id] [class-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + classID := args[1] + if err := collection.ValidateClassID(classID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryNFTClassRequest{ + ContractId: contractID, + ClassId: classID, + } + res, err := queryClient.NFTClass(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdTokenClassTypeName() *cobra.Command { + cmd := &cobra.Command{ + Use: "token-class-type-name [contract-id] [class-id]", + Args: cobra.ExactArgs(2), + Short: "query token class type name based on its id", + Example: fmt.Sprintf(`$ %s query %s token-class-type-name [contract-id] [class-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + classID := args[1] + if err := collection.ValidateClassID(classID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryTokenClassTypeNameRequest{ + ContractId: contractID, + ClassId: classID, + } + res, err := queryClient.TokenClassTypeName(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +// func NewQueryCmdTokenClasses() *cobra.Command { +// cmd := &cobra.Command{ +// Use: "classes [contract-id]", +// Args: cobra.ExactArgs(1), +// Short: "query all token class metadata", +// Example: fmt.Sprintf(`$ %s query %s classes [contract-id]`, version.AppName, collection.ModuleName), +// RunE: func(cmd *cobra.Command, args []string) error { +// clientCtx, err := client.GetClientQueryContext(cmd) +// if err != nil { +// return err +// } + +// contractID := args[0] +// if err := collection.ValidateContractID(contractID); err != nil { +// return err +// } + +// queryClient := collection.NewQueryClient(clientCtx) +// pageReq, err := client.ReadPageRequest(cmd.Flags()) +// if err != nil { +// return err +// } +// req := &collection.QueryTokenClassesRequest{ +// ContractId: contractID, +// Pagination: pageReq, +// } +// res, err := queryClient.TokenClasses(cmd.Context(), req) +// if err != nil { +// return err +// } +// return clientCtx.PrintProto(res) +// }, +// } + +// flags.AddQueryFlagsToCmd(cmd) +// flags.AddPaginationFlagsToCmd(cmd, "classes") +// return cmd +// } + +func NewQueryCmdNFT() *cobra.Command { + cmd := &cobra.Command{ + Use: "nft [contract-id] [token-id]", + Args: cobra.ExactArgs(2), + Short: "query nft metadata based on its id", + Example: fmt.Sprintf(`$ %s query %s nft [contract-id] [token-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + tokenID := args[1] + if err := collection.ValidateTokenID(tokenID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryNFTRequest{ + ContractId: contractID, + TokenId: tokenID, + } + res, err := queryClient.NFT(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +// func NewQueryCmdNFTs() *cobra.Command { +// cmd := &cobra.Command{ +// Use: "nfts [contract-id]", +// Args: cobra.ExactArgs(1), +// Short: "query all nft metadata", +// Example: fmt.Sprintf(`$ %s query %s nfts [contract-id]`, version.AppName, collection.ModuleName), +// RunE: func(cmd *cobra.Command, args []string) error { +// clientCtx, err := client.GetClientQueryContext(cmd) +// if err != nil { +// return err +// } + +// contractID := args[0] +// if err := collection.ValidateContractID(contractID); err != nil { +// return err +// } + +// queryClient := collection.NewQueryClient(clientCtx) +// pageReq, err := client.ReadPageRequest(cmd.Flags()) +// if err != nil { +// return err +// } +// req := &collection.QueryNFTsRequest{ +// ContractId: contractID, +// Pagination: pageReq, +// } +// res, err := queryClient.NFTs(cmd.Context(), req) +// if err != nil { +// return err +// } +// return clientCtx.PrintProto(res) +// }, +// } + +// flags.AddQueryFlagsToCmd(cmd) +// flags.AddPaginationFlagsToCmd(cmd, "nfts") +// return cmd +// } + +func NewQueryCmdOwner() *cobra.Command { + cmd := &cobra.Command{ + Use: "owner [contract-id] [token-id]", + Args: cobra.ExactArgs(2), + Short: "query owner of an nft", + Example: fmt.Sprintf(`$ %s query %s owner [contract-id] [token-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + tokenID := args[1] + if err := collection.ValidateNFTID(tokenID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryOwnerRequest{ + ContractId: contractID, + TokenId: tokenID, + } + res, err := queryClient.Owner(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdRoot() *cobra.Command { + cmd := &cobra.Command{ + Use: "root [contract-id] [token-id]", + Args: cobra.ExactArgs(2), + Short: "query root of an nft", + Example: fmt.Sprintf(`$ %s query %s root [contract-id] [token-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + tokenID := args[1] + if err := collection.ValidateNFTID(tokenID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryRootRequest{ + ContractId: contractID, + TokenId: tokenID, + } + res, err := queryClient.Root(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdParent() *cobra.Command { + cmd := &cobra.Command{ + Use: "parent [contract-id] [token-id]", + Args: cobra.ExactArgs(2), + Short: "query parent of an nft", + Example: fmt.Sprintf(`$ %s query %s parent [contract-id] [token-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + tokenID := args[1] + if err := collection.ValidateNFTID(tokenID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryParentRequest{ + ContractId: contractID, + TokenId: tokenID, + } + res, err := queryClient.Parent(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdChildren() *cobra.Command { + cmd := &cobra.Command{ + Use: "children [contract-id] [token-id]", + Args: cobra.ExactArgs(2), + Short: "query children of an nft", + Example: fmt.Sprintf(`$ %s query %s children [contract-id] [token-id]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + tokenID := args[1] + if err := collection.ValidateNFTID(tokenID); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + req := &collection.QueryChildrenRequest{ + ContractId: contractID, + TokenId: tokenID, + Pagination: pageReq, + } + res, err := queryClient.Children(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "children") + return cmd +} + +func NewQueryCmdGrant() *cobra.Command { + cmd := &cobra.Command{ + Use: "grant [contract-id] [grantee] [permission]", + Args: cobra.ExactArgs(3), + Short: "query a permission on a given grantee", + Example: fmt.Sprintf(`$ %s query %s grant [contract-id] [grantee] [permission]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + grantee := args[1] + if err := sdk.ValidateAccAddress(grantee); err != nil { + return err + } + + permission := collection.Permission(collection.Permission_value[args[2]]) + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryGrantRequest{ + ContractId: contractID, + Grantee: grantee, + Permission: permission, + } + res, err := queryClient.Grant(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdGranteeGrants() *cobra.Command { + cmd := &cobra.Command{ + Use: "grantee-grants [contract-id] [grantee]", + Args: cobra.ExactArgs(2), + Short: "query grants on a given grantee", + Example: fmt.Sprintf(`$ %s query %s grantee-grants [contract-id] [grantee]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + grantee := args[1] + if err := sdk.ValidateAccAddress(grantee); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryGranteeGrantsRequest{ + ContractId: contractID, + Grantee: grantee, + } + res, err := queryClient.GranteeGrants(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdAuthorization() *cobra.Command { + cmd := &cobra.Command{ + Use: "authorization [contract-id] [operator] [holder]", + Args: cobra.ExactArgs(3), + Short: "query authorization on its operator and the token holder", + Example: fmt.Sprintf(`$ %s query %s authorization [contract-id] [operator] [holder]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + operator := args[1] + if err := sdk.ValidateAccAddress(operator); err != nil { + return err + } + + holder := args[2] + if err := sdk.ValidateAccAddress(holder); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + req := &collection.QueryAuthorizationRequest{ + ContractId: contractID, + Operator: operator, + Holder: holder, + } + res, err := queryClient.Authorization(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + return cmd +} + +func NewQueryCmdOperatorAuthorizations() *cobra.Command { + cmd := &cobra.Command{ + Use: "operator-authorizations [contract-id] [operator]", + Args: cobra.ExactArgs(2), + Short: "query all authorizations on a given operator", + Example: fmt.Sprintf(`$ %s query %s operator-authorizations [contract-id] [operator]`, version.AppName, collection.ModuleName), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + contractID := args[0] + if err := collection.ValidateContractID(contractID); err != nil { + return err + } + + operator := args[1] + if err := sdk.ValidateAccAddress(operator); err != nil { + return err + } + + queryClient := collection.NewQueryClient(clientCtx) + pageReq, err := client.ReadPageRequest(cmd.Flags()) + if err != nil { + return err + } + req := &collection.QueryOperatorAuthorizationsRequest{ + ContractId: contractID, + Operator: operator, + Pagination: pageReq, + } + res, err := queryClient.OperatorAuthorizations(cmd.Context(), req) + if err != nil { + return err + } + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + flags.AddPaginationFlagsToCmd(cmd, "authorizations") + return cmd +} diff --git a/x/collection/client/cli/tx.go b/x/collection/client/cli/tx.go new file mode 100644 index 0000000000..8f19740f38 --- /dev/null +++ b/x/collection/client/cli/tx.go @@ -0,0 +1,920 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/line/lbm-sdk/client" + "github.com/line/lbm-sdk/client/flags" + "github.com/line/lbm-sdk/client/tx" + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" + "github.com/line/lbm-sdk/version" + "github.com/line/lbm-sdk/x/collection" +) + +const ( + // common flags for the entities + FlagName = "name" + FlagMeta = "meta" + + // flag for contracts + FlagBaseImgURI = "base-img-uri" + + // flag for fungible token classes + FlagDecimals = "decimals" + FlagTo = "to" + FlagSupply = "supply" + + DefaultDecimals = 8 + DefaultSupply = "0" +) + +// NewTxCmd returns the transaction commands for this module +func NewTxCmd() *cobra.Command { + txCmd := &cobra.Command{ + Use: collection.ModuleName, + Short: fmt.Sprintf("%s transactions subcommands", collection.ModuleName), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + txCmd.AddCommand( + NewTxCmdSend(), + NewTxCmdOperatorSend(), + NewTxCmdCreateFTClass(), + NewTxCmdCreateNFTClass(), + NewTxCmdMintFT(), + NewTxCmdMintNFT(), + NewTxCmdBurn(), + NewTxCmdOperatorBurn(), + NewTxCmdModifyContract(), + NewTxCmdModifyTokenClass(), + NewTxCmdModifyNFT(), + NewTxCmdAttach(), + NewTxCmdDetach(), + NewTxCmdOperatorAttach(), + NewTxCmdOperatorDetach(), + NewTxCmdGrant(), + NewTxCmdAbandon(), + NewTxCmdAuthorizeOperator(), + NewTxCmdRevokeOperator(), + ) + + return txCmd +} + +func NewTxCmdSend() *cobra.Command { + cmd := &cobra.Command{ + Use: "send [contract-id] [from] [to] [amount]", + Args: cobra.ExactArgs(4), + Short: "send tokens", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s send [contract-id] [from] [to] [amount]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + from := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, from); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + amountStr := args[3] + amount, err := collection.ParseCoins(amountStr) + if err != nil { + return err + } + + msg := &collection.MsgSend{ + ContractId: args[0], + From: from, + To: args[2], + Amount: amount, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdOperatorSend() *cobra.Command { + cmd := &cobra.Command{ + Use: "operator-send [contract-id] [operator] [from] [to] [amount]", + Args: cobra.ExactArgs(5), + Short: "send tokens by operator", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s operator-send [contract-id] [operator] [from] [to] [amount]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + amountStr := args[4] + amount, err := collection.ParseCoins(amountStr) + if err != nil { + return err + } + + msg := collection.MsgOperatorSend{ + ContractId: args[0], + Operator: operator, + From: args[2], + To: args[3], + Amount: amount, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdCreateContract() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-contract [creator]", + Args: cobra.ExactArgs(1), + Short: "create a contract", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s create-contract [creator]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + creator := args[0] + if err := cmd.Flags().Set(flags.FlagFrom, creator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + name, err := cmd.Flags().GetString(FlagName) + if err != nil { + return err + } + + baseImgURI, err := cmd.Flags().GetString(FlagBaseImgURI) + if err != nil { + return err + } + + meta, err := cmd.Flags().GetString(FlagMeta) + if err != nil { + return err + } + + msg := collection.MsgCreateContract{ + Owner: creator, + Name: name, + BaseImgUri: baseImgURI, + Meta: meta, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + cmd.Flags().String(FlagName, "", "set name") + cmd.Flags().String(FlagBaseImgURI, "", "set base-img-uri") + cmd.Flags().String(FlagMeta, "", "set meta") + + return cmd +} + +func NewTxCmdCreateFTClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-ft-class [contract-id] [operator]", + Args: cobra.ExactArgs(2), + Short: "create a fungible token class", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s create-ft-class [contract-id] [operator]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + name, err := cmd.Flags().GetString(FlagName) + if err != nil { + return err + } + + meta, err := cmd.Flags().GetString(FlagMeta) + if err != nil { + return err + } + + decimals, err := cmd.Flags().GetInt32(FlagDecimals) + if err != nil { + return err + } + + supplyStr, err := cmd.Flags().GetString(FlagSupply) + if err != nil { + return err + } + supply, ok := sdk.NewIntFromString(supplyStr) + if !ok { + return sdkerrors.ErrInvalidType.Wrapf("failed to set supply: %s", supplyStr) + } + + to, err := cmd.Flags().GetString(FlagTo) + if err != nil { + return err + } + + msg := collection.MsgCreateFTClass{ + ContractId: args[0], + Operator: operator, + Name: name, + Meta: meta, + Decimals: decimals, + To: to, + Supply: supply, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + cmd.Flags().String(FlagName, "", "set name") + cmd.Flags().String(FlagMeta, "", "set meta") + cmd.Flags().String(FlagTo, "", "address to send the initial supply") + cmd.Flags().String(FlagSupply, DefaultSupply, "initial supply") + cmd.Flags().Int32(FlagDecimals, DefaultDecimals, "set decimals") + + return cmd +} + +func NewTxCmdCreateNFTClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-nft-class [contract-id] [operator]", + Args: cobra.ExactArgs(2), + Short: "create a non-fungible token class", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s create-nft-class [contract-id] [operator]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + name, err := cmd.Flags().GetString(FlagName) + if err != nil { + return err + } + + meta, err := cmd.Flags().GetString(FlagMeta) + if err != nil { + return err + } + + msg := collection.MsgCreateNFTClass{ + ContractId: args[0], + Operator: operator, + Name: name, + Meta: meta, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + cmd.Flags().String(FlagName, "", "set name") + cmd.Flags().String(FlagMeta, "", "set meta") + + return cmd +} + +func NewTxCmdMintFT() *cobra.Command { + cmd := &cobra.Command{ + Use: "mint-ft [contract-id] [operator] [to] [class-id] [amount]", + Args: cobra.ExactArgs(5), + Short: "mint fungible tokens", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s mint-ft [contract-id] [operator] [to] [class-id] [amount]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + amountStr := args[4] + amount, ok := sdk.NewIntFromString(amountStr) + if !ok { + return sdkerrors.ErrInvalidType.Wrapf("failed to set amount: %s", amountStr) + } + + coins := collection.NewCoins(collection.NewFTCoin(args[3], amount)) + msg := collection.MsgMintFT{ + ContractId: args[0], + From: args[1], + To: args[2], + Amount: coins, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdMintNFT() *cobra.Command { + cmd := &cobra.Command{ + Use: "mint-nft [contract-id] [operator] [to] [class-id]", + Args: cobra.ExactArgs(4), + Short: "mint non-fungible tokens", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s mint-nft [contract-id] [operator] [to] [class-id]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + name, err := cmd.Flags().GetString(FlagName) + if err != nil { + return err + } + + meta, err := cmd.Flags().GetString(FlagMeta) + if err != nil { + return err + } + + params := []collection.MintNFTParam{{ + TokenType: args[3], + Name: name, + Meta: meta, + }} + + msg := collection.MsgMintNFT{ + ContractId: args[0], + From: args[1], + To: args[2], + Params: params, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + cmd.Flags().String(FlagName, "", "set name") + cmd.Flags().String(FlagMeta, "", "set meta") + + return cmd +} + +func NewTxCmdBurn() *cobra.Command { + cmd := &cobra.Command{ + Use: "burn [contract-id] [from] [amount]", + Args: cobra.ExactArgs(3), + Short: "burn tokens", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s burn [contract-id] [from] [amount]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + from := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, from); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + amountStr := args[2] + amount, err := collection.ParseCoins(amountStr) + if err != nil { + return err + } + + msg := collection.MsgBurn{ + ContractId: args[0], + From: from, + Amount: amount, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdOperatorBurn() *cobra.Command { + cmd := &cobra.Command{ + Use: "operator-burn [contract-id] [operator] [from] [amount]", + Args: cobra.ExactArgs(4), + Short: "burn tokens by a given operator", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s operator-burn [contract-id] [operator] [from] [amount]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + amountStr := args[3] + amount, err := collection.ParseCoins(amountStr) + if err != nil { + return err + } + + msg := collection.MsgOperatorBurn{ + ContractId: args[0], + Operator: operator, + From: args[2], + Amount: amount, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdModifyContract() *cobra.Command { + cmd := &cobra.Command{ + Use: "modify-contract [contract-id] [operator] [key] [value]", + Args: cobra.ExactArgs(4), + Short: "modify contract metadata", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s modify-contract [contract-id] [operator] [key] [value]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + changes := []collection.Attribute{{ + Key: args[2], + Value: args[3], + }} + msg := collection.MsgModifyContract{ + ContractId: args[0], + Operator: args[1], + Changes: changes, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdModifyTokenClass() *cobra.Command { + cmd := &cobra.Command{ + Use: "modify-token-class [contract-id] [operator] [class-id] [key] [value]", + Args: cobra.ExactArgs(5), + Short: "modify token class metadata", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s modify-token-class [contract-id] [operator] [class-id] [key] [value]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + changes := []collection.Attribute{{ + Key: args[3], + Value: args[4], + }} + msg := collection.MsgModifyTokenClass{ + ContractId: args[0], + Operator: args[1], + ClassId: args[2], + Changes: changes, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdModifyNFT() *cobra.Command { + cmd := &cobra.Command{ + Use: "modify-nft [contract-id] [operator] [token-id] [key] [value]", + Args: cobra.ExactArgs(5), + Short: "modify nft metadata", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s modify-nft [contract-id] [operator] [token-id] [key] [value]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + changes := []collection.Attribute{{ + Key: args[3], + Value: args[4], + }} + msg := collection.MsgModifyNFT{ + ContractId: args[0], + Operator: args[1], + TokenId: args[2], + Changes: changes, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdAttach() *cobra.Command { + cmd := &cobra.Command{ + Use: "attach [contract-id] [holder] [subject] [target]", + Args: cobra.ExactArgs(4), + Short: "attach a token to another", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s attach [contract-id] [holder] [subject] [target]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + holder := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, holder); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := collection.MsgAttach{ + ContractId: args[0], + From: holder, + TokenId: args[2], + ToTokenId: args[3], + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdDetach() *cobra.Command { + cmd := &cobra.Command{ + Use: "detach [contract-id] [holder] [subject]", + Args: cobra.ExactArgs(3), + Short: "detach a token from its parent", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s detach [contract-id] [holder] [subject]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + holder := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, holder); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := collection.MsgDetach{ + ContractId: args[0], + From: holder, + TokenId: args[2], + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdOperatorAttach() *cobra.Command { + cmd := &cobra.Command{ + Use: "operator-attach [contract-id] [operator] [holder] [subject] [target]", + Args: cobra.ExactArgs(5), + Short: "attach a token to another by the operator", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s operator-attach [contract-id] [operator] [holder] [subject] [target]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := collection.MsgOperatorAttach{ + ContractId: args[0], + Operator: operator, + Owner: args[2], + Subject: args[3], + Target: args[4], + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdOperatorDetach() *cobra.Command { + cmd := &cobra.Command{ + Use: "operator-detach [contract-id] [operator] [holder] [subject]", + Args: cobra.ExactArgs(4), + Short: "detach a token from its parent by the operator", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s operator-detach [contract-id] [operator] [holder] [subject]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + operator := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, operator); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := collection.MsgOperatorDetach{ + ContractId: args[0], + Operator: operator, + Owner: args[2], + Subject: args[3], + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdGrant() *cobra.Command { + cmd := &cobra.Command{ + Use: "grant [contract-id] [granter] [grantee] [permission]", + Args: cobra.ExactArgs(4), + Short: "grant a permission for mint, burn, modify and issue", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s grant [contract-id] [granter] [grantee] [permission]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + granter := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, granter); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + permission := collection.Permission(collection.Permission_value[args[3]]) + + msg := collection.MsgGrant{ + ContractId: args[0], + Granter: granter, + Grantee: args[2], + Permission: permission, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdAbandon() *cobra.Command { + cmd := &cobra.Command{ + Use: "abandon [contract-id] [grantee] [permission]", + Args: cobra.ExactArgs(3), + Short: "abandon a permission by a given grantee", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s abandon [contract-id] [grantee] [permission]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + grantee := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, grantee); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + permission := collection.Permission(collection.Permission_value[args[2]]) + + msg := collection.MsgAbandon{ + ContractId: args[0], + Grantee: grantee, + Permission: permission, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdAuthorizeOperator() *cobra.Command { + cmd := &cobra.Command{ + Use: "authorize-operator [contract-id] [holder] [operator]", + Args: cobra.ExactArgs(3), + Short: "authorize operator to manipulate tokens of holder", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s authorize-operator [contract-id] [holder] [operator]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + holder := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, holder); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := collection.MsgAuthorizeOperator{ + ContractId: args[0], + Holder: holder, + Operator: args[2], + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} + +func NewTxCmdRevokeOperator() *cobra.Command { + cmd := &cobra.Command{ + Use: "revoke-operator [contract-id] [holder] [operator]", + Args: cobra.ExactArgs(3), + Short: "revoke operator", + Long: strings.TrimSpace(fmt.Sprintf(` + $ %s tx %s revoke-operator [contract-id] [holder] [operator]`, version.AppName, collection.ModuleName), + ), + RunE: func(cmd *cobra.Command, args []string) error { + holder := args[1] + if err := cmd.Flags().Set(flags.FlagFrom, holder); err != nil { + return err + } + + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := collection.MsgRevokeOperator{ + ContractId: args[0], + Holder: holder, + Operator: args[2], + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd +} diff --git a/x/collection/client/testutil/cli_test.go b/x/collection/client/testutil/cli_test.go new file mode 100644 index 0000000000..f979202abe --- /dev/null +++ b/x/collection/client/testutil/cli_test.go @@ -0,0 +1,18 @@ +//go:build norace +// +build norace + +package testutil + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/line/lbm-sdk/testutil/network" +) + +func TestIntegrationTestSuite(t *testing.T) { + cfg := network.DefaultConfig() + cfg.NumValidators = 1 + suite.Run(t, NewIntegrationTestSuite(cfg)) +} diff --git a/x/collection/client/testutil/query.go b/x/collection/client/testutil/query.go new file mode 100644 index 0000000000..7545cacb16 --- /dev/null +++ b/x/collection/client/testutil/query.go @@ -0,0 +1,1126 @@ +package testutil + +import ( + "fmt" + + "github.com/gogo/protobuf/proto" + ostcli "github.com/line/ostracon/libs/cli" + + "github.com/line/lbm-sdk/client/flags" + clitestutil "github.com/line/lbm-sdk/testutil/cli" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/types/query" + "github.com/line/lbm-sdk/x/collection" + "github.com/line/lbm-sdk/x/collection/client/cli" +) + +func (s *IntegrationTestSuite) TestNewQueryCmdBalance() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.customer.String(), + fmt.Sprintf("--%s=%s", cli.FlagTokenID, collection.NewFTID(s.ftClassID)), + }, + true, + &collection.QueryBalanceResponse{ + Balance: collection.NewFTCoin(s.ftClassID, s.balance), + }, + }, + "extra args": { + []string{ + s.contractID, + s.customer.String(), + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "invalid address": { + []string{ + s.contractID, + "", + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdBalances() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryBalanceResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdSupply() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.ftClassID, + }, + true, + &collection.QuerySupplyResponse{ + Supply: s.balance.Mul(sdk.NewInt(3)), + }, + }, + "extra args": { + []string{ + s.contractID, + s.ftClassID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "invalid contract id": { + []string{ + "", + s.ftClassID, + }, + false, + nil, + }, + "invalid class id": { + []string{ + s.contractID, + "", + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdSupply() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QuerySupplyResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdMinted() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.ftClassID, + }, + true, + &collection.QueryMintedResponse{ + Minted: s.balance.Mul(sdk.NewInt(4)), + }, + }, + "extra args": { + []string{ + s.contractID, + s.ftClassID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "invalid contract id": { + []string{ + "", + s.ftClassID, + }, + false, + nil, + }, + "invalid class id": { + []string{ + s.contractID, + "", + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdMinted() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryMintedResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdBurnt() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.ftClassID, + }, + true, + &collection.QueryBurntResponse{ + Burnt: s.balance, + }, + }, + "extra args": { + []string{ + s.contractID, + s.ftClassID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "invalid contract id": { + []string{ + "", + s.ftClassID, + }, + false, + nil, + }, + "invalid class id": { + []string{ + s.contractID, + "", + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdBurnt() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryBurntResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdContract() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + }, + true, + &collection.QueryContractResponse{ + Contract: collection.Contract{ContractId: s.contractID}, + }, + }, + "extra args": { + []string{ + s.contractID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{}, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdContract() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryContractResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdContracts() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "query all": { + []string{}, + true, + &collection.QueryContractsResponse{ + Contracts: []collection.Contract{{ContractId: s.contractID}}, + Pagination: &query.PageResponse{}, + }, + }, + "extra args": { + []string{ + "extra", + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdContracts() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryContractsResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdFTClass() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.ftClassID, + }, + true, + &collection.QueryFTClassResponse{ + Class: collection.FTClass{ + Id: s.ftClassID, + Decimals: 8, + Mintable: true, + }, + }, + }, + "extra args": { + []string{ + s.contractID, + s.ftClassID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "class not found": { + []string{ + s.contractID, + "00bab10c", + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdFTClass() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryFTClassResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdNFTClass() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.nftClassID, + }, + true, + &collection.QueryNFTClassResponse{ + Class: collection.NFTClass{Id: s.nftClassID}, + }, + }, + "extra args": { + []string{ + s.contractID, + s.nftClassID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "class not found": { + []string{ + s.contractID, + "deadbeef", + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdNFTClass() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryNFTClassResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdNFT() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + tokenID := collection.NewNFTID(s.nftClassID, 1) + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + tokenID, + }, + true, + &collection.QueryNFTResponse{ + Token: collection.NFT{ + Id: tokenID, + }, + }, + }, + "extra args": { + []string{ + s.contractID, + tokenID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "token not found": { + []string{ + s.contractID, + collection.NewNFTID("deadbeef", 1), + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdNFT() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryNFTResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdRoot() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + tokenID := collection.NewNFTID(s.nftClassID, 2) + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + tokenID, + }, + true, + &collection.QueryRootResponse{ + Root: collection.NFT{ + Id: collection.NewNFTID(s.nftClassID, 1), + }, + }, + }, + "extra args": { + []string{ + s.contractID, + tokenID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "token not found": { + []string{ + s.contractID, + collection.NewNFTID("deadbeef", 1), + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdRoot() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryRootResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdParent() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + tokenID := collection.NewNFTID(s.nftClassID, 2) + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + tokenID, + }, + true, + &collection.QueryParentResponse{ + Parent: collection.NFT{ + Id: collection.NewNFTID(s.nftClassID, 1), + }, + }, + }, + "extra args": { + []string{ + s.contractID, + tokenID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "token not found": { + []string{ + s.contractID, + collection.NewNFTID("deadbeef", 1), + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdParent() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryParentResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdChildren() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + tokenID := collection.NewNFTID(s.nftClassID, 1) + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + tokenID, + }, + true, + &collection.QueryChildrenResponse{ + Children: []collection.NFT{{ + Id: collection.NewNFTID(s.nftClassID, 2), + }}, + Pagination: &query.PageResponse{}, + }, + }, + "extra args": { + []string{ + s.contractID, + tokenID, + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + "token not found": { + []string{ + s.contractID, + collection.NewNFTID("deadbeef", 1), + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdChildren() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryChildrenResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdGrant() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.operator.String(), + collection.PermissionIssue.String(), + }, + true, + &collection.QueryGrantResponse{ + Grant: collection.Grant{ + Grantee: s.operator.String(), + Permission: collection.PermissionIssue, + }, + }, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + collection.PermissionIssue.String(), + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + }, + false, + nil, + }, + "no permission found": { + []string{ + s.contractID, + s.customer.String(), + collection.PermissionIssue.String(), + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdGrant() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryGrantResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdGranteeGrants() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.operator.String(), + }, + true, + &collection.QueryGranteeGrantsResponse{ + Grants: []collection.Grant{ + { + Grantee: s.operator.String(), + Permission: collection.PermissionIssue, + }, + { + Grantee: s.operator.String(), + Permission: collection.PermissionModify, + }, + { + Grantee: s.operator.String(), + Permission: collection.PermissionMint, + }, + { + Grantee: s.operator.String(), + Permission: collection.PermissionBurn, + }, + }, + Pagination: &query.PageResponse{ + Total: 4, + }, + }, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdGranteeGrants() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryGranteeGrantsResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdAuthorization() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + }, + true, + &collection.QueryAuthorizationResponse{ + Authorization: collection.Authorization{ + Holder: s.customer.String(), + Operator: s.operator.String(), + }, + }, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + }, + false, + nil, + }, + "no authorization found": { + []string{ + s.contractID, + s.customer.String(), + s.vendor.String(), + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdAuthorization() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryAuthorizationResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} + +func (s *IntegrationTestSuite) TestNewQueryCmdOperatorAuthorizations() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=%d", flags.FlagHeight, s.setupHeight), + fmt.Sprintf("--%s=json", ostcli.OutputFlag), + } + + testCases := map[string]struct { + args []string + valid bool + expected proto.Message + }{ + "valid query": { + []string{ + s.contractID, + s.operator.String(), + }, + true, + &collection.QueryOperatorAuthorizationsResponse{ + Authorizations: []collection.Authorization{ + { + Holder: s.customer.String(), + Operator: s.operator.String(), + }, + }, + Pagination: &query.PageResponse{}, + }, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + "extra", + }, + false, + nil, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + nil, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewQueryCmdOperatorAuthorizations() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var actual collection.QueryOperatorAuthorizationsResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &actual), out.String()) + s.Require().Equal(tc.expected, &actual) + }) + } +} diff --git a/x/collection/client/testutil/suite.go b/x/collection/client/testutil/suite.go new file mode 100644 index 0000000000..965bd044d6 --- /dev/null +++ b/x/collection/client/testutil/suite.go @@ -0,0 +1,338 @@ +package testutil + +import ( + "fmt" + + "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/suite" + + "github.com/line/lbm-sdk/client/flags" + "github.com/line/lbm-sdk/crypto/hd" + "github.com/line/lbm-sdk/crypto/keyring" + clitestutil "github.com/line/lbm-sdk/testutil/cli" + "github.com/line/lbm-sdk/testutil/network" + sdk "github.com/line/lbm-sdk/types" + bankcli "github.com/line/lbm-sdk/x/bank/client/cli" + "github.com/line/lbm-sdk/x/collection" + "github.com/line/lbm-sdk/x/collection/client/cli" + abci "github.com/line/ostracon/abci/types" +) + +type IntegrationTestSuite struct { + suite.Suite + + cfg network.Config + network *network.Network + + setupHeight int64 + + vendor sdk.AccAddress + operator sdk.AccAddress + customer sdk.AccAddress + + contractID string + ftClassID string + nftClassID string + + balance sdk.Int + + lenChain int +} + +var commonArgs = []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))).String()), +} + +func NewIntegrationTestSuite(cfg network.Config) *IntegrationTestSuite { + return &IntegrationTestSuite{cfg: cfg} +} + +func (s *IntegrationTestSuite) SetupSuite() { + s.T().Log("setting up integration test suite") + + s.network = network.New(s.T(), s.cfg) + _, err := s.network.WaitForHeight(1) + s.Require().NoError(err) + + s.vendor = s.createAccount("vendor") + s.operator = s.createAccount("operator") + s.customer = s.createAccount("customer") + + s.balance = sdk.NewInt(1000000) + + // vendor creates 2 token classes + s.contractID = s.createContract(s.vendor) + s.ftClassID = s.createFTClass(s.contractID, s.vendor) + s.nftClassID = s.createNFTClass(s.contractID, s.vendor) + + // mint & burn fts + for _, to := range []sdk.AccAddress{s.customer, s.operator, s.vendor} { + s.mintFT(s.contractID, s.vendor, to, s.ftClassID, s.balance) + + if to == s.vendor { + tokenID := collection.NewFTID(s.ftClassID) + amount := collection.NewCoins(collection.NewCoin(tokenID, s.balance)) + s.burn(s.contractID, s.vendor, amount) + s.mintFT(s.contractID, s.vendor, to, s.ftClassID, s.balance) + } + } + + // mint nfts + s.lenChain = 2 + for _, to := range []sdk.AccAddress{s.customer, s.vendor} { + // mint N chains per account + numChains := 3 + for n := 0; n < numChains; n++ { + ids := make([]string, s.lenChain) + for i := range ids { + ids[i] = s.mintNFT(s.contractID, s.vendor, to, s.nftClassID) + } + + for i := range ids[1:] { + r := len(ids) - 1 - i + subject := ids[r] + target := ids[r-1] + s.attach(s.contractID, to, subject, target) + } + } + } + + // grant all the permissions to operator + for _, pv := range collection.Permission_value { + permission := collection.Permission(pv) + if permission == collection.PermissionUnspecified { + continue + } + s.grant(s.contractID, s.vendor, s.operator, permission) + } + + // customer approves the operator to manipulate its tokens, so vendor can do OperatorXXX (Send or Burn) later. + s.authorizeOperator(s.contractID, s.customer, s.operator) + // for the revocation. + s.authorizeOperator(s.contractID, s.operator, s.vendor) + + s.setupHeight, err = s.network.LatestHeight() + s.Require().NoError(err) + s.Require().NoError(s.network.WaitForNextBlock()) +} + +func (s *IntegrationTestSuite) pickEvent(events []abci.Event, event proto.Message, fn func(event proto.Message)) { + getType := func(msg proto.Message) string { + return proto.MessageName(msg) + } + + for _, e := range events { + if e.Type == getType(event) { + msg, err := sdk.ParseTypedEvent(e) + s.Require().NoError(err) + + fn(msg) + return + } + } + + s.Require().Failf("event not found", "%s", events) +} + +func (s *IntegrationTestSuite) createContract(creator sdk.AccAddress) string { + val := s.network.Validators[0] + args := append([]string{ + creator.String(), + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdCreateContract(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + + var event collection.EventCreatedContract + s.pickEvent(res.Events, &event, func(e proto.Message) { + event = *e.(*collection.EventCreatedContract) + }) + return event.ContractId +} + +func (s *IntegrationTestSuite) createFTClass(contractID string, operator sdk.AccAddress) string { + val := s.network.Validators[0] + args := append([]string{ + contractID, + operator.String(), + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdCreateFTClass(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + + var event collection.EventCreatedFTClass + s.pickEvent(res.Events, &event, func(e proto.Message) { + event = *e.(*collection.EventCreatedFTClass) + }) + return event.ClassId +} + +func (s *IntegrationTestSuite) createNFTClass(contractID string, operator sdk.AccAddress) string { + val := s.network.Validators[0] + args := append([]string{ + contractID, + operator.String(), + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdCreateNFTClass(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + + var event collection.EventCreatedNFTClass + s.pickEvent(res.Events, &event, func(e proto.Message) { + event = *e.(*collection.EventCreatedNFTClass) + }) + return event.ClassId +} + +func (s *IntegrationTestSuite) mintFT(contractID string, operator, to sdk.AccAddress, classID string, amount sdk.Int) { + val := s.network.Validators[0] + args := append([]string{ + contractID, + operator.String(), + to.String(), + classID, + amount.String(), + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdMintFT(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) +} + +func (s *IntegrationTestSuite) mintNFT(contractID string, operator, to sdk.AccAddress, classID string) string { + val := s.network.Validators[0] + args := append([]string{ + contractID, + operator.String(), + to.String(), + classID, + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdMintNFT(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + + var event collection.EventMintedNFT + s.pickEvent(res.Events, &event, func(e proto.Message) { + event = *e.(*collection.EventMintedNFT) + }) + + s.Require().Equal(1, len(event.Tokens)) + return event.Tokens[0].Id +} + +func (s *IntegrationTestSuite) burn(contractID string, from sdk.AccAddress, amount collection.Coins) { + val := s.network.Validators[0] + args := append([]string{ + contractID, + from.String(), + amount.String(), + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdBurn(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) +} + +func (s *IntegrationTestSuite) attach(contractID string, holder sdk.AccAddress, subject, target string) { + val := s.network.Validators[0] + args := append([]string{ + contractID, + holder.String(), + subject, + target, + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdAttach(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) +} + +func (s *IntegrationTestSuite) grant(contractID string, granter, grantee sdk.AccAddress, permission collection.Permission) { + val := s.network.Validators[0] + args := append([]string{ + contractID, + granter.String(), + grantee.String(), + permission.String(), + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdGrant(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) +} + +// creates an account and send some coins to it for the future transactions. +func (s *IntegrationTestSuite) createAccount(uid string) sdk.AccAddress { + val := s.network.Validators[0] + keyInfo, _, err := val.ClientCtx.Keyring.NewMnemonic(uid, keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + s.Require().NoError(err) + addr := keyInfo.GetAddress() + + fee := sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(1000000))) + args := append([]string{ + val.Address.String(), + addr.String(), + fee.String(), + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), + }, commonArgs...) + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, bankcli.NewSendTxCmd(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + + return addr +} + +func (s *IntegrationTestSuite) authorizeOperator(contractID string, holder, operator sdk.AccAddress) { + val := s.network.Validators[0] + args := append([]string{ + contractID, + holder.String(), + operator.String(), + fmt.Sprintf("--%s=%s", flags.FlagFrom, holder), + }, commonArgs...) + + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewTxCmdAuthorizeOperator(), args) + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) +} + +func (s *IntegrationTestSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + s.network.Cleanup() +} diff --git a/x/collection/client/testutil/tx.go b/x/collection/client/testutil/tx.go new file mode 100644 index 0000000000..8b85ece555 --- /dev/null +++ b/x/collection/client/testutil/tx.go @@ -0,0 +1,1359 @@ +package testutil + +import ( + "fmt" + + "github.com/line/lbm-sdk/client/flags" + clitestutil "github.com/line/lbm-sdk/testutil/cli" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" + "github.com/line/lbm-sdk/x/collection/client/cli" +) + +func (s *IntegrationTestSuite) TestNewTxCmdSend() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + amount := collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.vendor.String(), + s.customer.String(), + amount.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.vendor.String(), + s.customer.String(), + amount.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.vendor.String(), + s.customer.String(), + }, + false, + }, + "amount out of range": { + []string{ + s.contractID, + s.vendor.String(), + s.customer.String(), + fmt.Sprintf("%s:1%0127d", s.ftClassID, 0), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.vendor.String(), + s.customer.String(), + amount.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdSend() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdOperatorSend() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + amount := collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.vendor.String(), + amount.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.vendor.String(), + amount.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.vendor.String(), + }, + false, + }, + "amount out of range": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.vendor.String(), + fmt.Sprintf("%s:1%0127d", s.ftClassID, 0), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + s.customer.String(), + s.vendor.String(), + amount.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdOperatorSend() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdCreateContract() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.vendor.String(), + }, + true, + }, + "extra args": { + []string{ + s.vendor.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{}, + false, + }, + "invalid creator": { + []string{ + "", + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdCreateContract() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdCreateFTClass() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdCreateFTClass() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdCreateNFTClass() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdCreateNFTClass() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdMintFT() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.ftClassID, + s.balance.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.ftClassID, + s.balance.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.ftClassID, + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + s.customer.String(), + s.ftClassID, + s.balance.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdMintFT() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdMintNFT() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.nftClassID, + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + s.nftClassID, + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + s.customer.String(), + s.nftClassID, + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdMintNFT() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdBurn() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + amount := collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + amount.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + amount.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + amount.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdBurn() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdOperatorBurn() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + amount := collection.NewCoins(collection.NewNFTCoin(s.nftClassID, s.lenChain*2+1)) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + amount.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + amount.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + s.customer.String(), + amount.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdOperatorBurn() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdModifyContract() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + collection.AttributeKeyName.String(), + "fox", + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + collection.AttributeKeyName.String(), + "fox", + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + collection.AttributeKeyName.String(), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + collection.AttributeKeyName.String(), + "fox", + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdModifyContract() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdModifyTokenClass() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.ftClassID, + collection.AttributeKeyName.String(), + "tibetian fox", + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.ftClassID, + collection.AttributeKeyName.String(), + "tibetian fox", + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + s.ftClassID, + collection.AttributeKeyName.String(), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + s.ftClassID, + collection.AttributeKeyName.String(), + "tibetian fox", + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdModifyTokenClass() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdModifyNFT() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + tokenID := collection.NewNFTID(s.nftClassID, 1) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + tokenID, + collection.AttributeKeyName.String(), + "fennec fox 1", + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + tokenID, + collection.AttributeKeyName.String(), + "fennec fox 1", + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + tokenID, + collection.AttributeKeyName.String(), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + tokenID, + collection.AttributeKeyName.String(), + "fennec fox 1", + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdModifyNFT() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdAttach() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + subjectID := collection.NewNFTID(s.nftClassID, s.lenChain*4+1) + targetID := collection.NewNFTID(s.nftClassID, s.lenChain*3+1) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.vendor.String(), + subjectID, + targetID, + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.vendor.String(), + subjectID, + targetID, + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.vendor.String(), + subjectID, + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.vendor.String(), + subjectID, + targetID, + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdAttach() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdDetach() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + subjectID := collection.NewNFTID(s.nftClassID, s.lenChain*5) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.vendor.String(), + subjectID, + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.vendor.String(), + subjectID, + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.vendor.String(), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.vendor.String(), + subjectID, + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdDetach() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdOperatorAttach() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + subjectID := collection.NewNFTID(s.nftClassID, s.lenChain+1) + targetID := collection.NewNFTID(s.nftClassID, 1) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + subjectID, + targetID, + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + subjectID, + targetID, + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + subjectID, + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + s.customer.String(), + subjectID, + targetID, + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdOperatorAttach() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdOperatorDetach() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + subjectID := collection.NewNFTID(s.nftClassID, s.lenChain*2) + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + subjectID, + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + subjectID, + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + }, + false, + }, + "invalid contract id": { + []string{ + "", + s.operator.String(), + s.customer.String(), + subjectID, + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdOperatorDetach() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdGrant() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + collection.PermissionMint.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + collection.PermissionMint.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + s.customer.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdGrant() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdAbandon() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.vendor.String(), + collection.PermissionModify.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.vendor.String(), + collection.PermissionModify.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.vendor.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdAbandon() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdAuthorizeOperator() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.vendor.String(), + s.customer.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.vendor.String(), + s.customer.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.vendor.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdAuthorizeOperator() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestNewTxCmdRevokeOperator() { + val := s.network.Validators[0] + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + } + + testCases := map[string]struct { + args []string + valid bool + }{ + "valid transaction": { + []string{ + s.contractID, + s.operator.String(), + s.vendor.String(), + }, + true, + }, + "extra args": { + []string{ + s.contractID, + s.operator.String(), + s.vendor.String(), + "extra", + }, + false, + }, + "not enough args": { + []string{ + s.contractID, + s.operator.String(), + }, + false, + }, + } + + for name, tc := range testCases { + tc := tc + + s.Run(name, func() { + cmd := cli.NewTxCmdRevokeOperator() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, append(tc.args, commonArgs...)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + var res sdk.TxResponse + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &res), out.String()) + s.Require().EqualValues(0, res.Code, out.String()) + }) + } +} diff --git a/x/collection/codec.go b/x/collection/codec.go new file mode 100644 index 0000000000..90fef2dd8f --- /dev/null +++ b/x/collection/codec.go @@ -0,0 +1,57 @@ +package collection + +import ( + "github.com/line/lbm-sdk/codec/types" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/types/msgservice" +) + +func RegisterInterfaces(registry types.InterfaceRegistry) { + registry.RegisterImplementations((*sdk.Msg)(nil), + &MsgSend{}, + &MsgOperatorSend{}, + &MsgAuthorizeOperator{}, + &MsgRevokeOperator{}, + &MsgCreateContract{}, + &MsgIssueFT{}, + &MsgIssueNFT{}, + &MsgMintFT{}, + &MsgMintNFT{}, + &MsgBurn{}, + &MsgOperatorBurn{}, + &MsgModifyContract{}, + &MsgModifyTokenClass{}, + &MsgModifyNFT{}, + &MsgGrant{}, + &MsgAbandon{}, + &MsgAttach{}, + &MsgOperatorAttach{}, + &MsgDetach{}, + &MsgOperatorDetach{}, + // deprecated msgs + &MsgTransferFT{}, + &MsgTransferFTFrom{}, + &MsgTransferNFT{}, + &MsgTransferNFTFrom{}, + &MsgApprove{}, + &MsgDisapprove{}, + &MsgBurnFT{}, + &MsgBurnFTFrom{}, + &MsgBurnNFT{}, + &MsgBurnNFTFrom{}, + &MsgModify{}, + &MsgGrantPermission{}, + &MsgRevokePermission{}, + &MsgAttachFrom{}, + &MsgDetachFrom{}, + ) + + registry.RegisterInterface( + "lbm.collection.v1.TokenClass", + (*TokenClass)(nil), + &FTClass{}, + &NFTClass{}, + ) + + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) +} diff --git a/x/collection/collection.go b/x/collection/collection.go index 946852280c..f4b2dc1a72 100644 --- a/x/collection/collection.go +++ b/x/collection/collection.go @@ -2,21 +2,157 @@ package collection import ( "fmt" + "regexp" "strings" + proto "github.com/gogo/protobuf/proto" + codectypes "github.com/line/lbm-sdk/codec/types" sdk "github.com/line/lbm-sdk/types" ) -func ValidateTokenID(id string) error { +const ( + prefixLegacyPermission = "LEGACY_PERMISSION_" +) + +// Deprecated: use Permission. +func LegacyPermissionFromString(name string) LegacyPermission { + legacyPermissionName := prefixLegacyPermission + strings.ToUpper(name) + return LegacyPermission(LegacyPermission_value[legacyPermissionName]) +} + +func (x LegacyPermission) String() string { + lenPrefix := len(prefixLegacyPermission) + return strings.ToLower(LegacyPermission_name[int32(x)][lenPrefix:]) +} + +func DefaultNextClassIDs(contractID string) NextClassIDs { + return NextClassIDs{ + ContractId: contractID, + Fungible: sdk.NewUint(0), + NonFungible: sdk.NewUint(1 << 28), // "10000000" + } +} + +func validateParams(params Params) error { + // limits are uint32, so no need to validate them. return nil } -func ValidateNFTID(id string) error { +type TokenClass interface { + proto.Message + + GetId() string + SetId(ids *NextClassIDs) + + SetName(name string) + + SetMeta(meta string) + + ValidateBasic() error +} + +func TokenClassToAny(class TokenClass) *codectypes.Any { + msg := class.(proto.Message) + + any, err := codectypes.NewAnyWithValue(msg) + if err != nil { + panic(err) + } + + return any +} + +func TokenClassFromAny(any *codectypes.Any) TokenClass { + class := any.GetCachedValue().(TokenClass) + return class +} + +func TokenClassUnpackInterfaces(any *codectypes.Any, unpacker codectypes.AnyUnpacker) error { + var class TokenClass + return unpacker.UnpackAny(any, &class) +} + +//----------------------------------------------------------------------------- +// FTClass +var _ TokenClass = (*FTClass)(nil) + +//nolint:golint +func (c *FTClass) SetId(ids *NextClassIDs) { + id := ids.Fungible + ids.Fungible = id.Incr() + c.Id = fmt.Sprintf("%08x", id.Uint64()) +} + +func (c *FTClass) SetName(name string) { + c.Name = name +} + +func (c *FTClass) SetMeta(meta string) { + c.Meta = meta +} + +func (c FTClass) ValidateBasic() error { + if err := ValidateClassID(c.Id); err != nil { + return err + } + + if err := validateName(c.Name); err != nil { + return err + } + if err := validateMeta(c.Meta); err != nil { + return err + } + if err := validateDecimals(c.Decimals); err != nil { + return err + } + + return nil +} + +//----------------------------------------------------------------------------- +// NFTClass +var _ TokenClass = (*NFTClass)(nil) + +//nolint:golint +func (c *NFTClass) SetId(ids *NextClassIDs) { + id := ids.NonFungible + ids.NonFungible = id.Incr() + c.Id = fmt.Sprintf("%08x", id.Uint64()) +} + +func (c *NFTClass) SetName(name string) { + c.Name = name +} + +func (c *NFTClass) SetMeta(meta string) { + c.Meta = meta +} + +func (c NFTClass) ValidateBasic() error { + if err := ValidateClassID(c.Id); err != nil { + return err + } + + if err := validateName(c.Name); err != nil { + return err + } + if err := validateMeta(c.Meta); err != nil { + return err + } + return nil } //----------------------------------------------------------------------------- // Coin +func NewFTCoin(classID string, amount sdk.Int) Coin { + return NewCoin(NewFTID(classID), amount) +} + +func NewNFTCoin(classID string, number int) Coin { + return NewCoin(NewNFTID(classID, number), sdk.OneInt()) +} + func NewCoin(id string, amount sdk.Int) Coin { coin := Coin{ TokenId: id, @@ -60,6 +196,27 @@ func (c Coin) isNil() bool { return c.Amount.IsNil() } +var reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s%s):([[:digit:]]+)$`, patternClassID, patternAll)) + +func ParseCoin(coinStr string) (*Coin, error) { + coinStr = strings.TrimSpace(coinStr) + + matches := reDecCoin.FindStringSubmatch(coinStr) + if matches == nil { + return nil, fmt.Errorf("invalid coin expression: %s", coinStr) + } + + id, amountStr := matches[1], matches[2] + + amount, ok := sdk.NewIntFromString(amountStr) + if !ok { + return nil, fmt.Errorf("failed to parse coin amount: %s", amountStr) + } + + coin := NewCoin(id, amount) + return &coin, nil +} + //----------------------------------------------------------------------------- // Coins type Coins []Coin @@ -108,3 +265,28 @@ func (coins Coins) ValidateBasic() error { return nil } + +func ParseCoins(coinsStr string) (Coins, error) { + coinsStr = strings.TrimSpace(coinsStr) + if len(coinsStr) == 0 { + return nil, fmt.Errorf("invalid string for coins") + } + + coinStrs := strings.Split(coinsStr, ",") + coins := make(Coins, len(coinStrs)) + for i, coinStr := range coinStrs { + coin, err := ParseCoin(coinStr) + if err != nil { + return nil, err + } + + coins[i] = *coin + } + + return NewCoins(coins...), nil +} + +// Deprecated: do not use +type Token interface { + proto.Message +} diff --git a/x/collection/collection_test.go b/x/collection/collection_test.go new file mode 100644 index 0000000000..a600d76cef --- /dev/null +++ b/x/collection/collection_test.go @@ -0,0 +1,173 @@ +package collection_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +func TestFTClass(t *testing.T) { + nextIDs := collection.DefaultNextClassIDs("deadbeef") + testCases := map[string]struct { + id string + name string + meta string + decimals int32 + valid bool + }{ + "valid class": { + valid: true, + }, + "invalid id": { + id: "invalid", + }, + "invalid name": { + name: string(make([]rune, 21)), + }, + "invalid meta": { + meta: string(make([]rune, 1001)), + }, + "invalid decimals": { + decimals: 19, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + var class collection.TokenClass + class = &collection.FTClass{ + Id: tc.id, + Decimals: tc.decimals, + } + + if len(tc.id) == 0 { + class.SetId(&nextIDs) + } + class.SetName(tc.name) + class.SetMeta(tc.meta) + + err := class.ValidateBasic() + if !tc.valid { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestNFTClass(t *testing.T) { + nextIDs := collection.DefaultNextClassIDs("deadbeef") + testCases := map[string]struct { + name string + meta string + valid bool + }{ + "valid class": { + valid: true, + }, + "invalid name": { + name: string(make([]rune, 21)), + }, + "invalid meta": { + meta: string(make([]rune, 1001)), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + var class collection.TokenClass + class = &collection.NFTClass{} + class.SetId(&nextIDs) + class.SetName(tc.name) + class.SetMeta(tc.meta) + + err := class.ValidateBasic() + if !tc.valid { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestParseCoin(t *testing.T) { + testCases := map[string]struct { + input string + valid bool + expected collection.Coin + }{ + "valid coin": { + input: "00bab10c00000000:10", + valid: true, + expected: collection.NewFTCoin("00bab10c", sdk.NewInt(10)), + }, + "invalid expression": { + input: "oobabloc00000000:10", + }, + "invalid amount": { + input: "00bab10c00000000:" + fmt.Sprintf("1%0127d", 0), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + parsed, err := collection.ParseCoin(tc.input) + if !tc.valid { + require.Error(t, err) + return + } + require.NoError(t, err) + + require.Equal(t, tc.expected, *parsed) + require.Equal(t, tc.input, parsed.String()) + }) + } +} + +func TestParseCoins(t *testing.T) { + testCases := map[string]struct { + input string + valid bool + expected collection.Coins + }{ + "valid single coins": { + input: "00bab10c00000000:10", + valid: true, + expected: collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.NewInt(10)), + ), + }, + "valid multiple coins": { + input: "deadbeef00000001:1,00bab10c00000000:10", + valid: true, + expected: collection.NewCoins( + collection.NewNFTCoin("deadbeef", 1), + collection.NewFTCoin("00bab10c", sdk.NewInt(10)), + ), + }, + "empty string": {}, + "invalid coin": { + input: "oobabloc00000000:10", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + parsed, err := collection.ParseCoins(tc.input) + if !tc.valid { + require.Error(t, err) + return + } + require.NoError(t, err) + + require.Equal(t, tc.expected, parsed) + require.Equal(t, tc.input, parsed.String()) + }) + } +} diff --git a/x/collection/event.go b/x/collection/event.go new file mode 100644 index 0000000000..3828ad9ad6 --- /dev/null +++ b/x/collection/event.go @@ -0,0 +1,736 @@ +package collection + +import ( + "fmt" + "strings" + + sdk "github.com/line/lbm-sdk/types" +) + +const ( + prefixEventType = "EVENT_TYPE_" + prefixAttributeKey = "ATTRIBUTE_KEY_" +) + +func (x EventType) String() string { + lenPrefix := len(prefixEventType) + return strings.ToLower(EventType_name[int32(x)][lenPrefix:]) +} + +// Deprecated: use typed events. +func EventTypeFromString(name string) EventType { + eventTypeName := prefixEventType + strings.ToUpper(name) + return EventType(EventType_value[eventTypeName]) +} + +func (x AttributeKey) String() string { + lenPrefix := len(prefixAttributeKey) + return strings.ToLower(AttributeKey_name[int32(x)][lenPrefix:]) +} + +func AttributeKeyFromString(name string) AttributeKey { + attributeKeyName := prefixAttributeKey + strings.ToUpper(name) + return AttributeKey(AttributeKey_value[attributeKeyName]) +} + +// Deprecated: use EventCreatedContract. +func NewEventCreateCollection(event EventCreatedContract, creator sdk.AccAddress) sdk.Event { + eventType := EventTypeCreateCollection.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyName: event.Name, + AttributeKeyMeta: event.Meta, + AttributeKeyBaseImgURI: event.BaseImgUri, + + AttributeKeyOwner: creator.String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventCreatedFTClass. +func NewEventIssueFT(event EventCreatedFTClass, operator, to sdk.AccAddress, amount sdk.Int) sdk.Event { + eventType := EventTypeIssueFT.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyTokenID: NewFTID(event.ClassId), + AttributeKeyName: event.Name, + AttributeKeyMeta: event.Meta, + AttributeKeyDecimals: fmt.Sprintf("%d", event.Decimals), + AttributeKeyMintable: fmt.Sprintf("%t", event.Mintable), + + AttributeKeyOwner: operator.String(), + AttributeKeyTo: to.String(), + AttributeKeyAmount: amount.String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventCreatedNFTClass. +func NewEventIssueNFT(event EventCreatedNFTClass) sdk.Event { + eventType := EventTypeIssueNFT.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyTokenType: event.ClassId, + AttributeKeyName: event.Name, + AttributeKeyMeta: event.Meta, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventMintedFT. +func NewEventMintFT(event EventMintedFT) sdk.Event { + eventType := EventTypeMintFT.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.Operator, + AttributeKeyTo: event.To, + AttributeKeyAmount: Coins(event.Amount).String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventMintedNFT. +func NewEventMintNFT(event EventMintedNFT) sdk.Events { + eventType := EventTypeMintNFT.String() + + res := make(sdk.Events, len(event.Tokens)) + for i, token := range event.Tokens { + e := sdk.NewEvent(eventType) + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.Operator, + AttributeKeyTo: event.To, + + AttributeKeyTokenID: token.Id, + AttributeKeyName: token.Name, + AttributeKeyMeta: token.Meta, + } + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + e = e.AppendAttributes(attribute) + } + + res[i] = e + } + + return res +} + +// Deprecated: use EventBurned. +func NewEventBurnFT(event EventBurned) *sdk.Event { + eventType := EventTypeBurnFT.String() + + amount := []Coin{} + for _, coin := range event.Amount { + if err := ValidateFTID(coin.TokenId); err == nil { + amount = append(amount, coin) + } + } + if len(amount) == 0 { + return nil + } + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.From, + AttributeKeyAmount: Coins(amount).String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return &res +} + +// Deprecated: use EventBurned. +func NewEventBurnNFT(event EventBurned) sdk.Events { + eventType := EventTypeBurnNFT.String() + + amount := []Coin{} + for _, coin := range event.Amount { + if err := ValidateNFTID(coin.TokenId); err == nil { + amount = append(amount, coin) + } + } + if len(amount) == 0 { + return nil + } + + res := make(sdk.Events, 0, len(amount)+1) + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.From, + } + head := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + head = head.AppendAttributes(attribute) + } + res = append(res, head) + + for _, coin := range amount { + attribute := sdk.NewAttribute(AttributeKeyTokenID.String(), coin.TokenId) + event := sdk.NewEvent(eventType, attribute) + res = append(res, event) + } + + return res +} + +// Deprecated: use EventBurned. +func NewEventBurnFTFrom(event EventBurned) *sdk.Event { + eventType := EventTypeBurnFTFrom.String() + + amount := []Coin{} + for _, coin := range event.Amount { + if err := ValidateFTID(coin.TokenId); err == nil { + amount = append(amount, coin) + } + } + if len(amount) == 0 { + return nil + } + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyProxy: event.Operator, + AttributeKeyFrom: event.From, + AttributeKeyAmount: Coins(amount).String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return &res +} + +// Deprecated: use EventBurned. +func NewEventBurnNFTFrom(event EventBurned) sdk.Events { + eventType := EventTypeBurnNFTFrom.String() + + amount := []Coin{} + for _, coin := range event.Amount { + if err := ValidateNFTID(coin.TokenId); err == nil { + amount = append(amount, coin) + } + } + if len(amount) == 0 { + return nil + } + + res := make(sdk.Events, 0, len(amount)+1) + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyProxy: event.Operator, + AttributeKeyFrom: event.From, + } + head := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + head = head.AppendAttributes(attribute) + } + res = append(res, head) + + for _, coin := range amount { + attribute := sdk.NewAttribute(AttributeKeyTokenID.String(), coin.TokenId) + event := sdk.NewEvent(eventType, attribute) + res = append(res, event) + } + + return res +} + +// Deprecated: use EventModifiedContract +func NewEventModifyCollection(event EventModifiedContract) sdk.Events { + eventType := EventTypeModifyCollection.String() + res := make(sdk.Events, 0, 1+len(event.Changes)) + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + } + head := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + head = head.AppendAttributes(attribute) + } + res = append(res, head) + + for _, pair := range event.Changes { + attribute := sdk.NewAttribute(pair.Key, pair.Value) + event := sdk.NewEvent(eventType, attribute) + res = append(res, event) + } + return res +} + +// Deprecated: use EventModifiedTokenClass +func NewEventModifyTokenType(event EventModifiedTokenClass) sdk.Events { + eventType := EventTypeModifyTokenType.String() + res := make(sdk.Events, 0, 1+len(event.Changes)) + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyTokenType: event.ClassId, + } + head := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + head = head.AppendAttributes(attribute) + } + res = append(res, head) + + for _, pair := range event.Changes { + attribute := sdk.NewAttribute(pair.Key, pair.Value) + event := sdk.NewEvent(eventType, attribute) + res = append(res, event) + } + return res +} + +// Deprecated: use EventModifiedTokenClass +func NewEventModifyTokenOfFTClass(event EventModifiedTokenClass) sdk.Events { + eventType := EventTypeModifyToken.String() + res := make(sdk.Events, 0, 1+len(event.Changes)) + + tokenID := NewFTID(event.ClassId) + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyTokenID: tokenID, + } + head := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + head = head.AppendAttributes(attribute) + } + res = append(res, head) + + for _, pair := range event.Changes { + attribute := sdk.NewAttribute(pair.Key, pair.Value) + event := sdk.NewEvent(eventType, attribute) + res = append(res, event) + } + return res +} + +// Deprecated: use EventModifiedNFT +func NewEventModifyTokenOfNFT(event EventModifiedNFT) sdk.Events { + eventType := EventTypeModifyToken.String() + res := make(sdk.Events, 0, 1+len(event.Changes)) + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyTokenID: event.TokenId, + } + head := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + head = head.AppendAttributes(attribute) + } + res = append(res, head) + + for _, pair := range event.Changes { + attribute := sdk.NewAttribute(pair.Key, pair.Value) + event := sdk.NewEvent(eventType, attribute) + res = append(res, event) + } + return res +} + +// Deprecated: use EventSent. +func NewEventTransferFT(event EventSent) *sdk.Event { + eventType := EventTypeTransferFT.String() + + amount := []Coin{} + for _, coin := range event.Amount { + if err := ValidateFTID(coin.TokenId); err == nil { + amount = append(amount, coin) + } + } + if len(amount) == 0 { + return nil + } + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.From, + AttributeKeyTo: event.To, + AttributeKeyAmount: Coins(amount).String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return &res +} + +// Deprecated: use EventSent. +func NewEventTransferNFT(event EventSent) sdk.Events { + eventType := EventTypeTransferNFT.String() + + amount := []Coin{} + for _, coin := range event.Amount { + if err := ValidateNFTID(coin.TokenId); err == nil { + amount = append(amount, coin) + } + } + if len(amount) == 0 { + return nil + } + + res := make(sdk.Events, 0, 1+len(amount)) + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.From, + AttributeKeyTo: event.To, + } + head := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + head = head.AppendAttributes(attribute) + } + res = append(res, head) + + for _, coin := range amount { + attribute := sdk.NewAttribute(AttributeKeyTokenID.String(), coin.TokenId) + event := sdk.NewEvent(eventType, attribute) + res = append(res, event) + } + return res +} + +// Deprecated: use EventSent. +func NewEventTransferFTFrom(event EventSent) *sdk.Event { + eventType := EventTypeTransferFTFrom.String() + + amount := []Coin{} + for _, coin := range event.Amount { + if err := ValidateFTID(coin.TokenId); err == nil { + amount = append(amount, coin) + } + } + if len(amount) == 0 { + return nil + } + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyProxy: event.Operator, + AttributeKeyFrom: event.From, + AttributeKeyTo: event.To, + AttributeKeyAmount: Coins(amount).String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return &res +} + +// Deprecated: use EventSent. +func NewEventTransferNFTFrom(event EventSent) sdk.Events { + eventType := EventTypeTransferNFTFrom.String() + + amount := []Coin{} + for _, coin := range event.Amount { + if err := ValidateNFTID(coin.TokenId); err == nil { + amount = append(amount, coin) + } + } + if len(amount) == 0 { + return nil + } + + res := make(sdk.Events, 0, 1+len(amount)) + + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyProxy: event.Operator, + AttributeKeyFrom: event.From, + AttributeKeyTo: event.To, + } + head := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + head = head.AppendAttributes(attribute) + } + res = append(res, head) + + for _, coin := range amount { + attribute := sdk.NewAttribute(AttributeKeyTokenID.String(), coin.TokenId) + event := sdk.NewEvent(eventType, attribute) + res = append(res, event) + } + return res +} + +// Deprecated: use EventGrant. +func NewEventGrantPermToken(event EventGrant) sdk.Event { + eventType := EventTypeGrantPermToken.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyTo: event.Grantee, + AttributeKeyPerm: LegacyPermission(event.Permission).String(), + } + if len(event.Granter) != 0 { + attributes[AttributeKeyFrom] = event.Granter + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventGrant. +func NewEventGrantPermTokenHead(event EventGrant) sdk.Event { + eventType := EventTypeGrantPermToken.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyTo: event.Grantee, + } + if len(event.Granter) != 0 { + attributes[AttributeKeyFrom] = event.Granter + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventGrant. +func NewEventGrantPermTokenBody(event EventGrant) sdk.Event { + eventType := EventTypeGrantPermToken.String() + attributes := map[AttributeKey]string{ + AttributeKeyPerm: LegacyPermission(event.Permission).String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventAbandon. +func NewEventRevokePermToken(event EventAbandon) sdk.Event { + eventType := EventTypeRevokePermToken.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.Grantee, + AttributeKeyPerm: LegacyPermission(event.Permission).String(), + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventAuthorizedOperator. +func NewEventApproveCollection(event EventAuthorizedOperator) sdk.Event { + eventType := EventTypeApproveCollection.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyApprover: event.Holder, + AttributeKeyProxy: event.Operator, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventRevokedOperator. +func NewEventDisapproveCollection(event EventRevokedOperator) sdk.Event { + eventType := EventTypeDisapproveCollection.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyApprover: event.Holder, + AttributeKeyProxy: event.Operator, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventAttached. +func NewEventAttachToken(event EventAttached, newRoot string) sdk.Event { + eventType := EventTypeAttachToken.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.Holder, + AttributeKeyTokenID: event.Subject, + AttributeKeyToTokenID: event.Target, + + AttributeKeyOldRoot: event.Subject, + AttributeKeyNewRoot: newRoot, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventDetached. +func NewEventDetachToken(event EventDetached, oldRoot string) sdk.Event { + eventType := EventTypeDetachToken.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyFrom: event.Holder, + AttributeKeyFromTokenID: event.Subject, + + AttributeKeyOldRoot: oldRoot, + AttributeKeyNewRoot: event.Subject, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventAttached. +func NewEventAttachFrom(event EventAttached, newRoot string) sdk.Event { + eventType := EventTypeAttachFrom.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyProxy: event.Operator, + AttributeKeyFrom: event.Holder, + AttributeKeyTokenID: event.Subject, + AttributeKeyToTokenID: event.Target, + + AttributeKeyOldRoot: event.Subject, + AttributeKeyNewRoot: newRoot, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventDetached. +func NewEventDetachFrom(event EventDetached, oldRoot string) sdk.Event { + eventType := EventTypeDetachFrom.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: event.ContractId, + AttributeKeyProxy: event.Operator, + AttributeKeyFrom: event.Holder, + AttributeKeyFromTokenID: event.Subject, + + AttributeKeyOldRoot: oldRoot, + AttributeKeyNewRoot: event.Subject, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: do not use. +func NewEventOperationTransferNFT(contractID string, tokenID string) sdk.Event { + eventType := EventTypeOperationTransferNFT.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: contractID, + AttributeKeyTokenID: tokenID, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: use EventBurned. +func NewEventOperationBurnNFT(contractID string, tokenID string) sdk.Event { + eventType := EventTypeOperationBurnNFT.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: contractID, + AttributeKeyTokenID: tokenID, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} + +// Deprecated: do not use. +func NewEventOperationRootChanged(contractID string, tokenID string) sdk.Event { + eventType := EventTypeOperationRootChanged.String() + attributes := map[AttributeKey]string{ + AttributeKeyContractID: contractID, + AttributeKeyTokenID: tokenID, + } + + res := sdk.NewEvent(eventType) + for key, value := range attributes { + attribute := sdk.NewAttribute(key.String(), value) + res = res.AppendAttributes(attribute) + } + return res +} diff --git a/x/collection/event_test.go b/x/collection/event_test.go new file mode 100644 index 0000000000..c917fb5d64 --- /dev/null +++ b/x/collection/event_test.go @@ -0,0 +1,950 @@ +package collection_test + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +func TestEventTypeStringer(t *testing.T) { + for _, name := range collection.EventType_name { + value := collection.EventType(collection.EventType_value[name]) + customName := value.String() + require.EqualValues(t, value, collection.EventTypeFromString(customName), name) + } +} + +func TestAttributeKeyStringer(t *testing.T) { + for _, name := range collection.AttributeKey_name { + value := collection.AttributeKey(collection.AttributeKey_value[name]) + customName := value.String() + require.EqualValues(t, value, collection.AttributeKeyFromString(customName), name) + } +} + +func randomString(length int) string { + letters := []rune("0123456789abcdef") + res := make([]rune, length) + for i := range res { + res[i] = letters[rand.Intn(len(letters))] + } + return string(res) +} + +func assertAttribute(e sdk.Event, key, value string) bool { + for _, attr := range e.Attributes { + if string(attr.Key) == key { + return string(attr.Value) == value + } + } + return false +} + +func TestNewEventCreateCollection(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventCreatedContract{ + ContractId: str(), + Name: str(), + Meta: str(), + BaseImgUri: str(), + } + creator := sdk.AccAddress(str()) + legacy := collection.NewEventCreateCollection(event, creator) + + require.Equal(t, collection.EventTypeCreateCollection.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyName: event.Name, + collection.AttributeKeyMeta: event.Meta, + collection.AttributeKeyBaseImgURI: event.BaseImgUri, + collection.AttributeKeyOwner: creator.String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventIssueFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventCreatedFTClass{ + ContractId: str(), + ClassId: str(), + Name: str(), + Meta: str(), + Decimals: 0, + Mintable: true, + } + operator := sdk.AccAddress(str()) + to := sdk.AccAddress(str()) + amount := sdk.OneInt() + legacy := collection.NewEventIssueFT(event, operator, to, amount) + + require.Equal(t, collection.EventTypeIssueFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyTokenID: collection.NewFTID(event.ClassId), + collection.AttributeKeyName: event.Name, + collection.AttributeKeyMeta: event.Meta, + collection.AttributeKeyMintable: fmt.Sprintf("%v", event.Mintable), + collection.AttributeKeyDecimals: fmt.Sprintf("%d", event.Decimals), + collection.AttributeKeyAmount: amount.String(), + collection.AttributeKeyOwner: operator.String(), + collection.AttributeKeyTo: to.String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventIssueNFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventCreatedNFTClass{ + ContractId: str(), + ClassId: str(), + Name: str(), + Meta: str(), + } + legacy := collection.NewEventIssueNFT(event) + + require.Equal(t, collection.EventTypeIssueNFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyTokenType: event.ClassId, + collection.AttributeKeyName: event.Name, + collection.AttributeKeyMeta: event.Meta, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventMintFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventMintedFT{ + ContractId: str(), + Operator: str(), + To: str(), + Amount: collection.NewCoins(collection.NewFTCoin(str(), sdk.OneInt())), + } + legacy := collection.NewEventMintFT(event) + + require.Equal(t, collection.EventTypeMintFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.Operator, + collection.AttributeKeyTo: event.To, + collection.AttributeKeyAmount: collection.Coins(event.Amount).String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventMintNFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventMintedNFT{ + ContractId: str(), + Operator: str(), + To: str(), + Tokens: []collection.NFT{{ + Id: str(), + Name: str(), + Meta: str(), + }}, + } + legacies := collection.NewEventMintNFT(event) + + for i, legacy := range legacies { + require.Equal(t, collection.EventTypeMintNFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.Operator, + collection.AttributeKeyTo: event.To, + collection.AttributeKeyTokenID: event.Tokens[i].Id, + collection.AttributeKeyName: event.Tokens[i].Name, + collection.AttributeKeyMeta: event.Tokens[i].Meta, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } + } +} + +func TestNewEventBurnFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + from := str() + event := collection.EventBurned{ + ContractId: str(), + Operator: from, + From: from, + Amount: collection.NewCoins( + collection.NewFTCoin(str(), sdk.OneInt()), + ), + } + legacy := collection.NewEventBurnFT(event) + require.NotNil(t, legacy) + + require.Equal(t, collection.EventTypeBurnFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.From, + collection.AttributeKeyAmount: collection.Coins(event.Amount).String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(*legacy, key.String(), value), key) + } + + empty := collection.NewEventBurnNFT(event) + require.Empty(t, empty) +} + +func TestNewEventBurnNFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + from := str() + event := collection.EventBurned{ + ContractId: str(), + Operator: from, + From: from, + Amount: collection.NewCoins( + collection.NewNFTCoin(str(), 1), + ), + } + legacies := collection.NewEventBurnNFT(event) + require.Greater(t, len(legacies), 1) + + require.Equal(t, collection.EventTypeBurnNFT.String(), legacies[0].Type) + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.From, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacies[0], key.String(), value), key) + } + + for i, legacy := range legacies[1:] { + require.Equal(t, collection.EventTypeBurnNFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyTokenID: event.Amount[i].TokenId, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } + } + + empty := collection.NewEventBurnFT(event) + require.Nil(t, empty) +} + +func TestNewEventBurnFTFrom(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventBurned{ + ContractId: str(), + Operator: str(), + From: str(), + Amount: collection.NewCoins( + collection.NewFTCoin(str(), sdk.OneInt()), + ), + } + legacy := collection.NewEventBurnFTFrom(event) + require.NotNil(t, legacy) + + require.Equal(t, collection.EventTypeBurnFTFrom.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyProxy: event.Operator, + collection.AttributeKeyFrom: event.From, + collection.AttributeKeyAmount: collection.Coins(event.Amount).String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(*legacy, key.String(), value), key) + } + + empty := collection.NewEventBurnNFTFrom(event) + require.Empty(t, empty) +} + +func TestNewEventBurnNFTFrom(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventBurned{ + ContractId: str(), + Operator: str(), + From: str(), + Amount: collection.NewCoins( + collection.NewNFTCoin(str(), 1), + ), + } + legacies := collection.NewEventBurnNFTFrom(event) + require.Greater(t, len(legacies), 1) + + require.Equal(t, collection.EventTypeBurnNFTFrom.String(), legacies[0].Type) + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyProxy: event.Operator, + collection.AttributeKeyFrom: event.From, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacies[0], key.String(), value), key) + } + + for i, legacy := range legacies[1:] { + require.Equal(t, collection.EventTypeBurnNFTFrom.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyTokenID: event.Amount[i].TokenId, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } + } + + empty := collection.NewEventBurnFTFrom(event) + require.Nil(t, empty) +} + +func TestNewEventModifyCollection(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventModifiedContract{ + ContractId: str(), + Operator: str(), + Changes: []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: str(), + }}, + } + legacies := collection.NewEventModifyCollection(event) + require.Greater(t, len(legacies), 1) + + require.Equal(t, collection.EventTypeModifyCollection.String(), legacies[0].Type) + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacies[0], key.String(), value), key) + } + + for i, legacy := range legacies[1:] { + require.Equal(t, collection.EventTypeModifyCollection.String(), legacy.Type) + + attributes := map[string]string{ + event.Changes[i].Key: event.Changes[i].Value, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key, value), key) + } + } +} + +func TestNewEventModifyTokenType(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventModifiedTokenClass{ + ContractId: str(), + Operator: str(), + ClassId: str(), + Changes: []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: str(), + }}, + } + legacies := collection.NewEventModifyTokenType(event) + require.Greater(t, len(legacies), 1) + + require.Equal(t, collection.EventTypeModifyTokenType.String(), legacies[0].Type) + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyTokenType: event.ClassId, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacies[0], key.String(), value), key) + } + + for i, legacy := range legacies[1:] { + require.Equal(t, collection.EventTypeModifyTokenType.String(), legacy.Type) + + attributes := map[string]string{ + event.Changes[i].Key: event.Changes[i].Value, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key, value), key) + } + } +} + +func TestNewEventModifyTokenOfFTClass(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventModifiedTokenClass{ + ContractId: str(), + Operator: str(), + ClassId: str(), + Changes: []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: str(), + }}, + } + legacies := collection.NewEventModifyTokenOfFTClass(event) + require.Greater(t, len(legacies), 1) + + require.Equal(t, collection.EventTypeModifyToken.String(), legacies[0].Type) + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyTokenID: collection.NewFTID(event.ClassId), + } + for key, value := range attributes { + require.True(t, assertAttribute(legacies[0], key.String(), value), key) + } + + for i, legacy := range legacies[1:] { + require.Equal(t, collection.EventTypeModifyToken.String(), legacy.Type) + + attributes := map[string]string{ + event.Changes[i].Key: event.Changes[i].Value, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key, value), key) + } + } +} + +func TestNewEventModifyTokenOfNFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventModifiedNFT{ + ContractId: str(), + Operator: str(), + TokenId: str(), + Changes: []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: str(), + }}, + } + legacies := collection.NewEventModifyTokenOfNFT(event) + require.Greater(t, len(legacies), 1) + + require.Equal(t, collection.EventTypeModifyToken.String(), legacies[0].Type) + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyTokenID: event.TokenId, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacies[0], key.String(), value), key) + } + + for i, legacy := range legacies[1:] { + require.Equal(t, collection.EventTypeModifyToken.String(), legacy.Type) + + attributes := map[string]string{ + event.Changes[i].Key: event.Changes[i].Value, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key, value), key) + } + } +} + +func TestNewEventTransferFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + from := str() + event := collection.EventSent{ + ContractId: str(), + Operator: from, + From: from, + Amount: collection.NewCoins( + collection.NewFTCoin(str(), sdk.OneInt()), + ), + } + legacy := collection.NewEventTransferFT(event) + require.NotNil(t, legacy) + + require.Equal(t, collection.EventTypeTransferFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.From, + collection.AttributeKeyTo: event.To, + collection.AttributeKeyAmount: collection.Coins(event.Amount).String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(*legacy, key.String(), value), key) + } + + empty := collection.NewEventTransferNFT(event) + require.Empty(t, empty) +} + +func TestNewEventTransferNFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + from := str() + event := collection.EventSent{ + ContractId: str(), + Operator: from, + From: from, + Amount: collection.NewCoins( + collection.NewNFTCoin(str(), 1), + ), + } + legacies := collection.NewEventTransferNFT(event) + require.Greater(t, len(legacies), 1) + + require.Equal(t, collection.EventTypeTransferNFT.String(), legacies[0].Type) + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.From, + collection.AttributeKeyTo: event.To, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacies[0], key.String(), value), key) + } + + for i, legacy := range legacies[1:] { + require.Equal(t, collection.EventTypeTransferNFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyTokenID: event.Amount[i].TokenId, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } + } + + empty := collection.NewEventTransferFT(event) + require.Nil(t, empty) +} + +func TestNewEventTransferFTFrom(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventSent{ + ContractId: str(), + Operator: str(), + From: str(), + Amount: collection.NewCoins( + collection.NewFTCoin(str(), sdk.OneInt()), + ), + } + legacy := collection.NewEventTransferFTFrom(event) + require.NotNil(t, legacy) + + require.Equal(t, collection.EventTypeTransferFTFrom.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyProxy: event.Operator, + collection.AttributeKeyFrom: event.From, + collection.AttributeKeyTo: event.To, + collection.AttributeKeyAmount: collection.Coins(event.Amount).String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(*legacy, key.String(), value), key) + } + + empty := collection.NewEventTransferNFTFrom(event) + require.Empty(t, empty) +} + +func TestNewEventTransferNFTFrom(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventSent{ + ContractId: str(), + Operator: str(), + From: str(), + Amount: collection.NewCoins( + collection.NewNFTCoin(str(), 1), + ), + } + legacies := collection.NewEventTransferNFTFrom(event) + require.Greater(t, len(legacies), 1) + + require.Equal(t, collection.EventTypeTransferNFTFrom.String(), legacies[0].Type) + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyProxy: event.Operator, + collection.AttributeKeyFrom: event.From, + collection.AttributeKeyTo: event.To, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacies[0], key.String(), value), key) + } + + for i, legacy := range legacies[1:] { + require.Equal(t, collection.EventTypeTransferNFTFrom.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyTokenID: event.Amount[i].TokenId, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } + } + + empty := collection.NewEventTransferFTFrom(event) + require.Nil(t, empty) +} + +func TestNewEventGrantPermToken(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + permission := func() collection.Permission { + n := len(collection.Permission_value) - 1 + return collection.Permission(1 + rand.Intn(n)) + } + + event := collection.EventGrant{ + ContractId: str(), + Granter: str(), + Grantee: str(), + Permission: permission(), + } + legacy := collection.NewEventGrantPermToken(event) + + require.Equal(t, collection.EventTypeGrantPermToken.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.Granter, + collection.AttributeKeyTo: event.Grantee, + collection.AttributeKeyPerm: collection.LegacyPermission(event.Permission).String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventGrantPermTokenHead(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + permission := func() collection.Permission { + n := len(collection.Permission_value) - 1 + return collection.Permission(1 + rand.Intn(n)) + } + + event := collection.EventGrant{ + ContractId: str(), + Granter: str(), + Grantee: str(), + Permission: permission(), + } + legacy := collection.NewEventGrantPermTokenHead(event) + + require.Equal(t, collection.EventTypeGrantPermToken.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyTo: event.Grantee, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventGrantPermTokenBody(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + permission := func() collection.Permission { + n := len(collection.Permission_value) - 1 + return collection.Permission(1 + rand.Intn(n)) + } + + event := collection.EventGrant{ + ContractId: str(), + Granter: str(), + Grantee: str(), + Permission: permission(), + } + legacy := collection.NewEventGrantPermTokenBody(event) + + require.Equal(t, collection.EventTypeGrantPermToken.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyPerm: collection.LegacyPermission(event.Permission).String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventRevokePermToken(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + permission := func() collection.Permission { + n := len(collection.Permission_value) - 1 + return collection.Permission(1 + rand.Intn(n)) + } + + event := collection.EventAbandon{ + ContractId: str(), + Grantee: str(), + Permission: permission(), + } + legacy := collection.NewEventRevokePermToken(event) + + require.Equal(t, collection.EventTypeRevokePermToken.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.Grantee, + collection.AttributeKeyPerm: collection.LegacyPermission(event.Permission).String(), + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventApproveCollection(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventAuthorizedOperator{ + ContractId: str(), + Holder: str(), + Operator: str(), + } + legacy := collection.NewEventApproveCollection(event) + + require.Equal(t, collection.EventTypeApproveCollection.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyApprover: event.Holder, + collection.AttributeKeyProxy: event.Operator, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventDisapproveCollection(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventRevokedOperator{ + ContractId: str(), + Holder: str(), + Operator: str(), + } + legacy := collection.NewEventDisapproveCollection(event) + + require.Equal(t, collection.EventTypeDisapproveCollection.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyApprover: event.Holder, + collection.AttributeKeyProxy: event.Operator, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventAttachToken(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventAttached{ + ContractId: str(), + Operator: str(), + Holder: str(), + Subject: collection.NewNFTID(str(), 1), + Target: collection.NewNFTID(str(), 2), + } + newRoot := collection.NewNFTID(str(), 3) + legacy := collection.NewEventAttachToken(event, newRoot) + + require.Equal(t, collection.EventTypeAttachToken.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.Holder, + collection.AttributeKeyTokenID: event.Subject, + collection.AttributeKeyToTokenID: event.Target, + collection.AttributeKeyOldRoot: event.Subject, + collection.AttributeKeyNewRoot: newRoot, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventDetachToken(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventDetached{ + ContractId: str(), + Operator: str(), + Holder: str(), + Subject: collection.NewNFTID(str(), 1), + } + oldRoot := collection.NewNFTID(str(), 3) + legacy := collection.NewEventDetachToken(event, oldRoot) + + require.Equal(t, collection.EventTypeDetachToken.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.Holder, + collection.AttributeKeyFromTokenID: event.Subject, + collection.AttributeKeyOldRoot: oldRoot, + collection.AttributeKeyNewRoot: event.Subject, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventAttachFrom(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventAttached{ + ContractId: str(), + Operator: str(), + Holder: str(), + Subject: collection.NewNFTID(str(), 1), + Target: collection.NewNFTID(str(), 2), + } + newRoot := collection.NewNFTID(str(), 3) + legacy := collection.NewEventAttachFrom(event, newRoot) + + require.Equal(t, collection.EventTypeAttachFrom.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.Holder, + collection.AttributeKeyTokenID: event.Subject, + collection.AttributeKeyToTokenID: event.Target, + collection.AttributeKeyOldRoot: event.Subject, + collection.AttributeKeyNewRoot: newRoot, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventDetachFrom(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + event := collection.EventDetached{ + ContractId: str(), + Operator: str(), + Holder: str(), + Subject: collection.NewNFTID(str(), 1), + } + oldRoot := collection.NewNFTID(str(), 3) + legacy := collection.NewEventDetachFrom(event, oldRoot) + + require.Equal(t, collection.EventTypeDetachFrom.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: event.ContractId, + collection.AttributeKeyFrom: event.Holder, + collection.AttributeKeyFromTokenID: event.Subject, + collection.AttributeKeyOldRoot: oldRoot, + collection.AttributeKeyNewRoot: event.Subject, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventOperationTransferNFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + contractID := str() + tokenID := str() + legacy := collection.NewEventOperationTransferNFT(contractID, tokenID) + + require.Equal(t, collection.EventTypeOperationTransferNFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: contractID, + collection.AttributeKeyTokenID: tokenID, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventOperationBurnNFT(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + contractID := str() + tokenID := str() + legacy := collection.NewEventOperationBurnNFT(contractID, tokenID) + + require.Equal(t, collection.EventTypeOperationBurnNFT.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: contractID, + collection.AttributeKeyTokenID: tokenID, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} + +func TestNewEventOperationRootChanged(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + str := func() string { return randomString(8) } + + contractID := str() + tokenID := str() + legacy := collection.NewEventOperationRootChanged(contractID, tokenID) + + require.Equal(t, collection.EventTypeOperationRootChanged.String(), legacy.Type) + + attributes := map[collection.AttributeKey]string{ + collection.AttributeKeyContractID: contractID, + collection.AttributeKeyTokenID: tokenID, + } + for key, value := range attributes { + require.True(t, assertAttribute(legacy, key.String(), value), key) + } +} diff --git a/x/collection/expected_keepers.go b/x/collection/expected_keepers.go new file mode 100644 index 0000000000..458b1e7bdf --- /dev/null +++ b/x/collection/expected_keepers.go @@ -0,0 +1,22 @@ +package collection + +import ( + sdk "github.com/line/lbm-sdk/types" + authtypes "github.com/line/lbm-sdk/x/auth/types" +) + +type ( + // AccountKeeper defines the contract required for account APIs. + AccountKeeper interface { + HasAccount(ctx sdk.Context, addr sdk.AccAddress) bool + SetAccount(ctx sdk.Context, account authtypes.AccountI) + + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + } + + // ClassKeeper defines the contract needed to be fulfilled for class dependencies. + ClassKeeper interface { + NewID(ctx sdk.Context) string + HasID(ctx sdk.Context, id string) bool + } +) diff --git a/x/collection/genesis.go b/x/collection/genesis.go new file mode 100644 index 0000000000..0851203284 --- /dev/null +++ b/x/collection/genesis.go @@ -0,0 +1,228 @@ +package collection + +import ( + codectypes "github.com/line/lbm-sdk/codec/types" + + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" +) + +const ( + DefaultDepthLimit = 3 + DefaultWidthLimit = 8 +) + +// ValidateGenesis check the given genesis state has no integrity issues +func ValidateGenesis(data GenesisState) error { + if err := validateParams(data.Params); err != nil { + return err + } + + for _, contract := range data.Contracts { + if err := ValidateContractID(contract.ContractId); err != nil { + return err + } + + if err := validateName(contract.Name); err != nil { + return err + } + if err := validateBaseImgURI(contract.BaseImgUri); err != nil { + return err + } + if err := validateMeta(contract.Meta); err != nil { + return err + } + } + + for _, nextClassID := range data.NextClassIds { + if err := ValidateContractID(nextClassID.ContractId); err != nil { + return err + } + } + + for _, contractClasses := range data.Classes { + if err := ValidateContractID(contractClasses.ContractId); err != nil { + return err + } + + if len(contractClasses.Classes) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("classes cannot be empty") + } + for i := range contractClasses.Classes { + any := &contractClasses.Classes[i] + class := TokenClassFromAny(any) + if err := class.ValidateBasic(); err != nil { + return err + } + } + } + + for _, contractNextTokenIDs := range data.NextTokenIds { + if err := ValidateContractID(contractNextTokenIDs.ContractId); err != nil { + return err + } + + if len(contractNextTokenIDs.TokenIds) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("next token ids cannot be empty") + } + for _, nextTokenIDs := range contractNextTokenIDs.TokenIds { + if err := ValidateClassID(nextTokenIDs.ClassId); err != nil { + return err + } + } + } + + for _, contractBalances := range data.Balances { + if err := ValidateContractID(contractBalances.ContractId); err != nil { + return err + } + + if len(contractBalances.Balances) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("balances cannot be empty") + } + for _, balance := range contractBalances.Balances { + if err := sdk.ValidateAccAddress(balance.Address); err != nil { + return err + } + if err := balance.Amount.ValidateBasic(); err != nil { + return err + } + } + } + + for _, contractNFTs := range data.Nfts { + if err := ValidateContractID(contractNFTs.ContractId); err != nil { + return err + } + + if len(contractNFTs.Nfts) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("nfts cannot be empty") + } + for _, token := range contractNFTs.Nfts { + if err := ValidateTokenID(token.Id); err != nil { + return err + } + if err := validateName(token.Name); err != nil { + return err + } + if err := validateMeta(token.Meta); err != nil { + return err + } + } + } + + for _, contractParents := range data.Parents { + if err := ValidateContractID(contractParents.ContractId); err != nil { + return err + } + + if len(contractParents.Relations) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("parents cannot be empty") + } + for _, relation := range contractParents.Relations { + if err := ValidateTokenID(relation.Self); err != nil { + return err + } + if err := ValidateTokenID(relation.Other); err != nil { + return err + } + } + } + + for _, contractAuthorizations := range data.Authorizations { + if err := ValidateContractID(contractAuthorizations.ContractId); err != nil { + return err + } + + if len(contractAuthorizations.Authorizations) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("authorizations cannot be empty") + } + for _, authorization := range contractAuthorizations.Authorizations { + if err := sdk.ValidateAccAddress(authorization.Holder); err != nil { + return err + } + if err := sdk.ValidateAccAddress(authorization.Operator); err != nil { + return err + } + } + } + + for _, contractGrants := range data.Grants { + if err := ValidateContractID(contractGrants.ContractId); err != nil { + return err + } + + if len(contractGrants.Grants) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("grants cannot be empty") + } + for _, grant := range contractGrants.Grants { + if err := sdk.ValidateAccAddress(grant.Grantee); err != nil { + return err + } + if err := ValidatePermission(grant.Permission); err != nil { + return err + } + } + } + + for _, contractSupplies := range data.Supplies { + if err := ValidateContractID(contractSupplies.ContractId); err != nil { + return err + } + + if len(contractSupplies.Statistics) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("supplies cannot be empty") + } + for _, supply := range contractSupplies.Statistics { + if err := ValidateClassID(supply.ClassId); err != nil { + return err + } + if !supply.Amount.IsPositive() { + return sdkerrors.ErrInvalidRequest.Wrap("supply must be positive") + } + } + } + + for _, contractBurnts := range data.Burnts { + if err := ValidateContractID(contractBurnts.ContractId); err != nil { + return err + } + + if len(contractBurnts.Statistics) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("burnts cannot be empty") + } + for _, burnt := range contractBurnts.Statistics { + if err := ValidateClassID(burnt.ClassId); err != nil { + return err + } + if !burnt.Amount.IsPositive() { + return sdkerrors.ErrInvalidRequest.Wrap("burnt must be positive") + } + } + } + + return nil +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + Params: Params{ + DepthLimit: DefaultDepthLimit, + WidthLimit: DefaultWidthLimit, + }, + } +} + +func (data GenesisState) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + for _, contractClasses := range data.Classes { + for i := range contractClasses.Classes { + any := &contractClasses.Classes[i] + if err := TokenClassUnpackInterfaces(any, unpacker); err != nil { + return err + } + } + } + + return nil +} diff --git a/x/collection/genesis_test.go b/x/collection/genesis_test.go new file mode 100644 index 0000000000..0767a26522 --- /dev/null +++ b/x/collection/genesis_test.go @@ -0,0 +1,460 @@ +package collection_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + codectypes "github.com/line/lbm-sdk/codec/types" + "github.com/line/lbm-sdk/crypto/keys/secp256k1" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +func TestValidateGenesis(t *testing.T) { + addr := sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + testCases := map[string]struct { + gs *collection.GenesisState + valid bool + }{ + "default genesis": { + collection.DefaultGenesisState(), + true, + }, + "contract of invalid contract id": { + &collection.GenesisState{ + Contracts: []collection.Contract{{ + Name: "tibetian fox", + Meta: "Tibetian Fox", + BaseImgUri: "file:///tibetian_fox.png", + }}, + }, + false, + }, + "contract of invalid name": { + &collection.GenesisState{ + Contracts: []collection.Contract{{ + ContractId: "deadbeef", + Name: string(make([]rune, 21)), + Meta: "Tibetian Fox", + BaseImgUri: "file:///tibetian_fox.png", + }}, + }, + false, + }, + "contract of invalid base img uri": { + &collection.GenesisState{ + Contracts: []collection.Contract{{ + ContractId: "deadbeef", + Name: "tibetian fox", + BaseImgUri: string(make([]rune, 1001)), + Meta: "Tibetian Fox", + }}, + }, + false, + }, + "contract of invalid meta": { + &collection.GenesisState{ + Contracts: []collection.Contract{{ + ContractId: "deadbeef", + Name: "tibetian fox", + BaseImgUri: "file:///tibetian_fox.png", + Meta: string(make([]rune, 1001)), + }}, + }, + false, + }, + "next class ids of invalid contract id": { + &collection.GenesisState{ + NextClassIds: []collection.NextClassIDs{{ + Fungible: sdk.ZeroUint(), + NonFungible: sdk.OneUint(), + }}, + }, + false, + }, + "contract classes of invalid contract id": { + &collection.GenesisState{ + Classes: []collection.ContractClasses{{ + Classes: []codectypes.Any{ + *collection.TokenClassToAny(&collection.NFTClass{ + Id: "deadbeef", + Name: "tibetian fox", + Meta: "Tibetian Fox", + }), + }, + }}, + }, + false, + }, + "contract classes of empty classes": { + &collection.GenesisState{ + Classes: []collection.ContractClasses{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract classes of invalid class": { + &collection.GenesisState{ + Classes: []collection.ContractClasses{{ + ContractId: "deadbeef", + Classes: []codectypes.Any{ + *collection.TokenClassToAny(&collection.NFTClass{ + Name: "tibetian fox", + Meta: "Tibetian Fox", + }), + }, + }}, + }, + false, + }, + "contract next token ids of invalid contract id": { + &collection.GenesisState{ + NextTokenIds: []collection.ContractNextTokenIDs{{ + TokenIds: []collection.NextTokenID{{ + ClassId: "deadbeef", + Id: sdk.ZeroUint(), + }}, + }}, + }, + false, + }, + "contract next token ids of empty classes": { + &collection.GenesisState{ + NextTokenIds: []collection.ContractNextTokenIDs{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract next token ids of invalid class": { + &collection.GenesisState{ + NextTokenIds: []collection.ContractNextTokenIDs{{ + ContractId: "deadbeef", + TokenIds: []collection.NextTokenID{{ + Id: sdk.ZeroUint(), + }}, + }}, + }, + false, + }, + "contract balances of invalid contract id": { + &collection.GenesisState{ + Balances: []collection.ContractBalances{{ + Balances: []collection.Balance{{ + Address: addr.String(), + Amount: collection.NewCoins(collection.NewFTCoin("00bab10c", sdk.OneInt())), + }}, + }}, + }, + false, + }, + "contract balances of empty balances": { + &collection.GenesisState{ + Balances: []collection.ContractBalances{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract balances of invalid address": { + &collection.GenesisState{ + Balances: []collection.ContractBalances{{ + ContractId: "deadbeef", + Balances: []collection.Balance{{ + Amount: collection.NewCoins(collection.NewFTCoin("00bab10c", sdk.OneInt())), + }}, + }}, + }, + false, + }, + "contract balances of invalid amount": { + &collection.GenesisState{ + Balances: []collection.ContractBalances{{ + ContractId: "deadbeef", + Balances: []collection.Balance{{ + Address: addr.String(), + }}, + }}, + }, + false, + }, + "contract nfts of invalid contract id": { + &collection.GenesisState{ + Nfts: []collection.ContractNFTs{{ + Nfts: []collection.NFT{{ + Id: collection.NewNFTID("deadbeef", 1), + Name: "tibetian fox", + Meta: "Tibetian Fox", + }}, + }}, + }, + false, + }, + "contract nfts of empty nfts": { + &collection.GenesisState{ + Nfts: []collection.ContractNFTs{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract nfts of invalid class": { + &collection.GenesisState{ + Nfts: []collection.ContractNFTs{{ + ContractId: "deadbeef", + Nfts: []collection.NFT{{ + Name: "tibetian fox", + Meta: "Tibetian Fox", + }}, + }}, + }, + false, + }, + "contract nfts of invalid name": { + &collection.GenesisState{ + Nfts: []collection.ContractNFTs{{ + ContractId: "deadbeef", + Nfts: []collection.NFT{{ + Id: collection.NewNFTID("deadbeef", 1), + Name: string(make([]rune, 21)), + Meta: "Tibetian Fox", + }}, + }}, + }, + false, + }, + "contract nfts of invalid meta": { + &collection.GenesisState{ + Nfts: []collection.ContractNFTs{{ + ContractId: "deadbeef", + Nfts: []collection.NFT{{ + Id: collection.NewNFTID("deadbeef", 1), + Name: "tibetian fox", + Meta: string(make([]rune, 1001)), + }}, + }}, + }, + false, + }, + "contract parents of invalid contract id": { + &collection.GenesisState{ + Parents: []collection.ContractTokenRelations{{ + Relations: []collection.TokenRelation{{ + Self: collection.NewNFTID("deadbeef", 1), + Other: collection.NewNFTID("fee1dead", 1), + }}, + }}, + }, + false, + }, + "contract parents of empty relations": { + &collection.GenesisState{ + Parents: []collection.ContractTokenRelations{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract parents of invalid token": { + &collection.GenesisState{ + Parents: []collection.ContractTokenRelations{{ + ContractId: "deadbeef", + Relations: []collection.TokenRelation{{ + Other: collection.NewNFTID("fee1dead", 1), + }}, + }}, + }, + false, + }, + "contract parents of invalid parent": { + &collection.GenesisState{ + Parents: []collection.ContractTokenRelations{{ + ContractId: "deadbeef", + Relations: []collection.TokenRelation{{ + Self: collection.NewNFTID("deadbeef", 1), + }}, + }}, + }, + false, + }, + "contract authorizations of invalid contract id": { + &collection.GenesisState{ + Authorizations: []collection.ContractAuthorizations{{ + Authorizations: []collection.Authorization{{ + Holder: addr.String(), + Operator: addr.String(), + }}, + }}, + }, + false, + }, + "contract authorizations of empty authorizations": { + &collection.GenesisState{ + Authorizations: []collection.ContractAuthorizations{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract authorizations of invalid holder": { + &collection.GenesisState{ + Authorizations: []collection.ContractAuthorizations{{ + ContractId: "deadbeef", + Authorizations: []collection.Authorization{{ + Operator: addr.String(), + }}, + }}, + }, + false, + }, + "contract authorizations of invalid operator": { + &collection.GenesisState{ + Authorizations: []collection.ContractAuthorizations{{ + ContractId: "deadbeef", + Authorizations: []collection.Authorization{{ + Holder: addr.String(), + }}, + }}, + }, + false, + }, + "contract grants of invalid contract id": { + &collection.GenesisState{ + Grants: []collection.ContractGrants{{ + Grants: []collection.Grant{{ + Grantee: addr.String(), + Permission: collection.PermissionMint, + }}, + }}, + }, + false, + }, + "contract grants of empty grants": { + &collection.GenesisState{ + Grants: []collection.ContractGrants{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract grants of invalid grantee": { + &collection.GenesisState{ + Grants: []collection.ContractGrants{{ + ContractId: "deadbeef", + Grants: []collection.Grant{{ + Permission: collection.PermissionMint, + }}, + }}, + }, + false, + }, + "contract grants of invalid permission": { + &collection.GenesisState{ + Grants: []collection.ContractGrants{{ + ContractId: "deadbeef", + Grants: []collection.Grant{{ + Grantee: addr.String(), + }}, + }}, + }, + false, + }, + "contract supplies of invalid contract id": { + &collection.GenesisState{ + Supplies: []collection.ContractStatistics{{ + Statistics: []collection.ClassStatistics{{ + ClassId: "deadbeef", + Amount: sdk.OneInt(), + }}, + }}, + }, + false, + }, + "contract supplies of empty supplies": { + &collection.GenesisState{ + Supplies: []collection.ContractStatistics{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract supplies of invalid class id": { + &collection.GenesisState{ + Supplies: []collection.ContractStatistics{{ + ContractId: "deadbeef", + Statistics: []collection.ClassStatistics{{ + Amount: sdk.OneInt(), + }}, + }}, + }, + false, + }, + "contract supplies of invalid operator": { + &collection.GenesisState{ + Supplies: []collection.ContractStatistics{{ + ContractId: "deadbeef", + Statistics: []collection.ClassStatistics{{ + ClassId: "deadbeef", + Amount: sdk.ZeroInt(), + }}, + }}, + }, + false, + }, + "contract burnts of invalid contract id": { + &collection.GenesisState{ + Burnts: []collection.ContractStatistics{{ + Statistics: []collection.ClassStatistics{{ + ClassId: "deadbeef", + Amount: sdk.OneInt(), + }}, + }}, + }, + false, + }, + "contract burnts of empty burnts": { + &collection.GenesisState{ + Burnts: []collection.ContractStatistics{{ + ContractId: "deadbeef", + }}, + }, + false, + }, + "contract burnts of invalid class id": { + &collection.GenesisState{ + Burnts: []collection.ContractStatistics{{ + ContractId: "deadbeef", + Statistics: []collection.ClassStatistics{{ + Amount: sdk.OneInt(), + }}, + }}, + }, + false, + }, + "contract burnts of invalid operator": { + &collection.GenesisState{ + Burnts: []collection.ContractStatistics{{ + ContractId: "deadbeef", + Statistics: []collection.ClassStatistics{{ + ClassId: "deadbeef", + Amount: sdk.ZeroInt(), + }}, + }}, + }, + false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := collection.ValidateGenesis(*tc.gs) + if !tc.valid { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/x/collection/keeper/alias.go b/x/collection/keeper/alias.go new file mode 100644 index 0000000000..33cea464ba --- /dev/null +++ b/x/collection/keeper/alias.go @@ -0,0 +1,274 @@ +package keeper + +import ( + gogotypes "github.com/gogo/protobuf/types" + + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +// iterate through the balances of a contract and perform the provided function +func (k Keeper) iterateContractBalances(ctx sdk.Context, contractID string, fn func(address sdk.AccAddress, balance collection.Coin) (stop bool)) { + k.iterateBalancesImpl(ctx, balanceKeyPrefixByContractID(contractID), func(_ string, address sdk.AccAddress, balance collection.Coin) (stop bool) { + return fn(address, balance) + }) +} + +func (k Keeper) iterateBalancesImpl(ctx sdk.Context, prefix []byte, fn func(contractID string, address sdk.AccAddress, balance collection.Coin) (stop bool)) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + contractID, address, tokenID := splitBalanceKey(iterator.Key()) + + var amount sdk.Int + if err := amount.Unmarshal(iterator.Value()); err != nil { + panic(err) + } + balance := collection.NewCoin(tokenID, amount) + + stop := fn(contractID, address, balance) + if stop { + break + } + } +} + +func (k Keeper) iterateContracts(ctx sdk.Context, fn func(contract collection.Contract) (stop bool)) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, contractKeyPrefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var contract collection.Contract + k.cdc.MustUnmarshal(iterator.Value(), &contract) + + stop := fn(contract) + if stop { + break + } + } +} + +func (k Keeper) iterateContractClasses(ctx sdk.Context, contractID string, fn func(class collection.TokenClass) (stop bool)) { + k.iterateClassesImpl(ctx, classKeyPrefixByContractID(contractID), fn) +} + +// iterate through the classes and perform the provided function +func (k Keeper) iterateClassesImpl(ctx sdk.Context, prefix []byte, fn func(class collection.TokenClass) (stop bool)) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var class collection.TokenClass + if err := k.cdc.UnmarshalInterface(iterator.Value(), &class); err != nil { + panic(err) + } + + stop := fn(class) + if stop { + break + } + } +} + +func (k Keeper) iterateContractGrants(ctx sdk.Context, contractID string, fn func(grant collection.Grant) (stop bool)) { + k.iterateGrantsImpl(ctx, grantKeyPrefixByContractID(contractID), func(_ string, grant collection.Grant) (stop bool) { + return fn(grant) + }) +} + +func (k Keeper) iterateGrantsImpl(ctx sdk.Context, prefix []byte, fn func(contractID string, grant collection.Grant) (stop bool)) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + contractID, grantee, permission := splitGrantKey(iterator.Key()) + grant := collection.Grant{ + Grantee: grantee.String(), + Permission: permission, + } + + stop := fn(contractID, grant) + if stop { + break + } + } +} + +func (k Keeper) iterateContractAuthorizations(ctx sdk.Context, contractID string, fn func(authorization collection.Authorization) (stop bool)) { + k.iterateAuthorizationsImpl(ctx, authorizationKeyPrefixByContractID(contractID), func(_ string, authorization collection.Authorization) (stop bool) { + return fn(authorization) + }) +} + +func (k Keeper) iterateAuthorizationsImpl(ctx sdk.Context, prefix []byte, fn func(contractID string, authorization collection.Authorization) (stop bool)) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + contractID, operator, holder := splitAuthorizationKey(iterator.Key()) + authorization := collection.Authorization{ + Holder: holder.String(), + Operator: operator.String(), + } + + stop := fn(contractID, authorization) + if stop { + break + } + } +} + +func (k Keeper) iterateContractNFTs(ctx sdk.Context, contractID string, fn func(nft collection.NFT) (stop bool)) { + k.iterateNFTsImpl(ctx, nftKeyPrefixByContractID(contractID), func(_ string, nft collection.NFT) (stop bool) { + return fn(nft) + }) +} + +func (k Keeper) iterateNFTsImpl(ctx sdk.Context, prefix []byte, fn func(contractID string, NFT collection.NFT) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, prefix) + + defer iter.Close() + for ; iter.Valid(); iter.Next() { + contractID, _ := splitNFTKey(iter.Key()) + + var nft collection.NFT + k.cdc.MustUnmarshal(iter.Value(), &nft) + + if fn(contractID, nft) { + break + } + } +} + +func (k Keeper) iterateContractParents(ctx sdk.Context, contractID string, fn func(tokenID, parentID string) (stop bool)) { + k.iterateParentsImpl(ctx, parentKeyPrefixByContractID(contractID), func(_ string, tokenID, parentID string) (stop bool) { + return fn(tokenID, parentID) + }) +} + +func (k Keeper) iterateParentsImpl(ctx sdk.Context, prefix []byte, fn func(contractID string, tokenID, parentID string) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, prefix) + + defer iter.Close() + for ; iter.Valid(); iter.Next() { + contractID, tokenID := splitParentKey(iter.Key()) + + var parentID gogotypes.StringValue + k.cdc.MustUnmarshal(iter.Value(), &parentID) + + if fn(contractID, tokenID, parentID.Value) { + break + } + } +} + +func (k Keeper) iterateChildrenImpl(ctx sdk.Context, prefix []byte, fn func(contractID string, tokenID, childID string) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, prefix) + + defer iter.Close() + for ; iter.Valid(); iter.Next() { + contractID, tokenID, childID := splitChildKey(iter.Key()) + if fn(contractID, tokenID, childID) { + break + } + } +} + +func (k Keeper) iterateStatisticsImpl(ctx sdk.Context, prefix []byte, fn func(contractID string, classID string, amount sdk.Int) (stop bool)) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var amount sdk.Int + if err := amount.Unmarshal(iterator.Value()); err != nil { + panic(err) + } + + keyPrefix := prefix[:1] + contractID, classID := splitStatisticKey(keyPrefix, iterator.Key()) + + stop := fn(contractID, classID, amount) + if stop { + break + } + } +} + +func (k Keeper) iterateContractSupplies(ctx sdk.Context, contractID string, fn func(classID string, amount sdk.Int) (stop bool)) { + k.iterateStatisticsImpl(ctx, statisticKeyPrefixByContractID(supplyKeyPrefix, contractID), func(_ string, classID string, amount sdk.Int) (stop bool) { + return fn(classID, amount) + }) +} + +func (k Keeper) iterateContractBurnts(ctx sdk.Context, contractID string, fn func(classID string, amount sdk.Int) (stop bool)) { + k.iterateStatisticsImpl(ctx, statisticKeyPrefixByContractID(burntKeyPrefix, contractID), func(_ string, classID string, amount sdk.Int) (stop bool) { + return fn(classID, amount) + }) +} + +// iterate through the next token class ids and perform the provided function +func (k Keeper) iterateNextTokenClassIDs(ctx sdk.Context, fn func(class collection.NextClassIDs) (stop bool)) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, nextClassIDKeyPrefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var class collection.NextClassIDs + k.cdc.MustUnmarshal(iterator.Value(), &class) + + stop := fn(class) + if stop { + break + } + } +} + +func (k Keeper) iterateContractNextTokenIDs(ctx sdk.Context, contractID string, fn func(nextID collection.NextTokenID) (stop bool)) { + k.iterateNextTokenIDsImpl(ctx, nextTokenIDKeyPrefixByContractID(contractID), func(_ string, nextID collection.NextTokenID) (stop bool) { + return fn(nextID) + }) +} + +// iterate through the next (non-fungible) token ids and perform the provided function +func (k Keeper) iterateNextTokenIDsImpl(ctx sdk.Context, prefix []byte, fn func(contractID string, nextID collection.NextTokenID) (stop bool)) { + store := ctx.KVStore(k.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + contractID, classID := splitNextTokenIDKey(iterator.Key()) + + var id sdk.Uint + if err := id.Unmarshal(iterator.Value()); err != nil { + panic(err) + } + + nextID := collection.NextTokenID{ + ClassId: classID, + Id: id, + } + + stop := fn(contractID, nextID) + if stop { + break + } + } +} diff --git a/x/collection/keeper/genesis.go b/x/collection/keeper/genesis.go new file mode 100644 index 0000000000..3c8e6d5881 --- /dev/null +++ b/x/collection/keeper/genesis.go @@ -0,0 +1,342 @@ +package keeper + +import ( + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +// InitGenesis new collection genesis +func (k Keeper) InitGenesis(ctx sdk.Context, data *collection.GenesisState) { + k.setParams(ctx, data.Params) + + for _, contract := range data.Contracts { + k.setContract(ctx, contract) + } + + for _, nextClassIDs := range data.NextClassIds { + k.setNextClassIDs(ctx, nextClassIDs) + } + + for _, contractClasses := range data.Classes { + contractID := contractClasses.ContractId + + for i := range contractClasses.Classes { + any := &contractClasses.Classes[i] + class := collection.TokenClassFromAny(any) + k.setTokenClass(ctx, contractID, class) + + // legacy + if nftClass, ok := class.(*collection.NFTClass); ok { + k.setLegacyTokenType(ctx, contractID, nftClass.Id) + } + } + } + + for _, contractNextTokenIDs := range data.NextTokenIds { + contractID := contractNextTokenIDs.ContractId + + for _, nextTokenID := range contractNextTokenIDs.TokenIds { + k.setNextTokenID(ctx, contractID, nextTokenID.ClassId, nextTokenID.Id) + } + } + + for _, contractBalances := range data.Balances { + contractID := contractBalances.ContractId + + for _, balance := range contractBalances.Balances { + for _, coin := range balance.Amount { + k.setBalance(ctx, contractID, sdk.AccAddress(balance.Address), coin.TokenId, coin.Amount) + + if err := collection.ValidateNFTID(coin.TokenId); err == nil { + k.setOwner(ctx, contractID, coin.TokenId, sdk.AccAddress(balance.Address)) + } + } + } + } + + for _, contractNFTs := range data.Nfts { + contractID := contractNFTs.ContractId + + for _, nft := range contractNFTs.Nfts { + k.setNFT(ctx, contractID, nft) + } + } + + for _, contractParents := range data.Parents { + contractID := contractParents.ContractId + + for _, relation := range contractParents.Relations { + tokenID := relation.Self + parentID := relation.Other + k.setParent(ctx, contractID, tokenID, parentID) + k.setChild(ctx, contractID, parentID, tokenID) + } + } + + for _, contractAuthorizations := range data.Authorizations { + for _, authorization := range contractAuthorizations.Authorizations { + k.setAuthorization(ctx, contractAuthorizations.ContractId, sdk.AccAddress(authorization.Holder), sdk.AccAddress(authorization.Operator)) + } + } + + for _, contractGrants := range data.Grants { + for _, grant := range contractGrants.Grants { + k.setGrant(ctx, contractGrants.ContractId, sdk.AccAddress(grant.Grantee), grant.Permission) + } + } + + for _, contractBurnts := range data.Burnts { + contractID := contractBurnts.ContractId + for _, burnt := range contractBurnts.Statistics { + k.setBurnt(ctx, contractID, burnt.ClassId, burnt.Amount) + } + } + + for _, contractSupplies := range data.Supplies { + contractID := contractSupplies.ContractId + for _, supply := range contractSupplies.Statistics { + k.setSupply(ctx, contractID, supply.ClassId, supply.Amount) + + // calculate the amount of minted tokens + burnt := k.GetBurnt(ctx, contractID, supply.ClassId) + minted := supply.Amount.Add(burnt) + k.setMinted(ctx, contractID, supply.ClassId, minted) + } + } +} + +// ExportGenesis returns a GenesisState for a given context. +func (k Keeper) ExportGenesis(ctx sdk.Context) *collection.GenesisState { + contracts := k.getContracts(ctx) + + return &collection.GenesisState{ + Contracts: contracts, + NextClassIds: k.getAllNextClassIDs(ctx), + Classes: k.getClasses(ctx, contracts), + NextTokenIds: k.getNextTokenIDs(ctx, contracts), + Balances: k.getBalances(ctx, contracts), + Nfts: k.getNFTs(ctx, contracts), + Parents: k.getParents(ctx, contracts), + Grants: k.getGrants(ctx, contracts), + Authorizations: k.getAuthorizations(ctx, contracts), + Supplies: k.getSupplies(ctx, contracts), + Burnts: k.getBurnts(ctx, contracts), + } +} + +func (k Keeper) getContracts(ctx sdk.Context) []collection.Contract { + var contracts []collection.Contract + k.iterateContracts(ctx, func(contract collection.Contract) (stop bool) { + contracts = append(contracts, contract) + return false + }) + + return contracts +} + +func (k Keeper) getClasses(ctx sdk.Context, contracts []collection.Contract) []collection.ContractClasses { + var classes []collection.ContractClasses + for _, contract := range contracts { + contractID := contract.ContractId + contractClasses := collection.ContractClasses{ + ContractId: contractID, + } + + k.iterateContractClasses(ctx, contractID, func(class collection.TokenClass) (stop bool) { + any := collection.TokenClassToAny(class) + contractClasses.Classes = append(contractClasses.Classes, *any) + return false + }) + if len(contractClasses.Classes) != 0 { + classes = append(classes, contractClasses) + } + } + + return classes +} + +func (k Keeper) getAllNextClassIDs(ctx sdk.Context) []collection.NextClassIDs { + var nextIDs []collection.NextClassIDs + k.iterateNextTokenClassIDs(ctx, func(ids collection.NextClassIDs) (stop bool) { + nextIDs = append(nextIDs, ids) + return false + }) + + return nextIDs +} + +func (k Keeper) getNextTokenIDs(ctx sdk.Context, contracts []collection.Contract) []collection.ContractNextTokenIDs { + var nextIDs []collection.ContractNextTokenIDs + for _, contract := range contracts { + contractID := contract.ContractId + contractNextIDs := collection.ContractNextTokenIDs{ + ContractId: contractID, + } + + k.iterateContractNextTokenIDs(ctx, contractID, func(nextID collection.NextTokenID) (stop bool) { + contractNextIDs.TokenIds = append(contractNextIDs.TokenIds, nextID) + return false + }) + if len(contractNextIDs.TokenIds) != 0 { + nextIDs = append(nextIDs, contractNextIDs) + } + } + + return nextIDs +} + +func (k Keeper) getBalances(ctx sdk.Context, contracts []collection.Contract) []collection.ContractBalances { + var balances []collection.ContractBalances + for _, contract := range contracts { + contractID := contract.ContractId + contractBalances := collection.ContractBalances{ + ContractId: contractID, + } + + contractBalances.Balances = k.getContractBalances(ctx, contractID) + if len(contractBalances.Balances) != 0 { + balances = append(balances, contractBalances) + } + } + + return balances +} + +func (k Keeper) getContractBalances(ctx sdk.Context, contractID string) []collection.Balance { + var balances []collection.Balance + addressToBalanceIndex := make(map[sdk.AccAddress]int) + + k.iterateContractBalances(ctx, contractID, func(address sdk.AccAddress, balance collection.Coin) (stop bool) { + index, ok := addressToBalanceIndex[address] + if ok { + balances[index].Amount = append(balances[index].Amount, balance) + return false + } + + accountBalance := collection.Balance{ + Address: address.String(), + Amount: collection.Coins{balance}, + } + balances = append(balances, accountBalance) + addressToBalanceIndex[address] = len(balances) - 1 + return false + }) + + return balances +} + +func (k Keeper) getNFTs(ctx sdk.Context, contracts []collection.Contract) []collection.ContractNFTs { + var parents []collection.ContractNFTs + for _, contract := range contracts { + contractID := contract.ContractId + contractParents := collection.ContractNFTs{ + ContractId: contractID, + } + + k.iterateContractNFTs(ctx, contractID, func(nft collection.NFT) (stop bool) { + contractParents.Nfts = append(contractParents.Nfts, nft) + return false + }) + if len(contractParents.Nfts) != 0 { + parents = append(parents, contractParents) + } + } + + return parents +} + +func (k Keeper) getParents(ctx sdk.Context, contracts []collection.Contract) []collection.ContractTokenRelations { + var parents []collection.ContractTokenRelations + for _, contract := range contracts { + contractID := contract.ContractId + contractParents := collection.ContractTokenRelations{ + ContractId: contractID, + } + + k.iterateContractParents(ctx, contractID, func(tokenID, parentID string) (stop bool) { + relation := collection.TokenRelation{ + Self: tokenID, + Other: parentID, + } + contractParents.Relations = append(contractParents.Relations, relation) + return false + }) + if len(contractParents.Relations) != 0 { + parents = append(parents, contractParents) + } + } + + return parents +} + +func (k Keeper) getAuthorizations(ctx sdk.Context, contracts []collection.Contract) []collection.ContractAuthorizations { + var authorizations []collection.ContractAuthorizations + for _, contract := range contracts { + contractID := contract.ContractId + contractAuthorizations := collection.ContractAuthorizations{ + ContractId: contractID, + } + + k.iterateContractAuthorizations(ctx, contractID, func(authorization collection.Authorization) (stop bool) { + contractAuthorizations.Authorizations = append(contractAuthorizations.Authorizations, authorization) + return false + }) + if len(contractAuthorizations.Authorizations) != 0 { + authorizations = append(authorizations, contractAuthorizations) + } + } + + return authorizations +} + +func (k Keeper) getGrants(ctx sdk.Context, contracts []collection.Contract) []collection.ContractGrants { + var grants []collection.ContractGrants + for _, contract := range contracts { + contractID := contract.ContractId + contractGrants := collection.ContractGrants{ + ContractId: contractID, + } + + k.iterateContractGrants(ctx, contractID, func(grant collection.Grant) (stop bool) { + contractGrants.Grants = append(contractGrants.Grants, grant) + return false + }) + if len(contractGrants.Grants) != 0 { + grants = append(grants, contractGrants) + } + } + + return grants +} + +func (k Keeper) getSupplies(ctx sdk.Context, contracts []collection.Contract) []collection.ContractStatistics { + return k.getStatistics(ctx, contracts, k.iterateContractSupplies) +} + +func (k Keeper) getBurnts(ctx sdk.Context, contracts []collection.Contract) []collection.ContractStatistics { + return k.getStatistics(ctx, contracts, k.iterateContractBurnts) +} + +func (k Keeper) getStatistics(ctx sdk.Context, contracts []collection.Contract, iterator func(ctx sdk.Context, contractID string, cb func(classID string, amount sdk.Int) (stop bool))) []collection.ContractStatistics { + var statistics []collection.ContractStatistics + for _, contract := range contracts { + contractID := contract.ContractId + contractStatistics := collection.ContractStatistics{ + ContractId: contractID, + } + + iterator(ctx, contractID, func(classID string, amount sdk.Int) (stop bool) { + supply := collection.ClassStatistics{ + ClassId: classID, + Amount: amount, + } + contractStatistics.Statistics = append(contractStatistics.Statistics, supply) + return false + }) + if len(contractStatistics.Statistics) != 0 { + statistics = append(statistics, contractStatistics) + } + } + + return statistics +} diff --git a/x/collection/keeper/genesis_test.go b/x/collection/keeper/genesis_test.go new file mode 100644 index 0000000000..60e718e9df --- /dev/null +++ b/x/collection/keeper/genesis_test.go @@ -0,0 +1,30 @@ +package keeper_test + +import ( + "github.com/line/lbm-sdk/x/collection" +) + +func (s *KeeperTestSuite) TestImportExportGenesis() { + // export + genesis := s.keeper.ExportGenesis(s.ctx) + + // forge + amount := collection.NewCoins(collection.NewFTCoin(s.ftClassID, s.balance)) + err := s.keeper.SendCoins(s.ctx, s.contractID, s.vendor, s.customer, amount) + s.Require().NoError(err) + + err = s.keeper.SendCoins(s.ctx, s.contractID, s.customer, s.operator, amount) + s.Require().NoError(err) + + _, err = s.keeper.BurnCoins(s.ctx, s.contractID, s.operator, amount) + s.Require().NoError(err) + + s.keeper.Abandon(s.ctx, s.contractID, s.vendor, collection.PermissionMint) + + // restore + s.keeper.InitGenesis(s.ctx, genesis) + + // export again and compare + newGenesis := s.keeper.ExportGenesis(s.ctx) + s.Require().Equal(genesis, newGenesis) +} diff --git a/x/collection/keeper/grpc_query.go b/x/collection/keeper/grpc_query.go new file mode 100644 index 0000000000..0dc5730d07 --- /dev/null +++ b/x/collection/keeper/grpc_query.go @@ -0,0 +1,928 @@ +package keeper + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/gogo/protobuf/proto" + codectypes "github.com/line/lbm-sdk/codec/types" + "github.com/line/lbm-sdk/store/prefix" + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" + "github.com/line/lbm-sdk/types/query" + "github.com/line/lbm-sdk/x/collection" +) + +type queryServer struct { + keeper Keeper +} + +// NewQueryServer returns an implementation of the token QueryServer interface +// for the provided Keeper. +func NewQueryServer(keeper Keeper) collection.QueryServer { + return &queryServer{ + keeper: keeper, + } +} + +var _ collection.QueryServer = queryServer{} + +// Balance queries the number of tokens of a given token id owned by the owner. +func (s queryServer) Balance(c context.Context, req *collection.QueryBalanceRequest) (*collection.QueryBalanceResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := sdk.ValidateAccAddress(req.Address); err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid address: %s", req.Address) + } + + if err := collection.ValidateTokenID(req.TokenId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + balance := s.keeper.GetBalance(ctx, req.ContractId, sdk.AccAddress(req.Address), req.TokenId) + coin := collection.NewCoin(req.TokenId, balance) + + return &collection.QueryBalanceResponse{Balance: coin}, nil +} + +// AllBalances queries all tokens owned by owner. +func (s queryServer) AllBalances(c context.Context, req *collection.QueryAllBalancesRequest) (*collection.QueryAllBalancesResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := sdk.ValidateAccAddress(req.Address); err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid address: %s", req.Address) + } + + ctx := sdk.UnwrapSDKContext(c) + store := ctx.KVStore(s.keeper.storeKey) + balanceStore := prefix.NewStore(store, balanceKeyPrefixByAddress(req.ContractId, sdk.AccAddress(req.Address))) + var balances []collection.Coin + pageRes, err := query.Paginate(balanceStore, req.Pagination, func(key []byte, value []byte) error { + tokenID := string(key) + + var balance sdk.Int + if err := balance.Unmarshal(value); err != nil { + panic(err) + } + + coin := collection.NewCoin(tokenID, balance) + balances = append(balances, coin) + return nil + }) + if err != nil { + return nil, err + } + + return &collection.QueryAllBalancesResponse{Balances: balances, Pagination: pageRes}, nil +} + +// Supply queries the number of tokens from the given contract id. +func (s queryServer) Supply(c context.Context, req *collection.QuerySupplyRequest) (*collection.QuerySupplyResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateClassID(req.ClassId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + supply := s.keeper.GetSupply(ctx, req.ContractId, req.ClassId) + + return &collection.QuerySupplyResponse{Supply: supply}, nil +} + +// Minted queries the number of tokens from the given contract id. +func (s queryServer) Minted(c context.Context, req *collection.QueryMintedRequest) (*collection.QueryMintedResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateClassID(req.ClassId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + minted := s.keeper.GetMinted(ctx, req.ContractId, req.ClassId) + + return &collection.QueryMintedResponse{Minted: minted}, nil +} + +// Burnt queries the number of tokens from the given contract id. +func (s queryServer) Burnt(c context.Context, req *collection.QueryBurntRequest) (*collection.QueryBurntResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateClassID(req.ClassId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + burnt := s.keeper.GetBurnt(ctx, req.ContractId, req.ClassId) + + return &collection.QueryBurntResponse{Burnt: burnt}, nil +} + +func (s queryServer) FTSupply(c context.Context, req *collection.QueryFTSupplyRequest) (*collection.QueryFTSupplyResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateTokenID(req.TokenId); err != nil { + return nil, err + } + + classID := collection.SplitTokenID(req.TokenId) + + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetTokenClass(ctx, req.ContractId, classID); err != nil { + return nil, err + } + supply := s.keeper.GetSupply(ctx, req.ContractId, classID) + + return &collection.QueryFTSupplyResponse{Supply: supply}, nil +} + +func (s queryServer) FTMinted(c context.Context, req *collection.QueryFTMintedRequest) (*collection.QueryFTMintedResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateTokenID(req.TokenId); err != nil { + return nil, err + } + + classID := collection.SplitTokenID(req.TokenId) + + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetTokenClass(ctx, req.ContractId, classID); err != nil { + return nil, err + } + minted := s.keeper.GetMinted(ctx, req.ContractId, classID) + + return &collection.QueryFTMintedResponse{Minted: minted}, nil +} + +func (s queryServer) FTBurnt(c context.Context, req *collection.QueryFTBurntRequest) (*collection.QueryFTBurntResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateTokenID(req.TokenId); err != nil { + return nil, err + } + + classID := collection.SplitTokenID(req.TokenId) + + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetTokenClass(ctx, req.ContractId, classID); err != nil { + return nil, err + } + burnt := s.keeper.GetBurnt(ctx, req.ContractId, classID) + + return &collection.QueryFTBurntResponse{Burnt: burnt}, nil +} + +func (s queryServer) NFTSupply(c context.Context, req *collection.QueryNFTSupplyRequest) (*collection.QueryNFTSupplyResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + classID := req.TokenType + if err := collection.ValidateClassID(classID); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetTokenClass(ctx, req.ContractId, classID); err != nil { + return nil, err + } + supply := s.keeper.GetSupply(ctx, req.ContractId, classID) + + return &collection.QueryNFTSupplyResponse{Supply: supply}, nil +} + +func (s queryServer) NFTMinted(c context.Context, req *collection.QueryNFTMintedRequest) (*collection.QueryNFTMintedResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + classID := req.TokenType + if err := collection.ValidateClassID(classID); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetTokenClass(ctx, req.ContractId, classID); err != nil { + return nil, err + } + minted := s.keeper.GetMinted(ctx, req.ContractId, classID) + + return &collection.QueryNFTMintedResponse{Minted: minted}, nil +} + +func (s queryServer) NFTBurnt(c context.Context, req *collection.QueryNFTBurntRequest) (*collection.QueryNFTBurntResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + classID := req.TokenType + if err := collection.ValidateClassID(classID); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetTokenClass(ctx, req.ContractId, classID); err != nil { + return nil, err + } + burnt := s.keeper.GetBurnt(ctx, req.ContractId, classID) + + return &collection.QueryNFTBurntResponse{Burnt: burnt}, nil +} + +func (s queryServer) Contract(c context.Context, req *collection.QueryContractRequest) (*collection.QueryContractResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + contract, err := s.keeper.GetContract(ctx, req.ContractId) + if err != nil { + return nil, err + } + + return &collection.QueryContractResponse{Contract: *contract}, nil +} + +func (s queryServer) Contracts(c context.Context, req *collection.QueryContractsRequest) (*collection.QueryContractsResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + ctx := sdk.UnwrapSDKContext(c) + store := ctx.KVStore(s.keeper.storeKey) + contractStore := prefix.NewStore(store, contractKeyPrefix) + var contracts []collection.Contract + pageRes, err := query.Paginate(contractStore, req.Pagination, func(key []byte, value []byte) error { + var contract collection.Contract + s.keeper.cdc.MustUnmarshal(value, &contract) + + contracts = append(contracts, contract) + return nil + }) + if err != nil { + return nil, err + } + + return &collection.QueryContractsResponse{Contracts: contracts, Pagination: pageRes}, nil +} + +// FTClass queries a fungible token class based on its class id. +func (s queryServer) FTClass(c context.Context, req *collection.QueryFTClassRequest) (*collection.QueryFTClassResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateClassID(req.ClassId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + class, err := s.keeper.GetTokenClass(ctx, req.ContractId, req.ClassId) + if err != nil { + return nil, err + } + ftClass, ok := class.(*collection.FTClass) + if !ok { + return nil, sdkerrors.ErrInvalidType.Wrapf("not a class of fungible token: %s", req.ClassId) + } + + return &collection.QueryFTClassResponse{Class: *ftClass}, nil +} + +// NFTClass queries a non-fungible token class based on its class id. +func (s queryServer) NFTClass(c context.Context, req *collection.QueryNFTClassRequest) (*collection.QueryNFTClassResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateClassID(req.ClassId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + class, err := s.keeper.GetTokenClass(ctx, req.ContractId, req.ClassId) + if err != nil { + return nil, err + } + nftClass, ok := class.(*collection.NFTClass) + if !ok { + return nil, sdkerrors.ErrInvalidType.Wrapf("not a class of non-fungible token: %s", req.ClassId) + } + + return &collection.QueryNFTClassResponse{Class: *nftClass}, nil +} + +// TokenClassTypeName queries the fully qualified message type name of a token class based on its class id. +func (s queryServer) TokenClassTypeName(c context.Context, req *collection.QueryTokenClassTypeNameRequest) (*collection.QueryTokenClassTypeNameResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateClassID(req.ClassId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + class, err := s.keeper.GetTokenClass(ctx, req.ContractId, req.ClassId) + if err != nil { + return nil, err + } + name := proto.MessageName(class) + + return &collection.QueryTokenClassTypeNameResponse{Name: name}, nil +} + +// TokenClasses queries all token class metadata. +// func (s queryServer) TokenClasses(c context.Context, req *collection.QueryTokenClassesRequest) (*collection.QueryTokenClassesResponse, error) { +// if req == nil { +// return nil, status.Error(codes.InvalidArgument, "empty request") +// } + +// if err := collection.ValidateContractID(req.ContractId); err != nil { +// return nil, err +// } + +// ctx := sdk.UnwrapSDKContext(c) +// store := ctx.KVStore(s.keeper.storeKey) +// classStore := prefix.NewStore(store, classKeyPrefix) +// var classes []codectypes.Any +// pageRes, err := query.Paginate(classStore, req.Pagination, func(key []byte, value []byte) error { +// var class collection.TokenClass +// if err := s.keeper.cdc.UnmarshalInterface(value, &class); err != nil { +// panic(err) +// } +// classes = append(classes, *collection.TokenClassToAny(class)) +// return nil +// }) +// if err != nil { +// return nil, err +// } + +// return &collection.QueryTokenClassesResponse{Classes: classes, Pagination: pageRes}, nil +// } + +func (s queryServer) TokenType(c context.Context, req *collection.QueryTokenTypeRequest) (*collection.QueryTokenTypeResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + classID := req.TokenType + if err := collection.ValidateClassID(classID); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + class, err := s.keeper.GetTokenClass(ctx, req.ContractId, classID) + if err != nil { + return nil, err + } + + nftClass, ok := class.(*collection.NFTClass) + if !ok { + return nil, sdkerrors.ErrInvalidType.Wrapf("not a class of non-fungible token: %s", classID) + } + + tokenType := collection.TokenType{ + ContractId: req.ContractId, + TokenType: nftClass.Id, + Name: nftClass.Name, + Meta: nftClass.Meta, + } + + return &collection.QueryTokenTypeResponse{TokenType: tokenType}, nil +} + +func (s queryServer) TokenTypes(c context.Context, req *collection.QueryTokenTypesRequest) (*collection.QueryTokenTypesResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + store := ctx.KVStore(s.keeper.storeKey) + tokenTypeStore := prefix.NewStore(store, legacyTokenTypeKeyPrefixByContractID(req.ContractId)) + var tokenTypes []collection.TokenType + pageRes, err := query.Paginate(tokenTypeStore, req.Pagination, func(key []byte, value []byte) error { + classID := string(key) + class, err := s.keeper.GetTokenClass(ctx, req.ContractId, classID) + if err != nil { + panic(err) + } + + nftClass, ok := class.(*collection.NFTClass) + if !ok { + panic(sdkerrors.ErrInvalidType.Wrapf("not a class of non-fungible token: %s", key)) + } + + tokenType := collection.TokenType{ + ContractId: req.ContractId, + TokenType: nftClass.Id, + Name: nftClass.Name, + Meta: nftClass.Meta, + } + tokenTypes = append(tokenTypes, tokenType) + return nil + }) + if err != nil { + return nil, err + } + + return &collection.QueryTokenTypesResponse{TokenTypes: tokenTypes, Pagination: pageRes}, nil +} + +func (s queryServer) getToken(ctx sdk.Context, contractID string, tokenID string) (collection.Token, error) { + switch { + case collection.ValidateNFTID(tokenID) == nil: + token, err := s.keeper.GetNFT(ctx, contractID, tokenID) + if err != nil { + return nil, err + } + + owner := s.keeper.GetRootOwner(ctx, contractID, token.Id) + return &collection.OwnerNFT{ + ContractId: contractID, + TokenId: token.Id, + Name: token.Name, + Meta: token.Meta, + Owner: owner.String(), + }, nil + case collection.ValidateFTID(tokenID) == nil: + classID := collection.SplitTokenID(tokenID) + class, err := s.keeper.GetTokenClass(ctx, contractID, classID) + if err != nil { + return nil, err + } + + ftClass, ok := class.(*collection.FTClass) + if !ok { + panic(sdkerrors.ErrInvalidType.Wrapf("not a class of fungible token: %s", classID)) + } + + return &collection.FT{ + ContractId: contractID, + TokenId: ftClass.Id, + Name: ftClass.Name, + Meta: ftClass.Meta, + Decimals: ftClass.Decimals, + Mintable: ftClass.Mintable, + }, nil + default: + panic("cannot reach here: token must be ft or nft") + } +} + +func (s queryServer) Token(c context.Context, req *collection.QueryTokenRequest) (*collection.QueryTokenResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateTokenID(req.TokenId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + legacyToken, err := s.getToken(ctx, req.ContractId, req.TokenId) + if err != nil { + return nil, err + } + + any, err := codectypes.NewAnyWithValue(legacyToken) + if err != nil { + panic(err) + } + + return &collection.QueryTokenResponse{Token: *any}, nil +} + +func (s queryServer) Tokens(c context.Context, req *collection.QueryTokensRequest) (*collection.QueryTokensResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + store := ctx.KVStore(s.keeper.storeKey) + tokenStore := prefix.NewStore(store, legacyTokenKeyPrefixByContractID(req.ContractId)) + var tokens []codectypes.Any + pageRes, err := query.Paginate(tokenStore, req.Pagination, func(key []byte, value []byte) error { + tokenID := string(key) + legacyToken, err := s.getToken(ctx, req.ContractId, tokenID) + if err != nil { + panic(err) + } + + any, err := codectypes.NewAnyWithValue(legacyToken) + if err != nil { + panic(err) + } + + tokens = append(tokens, *any) + return nil + }) + if err != nil { + return nil, err + } + + return &collection.QueryTokensResponse{Tokens: tokens, Pagination: pageRes}, nil +} + +func (s queryServer) NFT(c context.Context, req *collection.QueryNFTRequest) (*collection.QueryNFTResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateNFTID(req.TokenId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + token, err := s.keeper.GetNFT(ctx, req.ContractId, req.TokenId) + if err != nil { + return nil, err + } + + return &collection.QueryNFTResponse{Token: *token}, nil +} + +func (s queryServer) Owner(c context.Context, req *collection.QueryOwnerRequest) (*collection.QueryOwnerResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateNFTID(req.TokenId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.hasNFT(ctx, req.ContractId, req.TokenId); err != nil { + return nil, err + } + + owner := s.keeper.GetRootOwner(ctx, req.ContractId, req.TokenId) + + return &collection.QueryOwnerResponse{Owner: owner.String()}, nil +} + +func (s queryServer) Root(c context.Context, req *collection.QueryRootRequest) (*collection.QueryRootResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateNFTID(req.TokenId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.hasNFT(ctx, req.ContractId, req.TokenId); err != nil { + return nil, err + } + + root := s.keeper.GetRoot(ctx, req.ContractId, req.TokenId) + token, err := s.keeper.GetNFT(ctx, req.ContractId, root) + if err != nil { + panic(err) + } + + return &collection.QueryRootResponse{Root: *token}, nil +} + +func (s queryServer) Parent(c context.Context, req *collection.QueryParentRequest) (*collection.QueryParentResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateNFTID(req.TokenId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.hasNFT(ctx, req.ContractId, req.TokenId); err != nil { + return nil, err + } + + parent, err := s.keeper.GetParent(ctx, req.ContractId, req.TokenId) + if err != nil { + return nil, err + } + + token, err := s.keeper.GetNFT(ctx, req.ContractId, *parent) + if err != nil { + panic(err) + } + + return &collection.QueryParentResponse{Parent: *token}, nil +} + +func (s queryServer) Children(c context.Context, req *collection.QueryChildrenRequest) (*collection.QueryChildrenResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := collection.ValidateNFTID(req.TokenId); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.hasNFT(ctx, req.ContractId, req.TokenId); err != nil { + return nil, err + } + + store := ctx.KVStore(s.keeper.storeKey) + childStore := prefix.NewStore(store, childKeyPrefixByTokenID(req.ContractId, req.TokenId)) + var children []collection.NFT + pageRes, err := query.Paginate(childStore, req.Pagination, func(key []byte, _ []byte) error { + childID := string(key) + child, err := s.keeper.GetNFT(ctx, req.ContractId, childID) + if err != nil { + panic(err) + } + + children = append(children, *child) + return nil + }) + if err != nil { + return nil, err + } + + return &collection.QueryChildrenResponse{Children: children, Pagination: pageRes}, nil +} + +func (s queryServer) Grant(c context.Context, req *collection.QueryGrantRequest) (*collection.QueryGrantResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := sdk.ValidateAccAddress(req.Grantee); err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid grantee address: %s", req.Grantee) + } + + if err := collection.ValidatePermission(req.Permission); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + grant, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.Grantee), req.Permission) + if err != nil { + return nil, err + } + + return &collection.QueryGrantResponse{Grant: *grant}, nil +} + +func (s queryServer) GranteeGrants(c context.Context, req *collection.QueryGranteeGrantsRequest) (*collection.QueryGranteeGrantsResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := sdk.ValidateAccAddress(req.Grantee); err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid grantee address: %s", req.Grantee) + } + + ctx := sdk.UnwrapSDKContext(c) + store := ctx.KVStore(s.keeper.storeKey) + grantStore := prefix.NewStore(store, grantKeyPrefixByGrantee(req.ContractId, sdk.AccAddress(req.Grantee))) + var grants []collection.Grant + pageRes, err := query.Paginate(grantStore, req.Pagination, func(key []byte, _ []byte) error { + permission := collection.Permission(key[0]) + grants = append(grants, collection.Grant{ + Grantee: req.Grantee, + Permission: permission, + }) + return nil + }) + if err != nil { + return nil, err + } + + return &collection.QueryGranteeGrantsResponse{Grants: grants, Pagination: pageRes}, nil +} + +func (s queryServer) Authorization(c context.Context, req *collection.QueryAuthorizationRequest) (*collection.QueryAuthorizationResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := sdk.ValidateAccAddress(req.Operator); err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", req.Operator) + } + if err := sdk.ValidateAccAddress(req.Holder); err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid holder address: %s", req.Holder) + } + + ctx := sdk.UnwrapSDKContext(c) + authorization, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.Holder), sdk.AccAddress(req.Operator)) + if err != nil { + return nil, err + } + + return &collection.QueryAuthorizationResponse{Authorization: *authorization}, nil +} + +func (s queryServer) OperatorAuthorizations(c context.Context, req *collection.QueryOperatorAuthorizationsRequest) (*collection.QueryOperatorAuthorizationsResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := sdk.ValidateAccAddress(req.Operator); err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", req.Operator) + } + + ctx := sdk.UnwrapSDKContext(c) + store := ctx.KVStore(s.keeper.storeKey) + authorizationStore := prefix.NewStore(store, authorizationKeyPrefixByOperator(req.ContractId, sdk.AccAddress(req.Operator))) + var authorizations []collection.Authorization + pageRes, err := query.Paginate(authorizationStore, req.Pagination, func(key []byte, value []byte) error { + holder := sdk.AccAddress(key) + authorizations = append(authorizations, collection.Authorization{ + Holder: holder.String(), + Operator: req.Operator, + }) + return nil + }) + if err != nil { + return nil, err + } + + return &collection.QueryOperatorAuthorizationsResponse{Authorizations: authorizations, Pagination: pageRes}, nil +} + +func (s queryServer) Approved(c context.Context, req *collection.QueryApprovedRequest) (*collection.QueryApprovedResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := sdk.ValidateAccAddress(req.Address); err != nil { + return nil, err + } + if err := sdk.ValidateAccAddress(req.Approver); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(c) + _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.Approver), sdk.AccAddress(req.Address)) + approved := (err == nil) + + return &collection.QueryApprovedResponse{Approved: approved}, nil +} + +func (s queryServer) Approvers(c context.Context, req *collection.QueryApproversRequest) (*collection.QueryApproversResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if err := collection.ValidateContractID(req.ContractId); err != nil { + return nil, err + } + + if err := sdk.ValidateAccAddress(req.Address); err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid address address: %s", req.Address) + } + + ctx := sdk.UnwrapSDKContext(c) + store := ctx.KVStore(s.keeper.storeKey) + authorizationStore := prefix.NewStore(store, authorizationKeyPrefixByOperator(req.ContractId, sdk.AccAddress(req.Address))) + var approvers []string + pageRes, err := query.Paginate(authorizationStore, req.Pagination, func(key []byte, value []byte) error { + holder := string(key) + approvers = append(approvers, holder) + return nil + }) + if err != nil { + return nil, err + } + + return &collection.QueryApproversResponse{Approvers: approvers, Pagination: pageRes}, nil +} diff --git a/x/collection/keeper/grpc_query_test.go b/x/collection/keeper/grpc_query_test.go new file mode 100644 index 0000000000..790f08069e --- /dev/null +++ b/x/collection/keeper/grpc_query_test.go @@ -0,0 +1,1729 @@ +package keeper_test + +import ( + "github.com/gogo/protobuf/proto" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/types/query" + "github.com/line/lbm-sdk/x/collection" +) + +func (s *KeeperTestSuite) TestQueryBalance() { + // empty request + _, err := s.queryServer.Balance(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewFTID(s.ftClassID) + testCases := map[string]struct { + contractID string + address sdk.AccAddress + tokenID string + valid bool + postTest func(res *collection.QueryBalanceResponse) + }{ + "valid request": { + contractID: s.contractID, + address: s.vendor, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryBalanceResponse) { + expected := collection.NewCoin(tokenID, s.balance) + s.Require().Equal(expected, res.Balance) + }, + }, + "invalid contract id": { + address: s.vendor, + tokenID: tokenID, + }, + "invalid address": { + contractID: s.contractID, + tokenID: tokenID, + }, + "valid token id": { + contractID: s.contractID, + address: s.vendor, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryBalanceRequest{ + ContractId: tc.contractID, + Address: tc.address.String(), + TokenId: tc.tokenID, + } + res, err := s.queryServer.Balance(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryAllBalances() { + // empty request + _, err := s.queryServer.AllBalances(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + address sdk.AccAddress + valid bool + count uint64 + postTest func(res *collection.QueryAllBalancesResponse) + }{ + "valid request": { + contractID: s.contractID, + address: s.customer, + valid: true, + postTest: func(res *collection.QueryAllBalancesResponse) { + s.Require().Equal(s.numNFTs+1, len(res.Balances)) + }, + }, + "valid request with limit": { + contractID: s.contractID, + address: s.customer, + valid: true, + count: 1, + postTest: func(res *collection.QueryAllBalancesResponse) { + s.Require().Equal(1, len(res.Balances)) + }, + }, + "invalid contract id": { + address: s.customer, + }, + "invalid address": { + contractID: s.contractID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + pageReq := &query.PageRequest{} + if tc.count != 0 { + pageReq.Limit = tc.count + } + req := &collection.QueryAllBalancesRequest{ + ContractId: tc.contractID, + Address: tc.address.String(), + Pagination: pageReq, + } + res, err := s.queryServer.AllBalances(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQuerySupply() { + // empty request + _, err := s.queryServer.Supply(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + classID string + valid bool + postTest func(res *collection.QuerySupplyResponse) + }{ + "valid request": { + contractID: s.contractID, + classID: s.ftClassID, + valid: true, + postTest: func(res *collection.QuerySupplyResponse) { + s.Require().Equal(s.balance.Mul(sdk.NewInt(3)), res.Supply) + }, + }, + "invalid contract id": { + classID: s.ftClassID, + }, + "invalid class id": { + contractID: s.contractID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QuerySupplyRequest{ + ContractId: tc.contractID, + ClassId: tc.classID, + } + res, err := s.queryServer.Supply(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryMinted() { + // empty request + _, err := s.queryServer.Minted(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + classID string + valid bool + postTest func(res *collection.QueryMintedResponse) + }{ + "valid request": { + contractID: s.contractID, + classID: s.ftClassID, + valid: true, + postTest: func(res *collection.QueryMintedResponse) { + s.Require().Equal(s.balance.Mul(sdk.NewInt(6)), res.Minted) + }, + }, + "invalid contract id": { + classID: s.ftClassID, + }, + "invalid class id": { + contractID: s.contractID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryMintedRequest{ + ContractId: tc.contractID, + ClassId: tc.classID, + } + res, err := s.queryServer.Minted(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryBurnt() { + // empty request + _, err := s.queryServer.Burnt(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + classID string + valid bool + postTest func(res *collection.QueryBurntResponse) + }{ + "valid request": { + contractID: s.contractID, + classID: s.ftClassID, + valid: true, + postTest: func(res *collection.QueryBurntResponse) { + s.Require().Equal(s.balance.Mul(sdk.NewInt(3)), res.Burnt) + }, + }, + "invalid contract id": { + classID: s.ftClassID, + }, + "invalid class id": { + contractID: s.contractID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryBurntRequest{ + ContractId: tc.contractID, + ClassId: tc.classID, + } + res, err := s.queryServer.Burnt(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryFTSupply() { + // empty request + _, err := s.queryServer.FTSupply(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewFTID(s.ftClassID) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + postTest func(res *collection.QueryFTSupplyResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryFTSupplyResponse) { + s.Require().Equal(s.balance.Mul(sdk.NewInt(3)), res.Supply) + }, + }, + "invalid contract id": { + tokenID: tokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + "no such a token": { + contractID: s.contractID, + tokenID: collection.NewFTID("00bab10c"), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryFTSupplyRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + } + res, err := s.queryServer.FTSupply(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryFTMinted() { + // empty request + _, err := s.queryServer.FTMinted(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewFTID(s.ftClassID) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + postTest func(res *collection.QueryFTMintedResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryFTMintedResponse) { + s.Require().Equal(s.balance.Mul(sdk.NewInt(6)), res.Minted) + }, + }, + "invalid contract id": { + tokenID: tokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + "no such a token": { + contractID: s.contractID, + tokenID: collection.NewFTID("00bab10c"), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryFTMintedRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + } + res, err := s.queryServer.FTMinted(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryFTBurnt() { + // empty request + _, err := s.queryServer.FTBurnt(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewFTID(s.ftClassID) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + postTest func(res *collection.QueryFTBurntResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryFTBurntResponse) { + s.Require().Equal(s.balance.Mul(sdk.NewInt(3)), res.Burnt) + }, + }, + "invalid contract id": { + tokenID: tokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + "no such a token": { + contractID: s.contractID, + tokenID: collection.NewFTID("00bab10c"), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryFTBurntRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + } + res, err := s.queryServer.FTBurnt(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryNFTSupply() { + // empty request + _, err := s.queryServer.NFTSupply(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + tokenType string + valid bool + postTest func(res *collection.QueryNFTSupplyResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenType: s.nftClassID, + valid: true, + postTest: func(res *collection.QueryNFTSupplyResponse) { + s.Require().EqualValues(s.numNFTs*3, res.Supply.Int64()) + }, + }, + "invalid contract id": { + tokenType: s.nftClassID, + }, + "invalid token type": { + contractID: s.contractID, + }, + "no such a token type": { + contractID: s.contractID, + tokenType: "deadbeef", + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryNFTSupplyRequest{ + ContractId: tc.contractID, + TokenType: tc.tokenType, + } + res, err := s.queryServer.NFTSupply(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryNFTMinted() { + // empty request + _, err := s.queryServer.NFTMinted(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + tokenType string + valid bool + postTest func(res *collection.QueryNFTMintedResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenType: s.nftClassID, + valid: true, + postTest: func(res *collection.QueryNFTMintedResponse) { + s.Require().EqualValues(s.numNFTs*3, res.Minted.Int64()) + }, + }, + "invalid contract id": { + tokenType: s.nftClassID, + }, + "invalid token type": { + contractID: s.contractID, + }, + "no such a token type": { + contractID: s.contractID, + tokenType: "deadbeef", + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryNFTMintedRequest{ + ContractId: tc.contractID, + TokenType: tc.tokenType, + } + res, err := s.queryServer.NFTMinted(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryNFTBurnt() { + // empty request + _, err := s.queryServer.NFTBurnt(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + tokenType string + valid bool + postTest func(res *collection.QueryNFTBurntResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenType: s.nftClassID, + valid: true, + postTest: func(res *collection.QueryNFTBurntResponse) { + s.Require().Equal(sdk.ZeroInt(), res.Burnt) + }, + }, + "invalid contract id": { + tokenType: s.nftClassID, + }, + "invalid token type": { + contractID: s.contractID, + }, + "no such a token type": { + contractID: s.contractID, + tokenType: "deadbeef", + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryNFTBurntRequest{ + ContractId: tc.contractID, + TokenType: tc.tokenType, + } + res, err := s.queryServer.NFTBurnt(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryContract() { + // empty request + _, err := s.queryServer.Contract(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + valid bool + postTest func(res *collection.QueryContractResponse) + }{ + "valid request": { + contractID: s.contractID, + valid: true, + postTest: func(res *collection.QueryContractResponse) { + s.Require().Equal(s.contractID, res.Contract.ContractId) + }, + }, + "invalid contract id": {}, + "no such an id": { + contractID: "deadbeef", + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryContractRequest{ + ContractId: tc.contractID, + } + res, err := s.queryServer.Contract(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryContracts() { + // empty request + _, err := s.queryServer.Contracts(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + valid bool + count uint64 + postTest func(res *collection.QueryContractsResponse) + }{ + "valid request": { + valid: true, + postTest: func(res *collection.QueryContractsResponse) { + s.Require().Equal(1, len(res.Contracts)) + }, + }, + "valid request with limit": { + valid: true, + count: 1, + postTest: func(res *collection.QueryContractsResponse) { + s.Require().Equal(1, len(res.Contracts)) + }, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + pageReq := &query.PageRequest{} + if tc.count != 0 { + pageReq.Limit = tc.count + } + req := &collection.QueryContractsRequest{ + Pagination: pageReq, + } + res, err := s.queryServer.Contracts(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryFTClass() { + // empty request + _, err := s.queryServer.FTClass(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + classID string + valid bool + postTest func(res *collection.QueryFTClassResponse) + }{ + "valid request": { + contractID: s.contractID, + classID: s.ftClassID, + valid: true, + postTest: func(res *collection.QueryFTClassResponse) { + s.Require().Equal(s.ftClassID, res.Class.GetId()) + }, + }, + "invalid contract id": { + classID: s.ftClassID, + }, + "invalid class id": { + contractID: s.contractID, + }, + "no such a class": { + contractID: s.contractID, + classID: "deadbeef", + }, + "not a class of ft": { + contractID: s.contractID, + classID: s.nftClassID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryFTClassRequest{ + ContractId: tc.contractID, + ClassId: tc.classID, + } + res, err := s.queryServer.FTClass(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryNFTClass() { + // empty request + _, err := s.queryServer.NFTClass(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + classID string + valid bool + postTest func(res *collection.QueryNFTClassResponse) + }{ + "valid request": { + contractID: s.contractID, + classID: s.nftClassID, + valid: true, + postTest: func(res *collection.QueryNFTClassResponse) { + s.Require().Equal(s.nftClassID, res.Class.GetId()) + }, + }, + "invalid contract id": { + classID: s.ftClassID, + }, + "invalid class id": { + contractID: s.contractID, + }, + "no such a class": { + contractID: s.contractID, + classID: "deadbeef", + }, + "not a class of nft": { + contractID: s.contractID, + classID: s.ftClassID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryNFTClassRequest{ + ContractId: tc.contractID, + ClassId: tc.classID, + } + res, err := s.queryServer.NFTClass(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryTokenClassTypeName() { + // empty request + _, err := s.queryServer.TokenClassTypeName(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + classID string + valid bool + postTest func(res *collection.QueryTokenClassTypeNameResponse) + }{ + "valid request": { + contractID: s.contractID, + classID: s.ftClassID, + valid: true, + postTest: func(res *collection.QueryTokenClassTypeNameResponse) { + s.Require().Equal(proto.MessageName(&collection.FTClass{}), res.Name) + }, + }, + "invalid contract id": { + classID: s.ftClassID, + }, + "invalid class id": { + contractID: s.contractID, + }, + "no such a class": { + contractID: s.contractID, + classID: "00bab10c", + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryTokenClassTypeNameRequest{ + ContractId: tc.contractID, + ClassId: tc.classID, + } + res, err := s.queryServer.TokenClassTypeName(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +// func (s *KeeperTestSuite) TestQueryTokenClasses() { +// // empty request +// _, err := s.queryServer.TokenClasses(s.goCtx, nil) +// s.Require().Error(err) + +// testCases := map[string]struct { +// contractID string +// valid bool +// count uint64 +// postTest func(res *collection.QueryTokenClassesResponse) +// }{ +// "valid request": { +// contractID: s.contractID, +// valid: true, +// postTest: func(res *collection.QueryTokenClassesResponse) { +// s.Require().Equal(2, len(res.Classes)) +// }, +// }, +// "valid request with limit": { +// contractID: s.contractID, +// valid: true, +// count: 1, +// postTest: func(res *collection.QueryTokenClassesResponse) { +// s.Require().Equal(1, len(res.Classes)) +// }, +// }, +// "invalid contract id": {}, +// } + +// for name, tc := range testCases { +// s.Run(name, func() { +// pageReq := &query.PageRequest{} +// if tc.count != 0 { +// pageReq.Limit = tc.count +// } +// req := &collection.QueryTokenClassesRequest{ +// ContractId: tc.contractID, +// Pagination: pageReq, +// } +// res, err := s.queryServer.TokenClasses(s.goCtx, req) +// if !tc.valid { +// s.Require().Error(err) +// return +// } +// s.Require().NoError(err) +// s.Require().NotNil(res) +// tc.postTest(res) +// }) +// } +// } + +func (s *KeeperTestSuite) TestQueryTokenType() { + // empty request + _, err := s.queryServer.TokenType(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + tokenType string + valid bool + postTest func(res *collection.QueryTokenTypeResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenType: s.nftClassID, + valid: true, + postTest: func(res *collection.QueryTokenTypeResponse) { + s.Require().Equal(s.contractID, res.TokenType.ContractId) + s.Require().Equal(s.nftClassID, res.TokenType.TokenType) + }, + }, + "invalid contract id": { + tokenType: s.nftClassID, + }, + "invalid token type": { + contractID: s.contractID, + }, + "no such a token type": { + contractID: s.contractID, + tokenType: "deadbeef", + }, + "not a class of nft": { + contractID: s.contractID, + tokenType: s.ftClassID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryTokenTypeRequest{ + ContractId: tc.contractID, + TokenType: tc.tokenType, + } + res, err := s.queryServer.TokenType(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryTokenTypes() { + // empty request + _, err := s.queryServer.TokenTypes(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + valid bool + count uint64 + postTest func(res *collection.QueryTokenTypesResponse) + }{ + "valid request": { + contractID: s.contractID, + valid: true, + postTest: func(res *collection.QueryTokenTypesResponse) { + s.Require().Equal(1, len(res.TokenTypes)) + }, + }, + "valid request with limit": { + contractID: s.contractID, + valid: true, + count: 1, + postTest: func(res *collection.QueryTokenTypesResponse) { + s.Require().Equal(1, len(res.TokenTypes)) + }, + }, + "invalid contract id": {}, + } + + for name, tc := range testCases { + s.Run(name, func() { + pageReq := &query.PageRequest{} + if tc.count != 0 { + pageReq.Limit = tc.count + } + req := &collection.QueryTokenTypesRequest{ + ContractId: tc.contractID, + Pagination: pageReq, + } + res, err := s.queryServer.TokenTypes(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryToken() { + // empty request + _, err := s.queryServer.Token(s.goCtx, nil) + s.Require().Error(err) + + ftTokenID := collection.NewFTID(s.ftClassID) + nftTokenID := collection.NewNFTID(s.nftClassID, 1) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + postTest func(res *collection.QueryTokenResponse) + }{ + "valid ft request": { + contractID: s.contractID, + tokenID: ftTokenID, + valid: true, + postTest: func(res *collection.QueryTokenResponse) { + s.Require().Equal("/lbm.collection.v1.FT", res.Token.TypeUrl) + }, + }, + "valid nft request": { + contractID: s.contractID, + tokenID: nftTokenID, + valid: true, + postTest: func(res *collection.QueryTokenResponse) { + s.Require().Equal("/lbm.collection.v1.OwnerNFT", res.Token.TypeUrl) + }, + }, + "invalid contract id": { + tokenID: ftTokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + "no such a fungible token": { + contractID: s.contractID, + tokenID: collection.NewFTID("00bab10c"), + }, + "no such a non-fungible token": { + contractID: s.contractID, + tokenID: collection.NewNFTID("deadbeef", 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryTokenRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + } + res, err := s.queryServer.Token(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryTokens() { + // empty request + _, err := s.queryServer.Tokens(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + valid bool + count uint64 + postTest func(res *collection.QueryTokensResponse) + }{ + "valid request": { + contractID: s.contractID, + valid: true, + count: 1000000, + postTest: func(res *collection.QueryTokensResponse) { + s.Require().Equal(s.numNFTs*3+1, len(res.Tokens)) + }, + }, + "valid request with limit": { + contractID: s.contractID, + valid: true, + count: 1, + postTest: func(res *collection.QueryTokensResponse) { + s.Require().Equal(1, len(res.Tokens)) + }, + }, + "invalid contract id": {}, + } + + for name, tc := range testCases { + s.Run(name, func() { + pageReq := &query.PageRequest{} + if tc.count != 0 { + pageReq.Limit = tc.count + } + req := &collection.QueryTokensRequest{ + ContractId: tc.contractID, + Pagination: pageReq, + } + res, err := s.queryServer.Tokens(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryNFT() { + // empty request + _, err := s.queryServer.NFT(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewNFTID(s.nftClassID, 1) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + postTest func(res *collection.QueryNFTResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryNFTResponse) { + s.Require().Equal(tokenID, res.Token.Id) + }, + }, + "invalid contract id": { + tokenID: tokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + "no such a token": { + contractID: s.contractID, + tokenID: collection.NewNFTID("deadbeef", 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryNFTRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + } + res, err := s.queryServer.NFT(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +// func (s *KeeperTestSuite) TestQueryNFTs() { +// // empty request +// _, err := s.queryServer.NFTs(s.goCtx, nil) +// s.Require().Error(err) + +// testCases := map[string]struct { +// contractID string +// valid bool +// count uint64 +// postTest func(res *collection.QueryNFTsResponse) +// }{ +// "valid request": { +// contractID: s.contractID, +// valid: true, +// count: 1000000, +// postTest: func(res *collection.QueryNFTsResponse) { +// s.Require().Equal(s.lenChain*6, len(res.Tokens)) +// }, +// }, +// "valid request with limit": { +// contractID: s.contractID, +// valid: true, +// count: 1, +// postTest: func(res *collection.QueryNFTsResponse) { +// s.Require().Equal(1, len(res.Tokens)) +// }, +// }, +// "invalid contract id": {}, +// } + +// for name, tc := range testCases { +// s.Run(name, func() { +// pageReq := &query.PageRequest{} +// if tc.count != 0 { +// pageReq.Limit = tc.count +// } +// req := &collection.QueryNFTsRequest{ +// ContractId: tc.contractID, +// Pagination: pageReq, +// } +// res, err := s.queryServer.NFTs(s.goCtx, req) +// if !tc.valid { +// s.Require().Error(err) +// return +// } +// s.Require().NoError(err) +// s.Require().NotNil(res) +// tc.postTest(res) +// }) +// } +// } + +func (s *KeeperTestSuite) TestQueryOwner() { + // empty request + _, err := s.queryServer.Owner(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewNFTID(s.nftClassID, 1) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + postTest func(res *collection.QueryOwnerResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryOwnerResponse) { + s.Require().Equal(s.customer.String(), res.Owner) + }, + }, + "invalid contract id": { + tokenID: tokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + "no such a token": { + contractID: s.contractID, + tokenID: collection.NewNFTID("deadbeef", 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryOwnerRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + } + res, err := s.queryServer.Owner(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryRoot() { + // empty request + _, err := s.queryServer.Root(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewNFTID(s.nftClassID, 2) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + postTest func(res *collection.QueryRootResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryRootResponse) { + s.Require().Equal(collection.NewNFTID(s.nftClassID, 1), res.Root.Id) + }, + }, + "invalid contract id": { + tokenID: tokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + "no such a token": { + contractID: s.contractID, + tokenID: collection.NewNFTID("deadbeef", 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryRootRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + } + res, err := s.queryServer.Root(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryParent() { + // empty request + _, err := s.queryServer.Parent(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewNFTID(s.nftClassID, 2) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + postTest func(res *collection.QueryParentResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryParentResponse) { + s.Require().Equal(collection.NewNFTID(s.nftClassID, 1), res.Parent.Id) + }, + }, + "invalid contract id": { + tokenID: tokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + "no such a token": { + contractID: s.contractID, + tokenID: collection.NewNFTID("deadbeef", 1), + }, + "no parent": { + contractID: s.contractID, + tokenID: collection.NewNFTID(s.nftClassID, 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryParentRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + } + res, err := s.queryServer.Parent(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryChildren() { + // empty request + _, err := s.queryServer.Children(s.goCtx, nil) + s.Require().Error(err) + + tokenID := collection.NewNFTID(s.nftClassID, 1) + testCases := map[string]struct { + contractID string + tokenID string + valid bool + count uint64 + postTest func(res *collection.QueryChildrenResponse) + }{ + "valid request": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + postTest: func(res *collection.QueryChildrenResponse) { + s.Require().Equal(1, len(res.Children)) + s.Require().Equal(collection.NewNFTID(s.nftClassID, 2), res.Children[0].Id) + }, + }, + "valid request with limit": { + contractID: s.contractID, + tokenID: tokenID, + valid: true, + count: 1, + postTest: func(res *collection.QueryChildrenResponse) { + s.Require().Equal(1, len(res.Children)) + s.Require().Equal(collection.NewNFTID(s.nftClassID, 2), res.Children[0].Id) + }, + }, + "invalid contract id": { + tokenID: tokenID, + }, + "invalid token id": { + contractID: s.contractID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + pageReq := &query.PageRequest{} + if tc.count != 0 { + pageReq.Limit = tc.count + } + req := &collection.QueryChildrenRequest{ + ContractId: tc.contractID, + TokenId: tc.tokenID, + Pagination: pageReq, + } + res, err := s.queryServer.Children(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryGrant() { + // empty request + _, err := s.queryServer.Grant(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + grantee sdk.AccAddress + permission collection.Permission + valid bool + postTest func(res *collection.QueryGrantResponse) + }{ + "valid request": { + contractID: s.contractID, + grantee: s.vendor, + permission: collection.PermissionModify, + valid: true, + postTest: func(res *collection.QueryGrantResponse) { + s.Require().Equal(s.vendor.String(), res.Grant.Grantee) + s.Require().Equal(collection.PermissionModify, res.Grant.Permission) + }, + }, + "invalid contract id": { + grantee: s.vendor, + permission: collection.PermissionModify, + }, + "invalid grantee": { + contractID: s.contractID, + permission: collection.PermissionModify, + }, + "invalid permission": { + contractID: s.contractID, + grantee: s.vendor, + }, + "no permission": { + contractID: s.contractID, + grantee: s.customer, + permission: collection.PermissionModify, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryGrantRequest{ + ContractId: tc.contractID, + Grantee: tc.grantee.String(), + Permission: tc.permission, + } + res, err := s.queryServer.Grant(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryGranteeGrants() { + // empty request + _, err := s.queryServer.GranteeGrants(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + grantee sdk.AccAddress + valid bool + postTest func(res *collection.QueryGranteeGrantsResponse) + }{ + "valid request": { + contractID: s.contractID, + grantee: s.vendor, + valid: true, + postTest: func(res *collection.QueryGranteeGrantsResponse) { + s.Require().Equal(4, len(res.Grants)) + }, + }, + "invalid contract id": { + grantee: s.vendor, + }, + "invalid grantee": { + contractID: s.contractID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryGranteeGrantsRequest{ + ContractId: tc.contractID, + Grantee: tc.grantee.String(), + } + res, err := s.queryServer.GranteeGrants(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryAuthorization() { + // empty request + _, err := s.queryServer.Authorization(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + holder sdk.AccAddress + valid bool + postTest func(res *collection.QueryAuthorizationResponse) + }{ + "valid request": { + contractID: s.contractID, + operator: s.operator, + holder: s.customer, + valid: true, + postTest: func(res *collection.QueryAuthorizationResponse) { + expected := collection.Authorization{ + Holder: s.customer.String(), + Operator: s.operator.String(), + } + s.Require().Equal(expected, res.Authorization) + }, + }, + "invalid contract id": { + operator: s.operator, + holder: s.customer, + }, + "invalid operator": { + contractID: s.contractID, + holder: s.customer, + }, + "invalid holder": { + contractID: s.contractID, + operator: s.operator, + }, + "no authorization found": { + contractID: s.contractID, + operator: s.vendor, + holder: s.customer, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryAuthorizationRequest{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Holder: tc.holder.String(), + } + res, err := s.queryServer.Authorization(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryOperatorAuthorizations() { + // empty request + _, err := s.queryServer.OperatorAuthorizations(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + valid bool + count uint64 + postTest func(res *collection.QueryOperatorAuthorizationsResponse) + }{ + "valid request": { + contractID: s.contractID, + operator: s.operator, + valid: true, + postTest: func(res *collection.QueryOperatorAuthorizationsResponse) { + s.Require().Equal(1, len(res.Authorizations)) + }, + }, + "valid request with limit": { + contractID: s.contractID, + operator: s.operator, + valid: true, + count: 1, + postTest: func(res *collection.QueryOperatorAuthorizationsResponse) { + s.Require().Equal(1, len(res.Authorizations)) + }, + }, + "invalid contract id": { + operator: s.operator, + }, + "invalid operator": { + contractID: s.contractID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + pageReq := &query.PageRequest{} + if tc.count != 0 { + pageReq.Limit = tc.count + } + req := &collection.QueryOperatorAuthorizationsRequest{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Pagination: pageReq, + } + res, err := s.queryServer.OperatorAuthorizations(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryApproved() { + // empty request + _, err := s.queryServer.Approved(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + address sdk.AccAddress + approver sdk.AccAddress + valid bool + postTest func(res *collection.QueryApprovedResponse) + }{ + "valid request": { + contractID: s.contractID, + address: s.operator, + approver: s.customer, + valid: true, + postTest: func(res *collection.QueryApprovedResponse) { + s.Require().True(res.Approved) + }, + }, + "invalid contract id": { + address: s.operator, + approver: s.customer, + }, + "invalid address": { + contractID: s.contractID, + approver: s.customer, + }, + "invalid approver": { + contractID: s.contractID, + address: s.operator, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + req := &collection.QueryApprovedRequest{ + ContractId: tc.contractID, + Address: tc.address.String(), + Approver: tc.approver.String(), + } + res, err := s.queryServer.Approved(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} + +func (s *KeeperTestSuite) TestQueryApprovers() { + // empty request + _, err := s.queryServer.Approvers(s.goCtx, nil) + s.Require().Error(err) + + testCases := map[string]struct { + contractID string + address sdk.AccAddress + valid bool + count uint64 + postTest func(res *collection.QueryApproversResponse) + }{ + "valid request": { + contractID: s.contractID, + address: s.operator, + valid: true, + postTest: func(res *collection.QueryApproversResponse) { + s.Require().Equal(1, len(res.Approvers)) + }, + }, + "valid request with limit": { + contractID: s.contractID, + address: s.operator, + valid: true, + count: 1, + postTest: func(res *collection.QueryApproversResponse) { + s.Require().Equal(1, len(res.Approvers)) + }, + }, + "invalid contract id": { + address: s.operator, + }, + "invalid address": { + contractID: s.contractID, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + pageReq := &query.PageRequest{} + if tc.count != 0 { + pageReq.Limit = tc.count + } + req := &collection.QueryApproversRequest{ + ContractId: tc.contractID, + Address: tc.address.String(), + Pagination: pageReq, + } + res, err := s.queryServer.Approvers(s.goCtx, req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + tc.postTest(res) + }) + } +} diff --git a/x/collection/keeper/keeper.go b/x/collection/keeper/keeper.go new file mode 100644 index 0000000000..21479d05c5 --- /dev/null +++ b/x/collection/keeper/keeper.go @@ -0,0 +1,42 @@ +package keeper + +import ( + "github.com/line/lbm-sdk/codec" + "github.com/line/lbm-sdk/telemetry" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +// Keeper defines the collection module Keeper +type Keeper struct { + accountKeeper collection.AccountKeeper + classKeeper collection.ClassKeeper + + // The (unexposed) keys used to access the stores from the Context. + storeKey sdk.StoreKey + + // The codec for binary encoding/decoding. + cdc codec.Codec +} + +// NewKeeper returns a collection keeper +func NewKeeper( + cdc codec.Codec, + key sdk.StoreKey, + ak collection.AccountKeeper, + ck collection.ClassKeeper, +) Keeper { + return Keeper{ + accountKeeper: ak, + classKeeper: ck, + storeKey: key, + cdc: cdc, + } +} + +func (k Keeper) createAccountOnAbsence(ctx sdk.Context, address sdk.AccAddress) { + if !k.accountKeeper.HasAccount(ctx, address) { + defer telemetry.IncrCounter(1, "new", "account") + k.accountKeeper.SetAccount(ctx, k.accountKeeper.NewAccountWithAddress(ctx, address)) + } +} diff --git a/x/collection/keeper/keeper_test.go b/x/collection/keeper/keeper_test.go new file mode 100644 index 0000000000..af9cf0f652 --- /dev/null +++ b/x/collection/keeper/keeper_test.go @@ -0,0 +1,163 @@ +package keeper_test + +import ( + "context" + "testing" + + ocproto "github.com/line/ostracon/proto/ostracon/types" + + "github.com/stretchr/testify/suite" + + "github.com/line/lbm-sdk/crypto/keys/secp256k1" + "github.com/line/lbm-sdk/simapp" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" + "github.com/line/lbm-sdk/x/collection/keeper" +) + +type KeeperTestSuite struct { + suite.Suite + ctx sdk.Context + goCtx context.Context + keeper keeper.Keeper + queryServer collection.QueryServer + msgServer collection.MsgServer + + vendor sdk.AccAddress + operator sdk.AccAddress + customer sdk.AccAddress + stranger sdk.AccAddress + + contractID string + ftClassID string + nftClassID string + + balance sdk.Int + + numNFTs int +} + +func createRandomAccounts(accNum int) []sdk.AccAddress { + seenAddresses := make(map[sdk.AccAddress]bool, accNum) + addresses := make([]sdk.AccAddress, accNum) + for i := 0; i < accNum; i++ { + var address sdk.AccAddress + for { + pk := secp256k1.GenPrivKey().PubKey() + address = sdk.BytesToAccAddress(pk.Address()) + if !seenAddresses[address] { + seenAddresses[address] = true + break + } + } + addresses[i] = address + } + return addresses +} + +func (s *KeeperTestSuite) SetupTest() { + checkTx := false + app := simapp.Setup(checkTx) + s.ctx = app.BaseApp.NewContext(checkTx, ocproto.Header{}) + s.goCtx = sdk.WrapSDKContext(s.ctx) + s.keeper = app.CollectionKeeper + + s.queryServer = keeper.NewQueryServer(s.keeper) + s.msgServer = keeper.NewMsgServer(s.keeper) + + addresses := []*sdk.AccAddress{ + &s.vendor, + &s.operator, + &s.customer, + &s.stranger, + } + for i, address := range createRandomAccounts(len(addresses)) { + *addresses[i] = address + } + + s.balance = sdk.NewInt(1000000) + + // create a contract + s.contractID = s.keeper.CreateContract(s.ctx, s.vendor, collection.Contract{ + Name: "fox", + }) + + for _, permission := range []collection.Permission{ + collection.PermissionMint, + collection.PermissionBurn, + } { + s.keeper.Grant(s.ctx, s.contractID, s.vendor, s.operator, permission) + } + + // create a fungible token class + ftClassID, err := s.keeper.CreateTokenClass(s.ctx, s.contractID, &collection.FTClass{ + Name: "tibetian fox", + Mintable: true, + }) + s.Require().NoError(err) + s.ftClassID = *ftClassID + + // create a non-fungible token class + nftClassID, err := s.keeper.CreateTokenClass(s.ctx, s.contractID, &collection.NFTClass{ + Name: "fennec fox", + }) + s.Require().NoError(err) + s.nftClassID = *nftClassID + + // mint & burn fts + for _, to := range []sdk.AccAddress{s.customer, s.operator, s.vendor} { + tokenID := collection.NewFTID(s.ftClassID) + amount := collection.NewCoins(collection.NewCoin(tokenID, s.balance)) + + err := s.keeper.MintFT(s.ctx, s.contractID, to, amount) + s.Require().NoError(err) + + _, err = s.keeper.BurnCoins(s.ctx, s.contractID, to, amount) + s.Require().NoError(err) + err = s.keeper.MintFT(s.ctx, s.contractID, to, amount) + s.Require().NoError(err) + } + + // mint nfts + newParams := func(classID string, size int) []collection.MintNFTParam { + res := make([]collection.MintNFTParam, size) + for i := range res { + res[i] = collection.MintNFTParam{ + TokenType: s.nftClassID, + } + } + return res + } + // 1 for the successful attach, 2 for the failure + remainders := 1 + 2 + s.numNFTs = collection.DefaultDepthLimit + remainders + for _, to := range []sdk.AccAddress{s.customer, s.operator, s.vendor} { + tokens, err := s.keeper.MintNFT(s.ctx, s.contractID, to, newParams(s.nftClassID, collection.DefaultDepthLimit)) + s.Require().NoError(err) + + for i := range tokens[1:] { + r := len(tokens) - 1 - i + subject := tokens[r].Id + target := tokens[r-1].Id + err := s.keeper.Attach(s.ctx, s.contractID, to, subject, target) + s.Require().NoError(err) + } + + tokens, err = s.keeper.MintNFT(s.ctx, s.contractID, to, newParams(s.nftClassID, remainders)) + s.Require().NoError(err) + + err = s.keeper.Attach(s.ctx, s.contractID, to, tokens[remainders-1].Id, tokens[remainders-2].Id) + s.Require().NoError(err) + + } + + // authorize + err = s.keeper.AuthorizeOperator(s.ctx, s.contractID, s.customer, s.operator) + s.Require().NoError(err) + err = s.keeper.AuthorizeOperator(s.ctx, s.contractID, s.customer, s.stranger) + s.Require().NoError(err) +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} diff --git a/x/collection/keeper/keys.go b/x/collection/keeper/keys.go new file mode 100644 index 0000000000..4ce5e60b3b --- /dev/null +++ b/x/collection/keeper/keys.go @@ -0,0 +1,533 @@ +package keeper + +import ( + sdk "github.com/line/lbm-sdk/types" + + "github.com/line/lbm-sdk/x/collection" +) + +var ( + paramsKey = []byte{0x00} + + contractKeyPrefix = []byte{0x10} + classKeyPrefix = []byte{0x11} + nextClassIDKeyPrefix = []byte{0x12} + nextTokenIDKeyPrefix = []byte{0x13} + + balanceKeyPrefix = []byte{0x20} + ownerKeyPrefix = []byte{0x21} + nftKeyPrefix = []byte{0x22} + parentKeyPrefix = []byte{0x23} + childKeyPrefix = []byte{0x24} + + authorizationKeyPrefix = []byte{0x30} + grantKeyPrefix = []byte{0x31} + + supplyKeyPrefix = []byte{0x40} + mintedKeyPrefix = []byte{0x41} + burntKeyPrefix = []byte{0x42} + + legacyTokenKeyPrefix = []byte{0xf0} + legacyTokenTypeKeyPrefix = []byte{0xf1} +) + +func balanceKey(contractID string, address sdk.AccAddress, tokenID string) []byte { + prefix := balanceKeyPrefixByAddress(contractID, address) + key := make([]byte, len(prefix)+len(tokenID)) + + copy(key, prefix) + copy(key[len(prefix):], tokenID) + + return key +} + +func balanceKeyPrefixByAddress(contractID string, address sdk.AccAddress) []byte { + prefix := balanceKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+1+len(address)) + + begin := 0 + copy(key, prefix) + + begin += len(prefix) + key[begin] = byte(len(address)) + + begin++ + copy(key[begin:], address) + + return key +} + +func balanceKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(balanceKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, balanceKeyPrefix) + + begin += len(balanceKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func splitBalanceKey(key []byte) (contractID string, address sdk.AccAddress, tokenID string) { + begin := len(balanceKeyPrefix) + 1 + end := begin + int(key[begin-1]) + contractID = string(key[begin:end]) + + begin = end + 1 + end = begin + int(key[begin-1]) + address = sdk.AccAddress(key[begin:end]) + + begin = end + tokenID = string(key[begin:]) + + return +} + +//----------------------------------------------------------------------------- +// owner +func ownerKey(contractID string, tokenID string) []byte { + prefix := ownerKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+len(tokenID)) + + copy(key, prefix) + copy(key[len(prefix):], tokenID) + + return key +} + +func ownerKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(ownerKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, ownerKeyPrefix) + + begin += len(ownerKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +//----------------------------------------------------------------------------- +// nft +func nftKey(contractID string, tokenID string) []byte { + prefix := nftKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+len(tokenID)) + + copy(key, prefix) + copy(key[len(prefix):], tokenID) + + return key +} + +func nftKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(nftKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, nftKeyPrefix) + + begin += len(nftKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func splitNFTKey(key []byte) (contractID string, tokenID string) { + begin := len(nftKeyPrefix) + 1 + end := begin + int(key[begin-1]) + contractID = string(key[begin:end]) + + begin = end + tokenID = string(key[begin:]) + + return +} + +//----------------------------------------------------------------------------- +// parent +func parentKey(contractID string, tokenID string) []byte { + prefix := parentKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+len(tokenID)) + + copy(key, prefix) + copy(key[len(prefix):], tokenID) + + return key +} + +func parentKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(parentKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, parentKeyPrefix) + + begin += len(parentKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func splitParentKey(key []byte) (contractID string, tokenID string) { + begin := len(parentKeyPrefix) + 1 + end := begin + int(key[begin-1]) + contractID = string(key[begin:end]) + + begin = end + tokenID = string(key[begin:]) + + return +} + +//----------------------------------------------------------------------------- +// child +func childKey(contractID string, tokenID, childID string) []byte { + prefix := childKeyPrefixByTokenID(contractID, tokenID) + key := make([]byte, len(prefix)+len(childID)) + + copy(key, prefix) + copy(key[len(prefix):], childID) + + return key +} + +func childKeyPrefixByTokenID(contractID string, tokenID string) []byte { + prefix := childKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+1+len(tokenID)) + + begin := 0 + copy(key, prefix) + + begin += len(prefix) + key[begin] = byte(len(tokenID)) + + begin++ + copy(key[begin:], tokenID) + + return key +} + +func childKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(childKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, childKeyPrefix) + + begin += len(childKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func splitChildKey(key []byte) (contractID string, tokenID, childID string) { + begin := len(childKeyPrefix) + 1 + end := begin + int(key[begin-1]) + contractID = string(key[begin:end]) + + begin = end + 1 + end = begin + int(key[begin-1]) + tokenID = string(key[begin:end]) + + begin = end + childID = string(key[begin:]) + + return +} + +//----------------------------------------------------------------------------- +func contractKey(contractID string) []byte { + key := make([]byte, len(contractKeyPrefix)+len(contractID)) + + copy(key, contractKeyPrefix) + copy(key[len(contractKeyPrefix):], contractID) + + return key +} + +func classKey(contractID string, classID string) []byte { + prefix := classKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+len(classID)) + + copy(key, prefix) + copy(key[len(prefix):], classID) + + return key +} + +func classKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(classKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, classKeyPrefix) + + begin += len(classKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func nextTokenIDKey(contractID string, classID string) []byte { + prefix := nextTokenIDKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+len(classID)) + + copy(key, prefix) + copy(key[len(prefix):], classID) + + return key +} + +func nextTokenIDKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(nextTokenIDKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, nextTokenIDKeyPrefix) + + begin += len(nextTokenIDKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func splitNextTokenIDKey(key []byte) (contractID string, classID string) { + begin := len(nextTokenIDKeyPrefix) + 1 + end := begin + int(key[begin-1]) + contractID = string(key[begin:end]) + + begin = end + classID = string(key[begin:]) + + return +} + +func nextClassIDKey(contractID string) []byte { + key := make([]byte, len(nextClassIDKeyPrefix)+len(contractID)) + + copy(key, nextClassIDKeyPrefix) + copy(key[len(nextClassIDKeyPrefix):], contractID) + + return key +} + +//----------------------------------------------------------------------------- +func authorizationKey(contractID string, operator, holder sdk.AccAddress) []byte { + prefix := authorizationKeyPrefixByOperator(contractID, operator) + key := make([]byte, len(prefix)+len(holder)) + + copy(key, prefix) + copy(key[len(prefix):], holder) + + return key +} + +func authorizationKeyPrefixByOperator(contractID string, operator sdk.AccAddress) []byte { + prefix := authorizationKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+1+len(operator)) + + begin := 0 + copy(key, prefix) + + begin += len(prefix) + key[begin] = byte(len(operator)) + + begin++ + copy(key[begin:], operator) + + return key +} + +func authorizationKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(authorizationKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, authorizationKeyPrefix) + + begin += len(authorizationKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func splitAuthorizationKey(key []byte) (contractID string, operator, holder sdk.AccAddress) { + begin := len(authorizationKeyPrefix) + 1 + end := begin + int(key[begin-1]) + contractID = string(key[begin:end]) + + begin = end + 1 + end = begin + int(key[begin-1]) + operator = sdk.AccAddress(key[begin:end]) + + begin = end + holder = sdk.AccAddress(key[begin:]) + + return +} + +//----------------------------------------------------------------------------- +func grantKey(contractID string, grantee sdk.AccAddress, permission collection.Permission) []byte { + prefix := grantKeyPrefixByGrantee(contractID, grantee) + key := make([]byte, len(prefix)+1) + + copy(key, prefix) + key[len(prefix)] = byte(permission) + + return key +} + +func grantKeyPrefixByGrantee(contractID string, grantee sdk.AccAddress) []byte { + prefix := grantKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+1+len(grantee)) + + begin := 0 + copy(key, prefix) + + begin += len(prefix) + key[begin] = byte(len(grantee)) + + begin++ + copy(key[begin:], grantee) + + return key +} + +func grantKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(grantKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, grantKeyPrefix) + + begin += len(grantKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func splitGrantKey(key []byte) (contractID string, grantee sdk.AccAddress, permission collection.Permission) { + begin := len(grantKeyPrefix) + 1 + end := begin + int(key[begin-1]) + contractID = string(key[begin:end]) + + begin = end + 1 + end = begin + int(key[begin-1]) + grantee = sdk.AccAddress(key[begin:end]) + + begin = end + permission = collection.Permission(key[begin]) + + return +} + +//----------------------------------------------------------------------------- +// statistics +func statisticKey(keyPrefix []byte, contractID string, classID string) []byte { + prefix := statisticKeyPrefixByContractID(keyPrefix, contractID) + key := make([]byte, len(prefix)+len(classID)) + + copy(key, prefix) + copy(key[len(prefix):], classID) + + return key +} + +func statisticKeyPrefixByContractID(keyPrefix []byte, contractID string) []byte { + key := make([]byte, len(keyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, keyPrefix) + + begin += len(keyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func splitStatisticKey(keyPrefix, key []byte) (contractID string, classID string) { + begin := len(keyPrefix) + 1 + end := begin + int(key[begin-1]) + contractID = string(key[begin:end]) + + begin = end + classID = string(key[begin:]) + + return +} + +//----------------------------------------------------------------------------- +// legacy keys +func legacyTokenKey(contractID string, tokenID string) []byte { + prefix := legacyTokenKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+len(tokenID)) + + copy(key, prefix) + copy(key[len(prefix):], tokenID) + + return key +} + +func legacyTokenKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(legacyTokenKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, legacyTokenKeyPrefix) + + begin += len(legacyTokenKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} + +func legacyTokenTypeKey(contractID string, tokenType string) []byte { + prefix := legacyTokenTypeKeyPrefixByContractID(contractID) + key := make([]byte, len(prefix)+len(tokenType)) + + copy(key, prefix) + copy(key[len(prefix):], tokenType) + + return key +} + +func legacyTokenTypeKeyPrefixByContractID(contractID string) []byte { + key := make([]byte, len(legacyTokenTypeKeyPrefix)+1+len(contractID)) + + begin := 0 + copy(key, legacyTokenTypeKeyPrefix) + + begin += len(legacyTokenTypeKeyPrefix) + key[begin] = byte(len(contractID)) + + begin++ + copy(key[begin:], contractID) + + return key +} diff --git a/x/collection/keeper/msg_server.go b/x/collection/keeper/msg_server.go new file mode 100644 index 0000000000..d9789d1015 --- /dev/null +++ b/x/collection/keeper/msg_server.go @@ -0,0 +1,1119 @@ +package keeper + +import ( + "context" + + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" + "github.com/line/lbm-sdk/x/collection" +) + +type msgServer struct { + keeper Keeper +} + +// NewMsgServer returns an implementation of the collection MsgServer interface +// for the provided Keeper. +func NewMsgServer(keeper Keeper) collection.MsgServer { + return &msgServer{ + keeper: keeper, + } +} + +var _ collection.MsgServer = (*msgServer)(nil) + +func (s msgServer) Send(c context.Context, req *collection.MsgSend) (*collection.MsgSendResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + // emit legacy events. + event := collection.EventSent{ + ContractId: req.ContractId, + Operator: req.From, + From: req.From, + To: req.To, + Amount: req.Amount, + } + if legacyEvent := collection.NewEventTransferFT(event); legacyEvent != nil { + ctx.EventManager().EmitEvent(*legacyEvent) + } + ctx.EventManager().EmitEvents(collection.NewEventTransferNFT(event)) + + if err := s.keeper.SendCoins(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.To), req.Amount); err != nil { + return nil, err + } + + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgSendResponse{}, nil +} + +func (s msgServer) OperatorSend(c context.Context, req *collection.MsgOperatorSend) (*collection.MsgOperatorSendResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.Operator)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + // emit legacy events. + event := collection.EventSent{ + ContractId: req.ContractId, + Operator: req.Operator, + From: req.From, + To: req.To, + Amount: req.Amount, + } + if legacyEvent := collection.NewEventTransferFTFrom(event); legacyEvent != nil { + ctx.EventManager().EmitEvent(*legacyEvent) + } + ctx.EventManager().EmitEvents(collection.NewEventTransferNFTFrom(event)) + + if err := s.keeper.SendCoins(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.To), req.Amount); err != nil { + return nil, err + } + + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgOperatorSendResponse{}, nil +} + +func (s msgServer) TransferFT(c context.Context, req *collection.MsgTransferFT) (*collection.MsgTransferFTResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.SendCoins(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.To), req.Amount); err != nil { + return nil, err + } + + event := collection.EventSent{ + ContractId: req.ContractId, + Operator: req.From, + From: req.From, + To: req.To, + Amount: req.Amount, + } + ctx.EventManager().EmitEvent(*collection.NewEventTransferFT(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgTransferFTResponse{}, nil +} + +func (s msgServer) TransferFTFrom(c context.Context, req *collection.MsgTransferFTFrom) (*collection.MsgTransferFTFromResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.Proxy)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + if err := s.keeper.SendCoins(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.To), req.Amount); err != nil { + return nil, err + } + + event := collection.EventSent{ + ContractId: req.ContractId, + Operator: req.Proxy, + From: req.From, + To: req.To, + Amount: req.Amount, + } + ctx.EventManager().EmitEvent(*collection.NewEventTransferFTFrom(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgTransferFTFromResponse{}, nil +} + +func (s msgServer) TransferNFT(c context.Context, req *collection.MsgTransferNFT) (*collection.MsgTransferNFTResponse, error) { + amount := make([]collection.Coin, len(req.TokenIds)) + for i, id := range req.TokenIds { + amount[i] = collection.Coin{TokenId: id, Amount: sdk.OneInt()} + } + + ctx := sdk.UnwrapSDKContext(c) + + // emit legacy events + event := collection.EventSent{ + ContractId: req.ContractId, + Operator: req.From, + From: req.From, + To: req.To, + Amount: amount, + } + ctx.EventManager().EmitEvents(collection.NewEventTransferNFT(event)) + + if err := s.keeper.SendCoins(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.To), amount); err != nil { + return nil, err + } + + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgTransferNFTResponse{}, nil +} + +func (s msgServer) TransferNFTFrom(c context.Context, req *collection.MsgTransferNFTFrom) (*collection.MsgTransferNFTFromResponse, error) { + amount := make([]collection.Coin, len(req.TokenIds)) + for i, id := range req.TokenIds { + amount[i] = collection.Coin{TokenId: id, Amount: sdk.OneInt()} + } + + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.Proxy)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + // emit legacy events + event := collection.EventSent{ + ContractId: req.ContractId, + Operator: req.Proxy, + From: req.From, + To: req.To, + Amount: amount, + } + ctx.EventManager().EmitEvents(collection.NewEventTransferNFTFrom(event)) + + if err := s.keeper.SendCoins(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.To), amount); err != nil { + return nil, err + } + + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgTransferNFTFromResponse{}, nil +} + +func (s msgServer) AuthorizeOperator(c context.Context, req *collection.MsgAuthorizeOperator) (*collection.MsgAuthorizeOperatorResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.AuthorizeOperator(ctx, req.ContractId, sdk.AccAddress(req.Holder), sdk.AccAddress(req.Operator)); err != nil { + return nil, err + } + + event := collection.EventAuthorizedOperator{ + ContractId: req.ContractId, + Holder: req.Holder, + Operator: req.Operator, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgAuthorizeOperatorResponse{}, nil +} + +func (s msgServer) RevokeOperator(c context.Context, req *collection.MsgRevokeOperator) (*collection.MsgRevokeOperatorResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.RevokeOperator(ctx, req.ContractId, sdk.AccAddress(req.Holder), sdk.AccAddress(req.Operator)); err != nil { + return nil, err + } + + event := collection.EventRevokedOperator{ + ContractId: req.ContractId, + Holder: req.Holder, + Operator: req.Operator, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgRevokeOperatorResponse{}, nil +} + +func (s msgServer) Approve(c context.Context, req *collection.MsgApprove) (*collection.MsgApproveResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.AuthorizeOperator(ctx, req.ContractId, sdk.AccAddress(req.Approver), sdk.AccAddress(req.Proxy)); err != nil { + return nil, err + } + + event := collection.EventAuthorizedOperator{ + ContractId: req.ContractId, + Holder: req.Approver, + Operator: req.Proxy, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgApproveResponse{}, nil +} + +func (s msgServer) Disapprove(c context.Context, req *collection.MsgDisapprove) (*collection.MsgDisapproveResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if err := s.keeper.RevokeOperator(ctx, req.ContractId, sdk.AccAddress(req.Approver), sdk.AccAddress(req.Proxy)); err != nil { + return nil, err + } + + event := collection.EventRevokedOperator{ + ContractId: req.ContractId, + Holder: req.Approver, + Operator: req.Proxy, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgDisapproveResponse{}, nil +} + +func (s msgServer) CreateContract(c context.Context, req *collection.MsgCreateContract) (*collection.MsgCreateContractResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + contract := collection.Contract{ + Name: req.Name, + BaseImgUri: req.BaseImgUri, + Meta: req.Meta, + } + id := s.keeper.CreateContract(ctx, sdk.AccAddress(req.Owner), contract) + + return &collection.MsgCreateContractResponse{Id: id}, nil +} + +func (s msgServer) CreateFTClass(c context.Context, req *collection.MsgCreateFTClass) (*collection.MsgCreateFTClassResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.Operator), collection.PermissionIssue); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + mintable := !req.Supply.IsPositive() + class := &collection.FTClass{ + Name: req.Name, + Meta: req.Meta, + Decimals: req.Decimals, + Mintable: mintable, + } + id, err := s.keeper.CreateTokenClass(ctx, req.ContractId, class) + if err != nil { + return nil, err + } + + event := collection.EventCreatedFTClass{ + ContractId: req.ContractId, + ClassId: *id, + Name: class.Name, + Meta: class.Meta, + Decimals: class.Decimals, + Mintable: class.Mintable, + } + ctx.EventManager().EmitEvent(collection.NewEventIssueFT(event, sdk.AccAddress(req.Operator), sdk.AccAddress(req.To), req.Supply)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + // supply tokens + if req.Supply.IsPositive() { + s.keeper.mintFT(ctx, req.ContractId, sdk.AccAddress(req.To), *id, req.Supply) + + event := collection.EventMintedFT{ + ContractId: req.ContractId, + Operator: req.Operator, + To: req.To, + Amount: collection.NewCoins(collection.NewFTCoin(*id, req.Supply)), + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + } + + return &collection.MsgCreateFTClassResponse{Id: *id}, nil +} + +func (s msgServer) CreateNFTClass(c context.Context, req *collection.MsgCreateNFTClass) (*collection.MsgCreateNFTClassResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.Operator), collection.PermissionIssue); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + class := &collection.NFTClass{ + Name: req.Name, + Meta: req.Meta, + } + id, err := s.keeper.CreateTokenClass(ctx, req.ContractId, class) + if err != nil { + return nil, err + } + + event := collection.EventCreatedNFTClass{ + ContractId: req.ContractId, + ClassId: *id, + Name: class.Name, + Meta: class.Meta, + } + ctx.EventManager().EmitEvent(collection.NewEventIssueNFT(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgCreateNFTClassResponse{Id: *id}, nil +} + +func (s msgServer) IssueFT(c context.Context, req *collection.MsgIssueFT) (*collection.MsgIssueFTResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.Owner), collection.PermissionIssue); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + class := &collection.FTClass{ + Name: req.Name, + Meta: req.Meta, + Decimals: req.Decimals, + Mintable: req.Mintable, + } + id, err := s.keeper.CreateTokenClass(ctx, req.ContractId, class) + if err != nil { + return nil, err + } + + event := collection.EventCreatedFTClass{ + ContractId: req.ContractId, + ClassId: *id, + Name: class.Name, + Meta: class.Meta, + Decimals: class.Decimals, + Mintable: class.Mintable, + } + ctx.EventManager().EmitEvent(collection.NewEventIssueFT(event, sdk.AccAddress(req.Owner), sdk.AccAddress(req.To), req.Amount)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + // supply tokens + if req.Amount.IsPositive() { + s.keeper.mintFT(ctx, req.ContractId, sdk.AccAddress(req.To), *id, req.Amount) + + event := collection.EventMintedFT{ + ContractId: req.ContractId, + Operator: req.Owner, + To: req.To, + Amount: collection.NewCoins(collection.NewFTCoin(*id, req.Amount)), + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + } + + return &collection.MsgIssueFTResponse{Id: *id}, nil +} + +func (s msgServer) IssueNFT(c context.Context, req *collection.MsgIssueNFT) (*collection.MsgIssueNFTResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.Owner), collection.PermissionIssue); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + class := &collection.NFTClass{ + Name: req.Name, + Meta: req.Meta, + } + id, err := s.keeper.CreateTokenClass(ctx, req.ContractId, class) + if err != nil { + return nil, err + } + + event := collection.EventCreatedNFTClass{ + ContractId: req.ContractId, + ClassId: *id, + Name: class.Name, + Meta: class.Meta, + } + ctx.EventManager().EmitEvent(collection.NewEventIssueNFT(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + for _, permission := range []collection.Permission{ + collection.PermissionMint, + collection.PermissionBurn, + } { + s.keeper.Grant(ctx, req.ContractId, "", sdk.AccAddress(req.Owner), permission) + } + + return &collection.MsgIssueNFTResponse{Id: *id}, nil +} + +func (s msgServer) MintFT(c context.Context, req *collection.MsgMintFT) (*collection.MsgMintFTResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.From), collection.PermissionMint); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + if err := s.keeper.MintFT(ctx, req.ContractId, sdk.AccAddress(req.To), req.Amount); err != nil { + return nil, err + } + + event := collection.EventMintedFT{ + ContractId: req.ContractId, + Operator: req.From, + To: req.To, + Amount: req.Amount, + } + ctx.EventManager().EmitEvent(collection.NewEventMintFT(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgMintFTResponse{}, nil +} + +func (s msgServer) MintNFT(c context.Context, req *collection.MsgMintNFT) (*collection.MsgMintNFTResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.From), collection.PermissionMint); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + tokens, err := s.keeper.MintNFT(ctx, req.ContractId, sdk.AccAddress(req.To), req.Params) + if err != nil { + return nil, err + } + + event := collection.EventMintedNFT{ + ContractId: req.ContractId, + Operator: req.From, + To: req.To, + Tokens: tokens, + } + ctx.EventManager().EmitEvents(collection.NewEventMintNFT(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + tokenIDs := make([]string, 0, len(tokens)) + for _, token := range tokens { + tokenIDs = append(tokenIDs, token.Id) + } + return &collection.MsgMintNFTResponse{Ids: tokenIDs}, nil +} + +func (s msgServer) Burn(c context.Context, req *collection.MsgBurn) (*collection.MsgBurnResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.From), collection.PermissionBurn); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + // legacy: emit events against the original request. + event := collection.EventBurned{ + ContractId: req.ContractId, + Operator: req.From, + From: req.From, + Amount: req.Amount, + } + if e := collection.NewEventBurnFT(event); e != nil { + ctx.EventManager().EmitEvent(*e) + } + ctx.EventManager().EmitEvents(collection.NewEventBurnNFT(event)) + + burnt, err := s.keeper.BurnCoins(ctx, req.ContractId, sdk.AccAddress(req.From), req.Amount) + if err != nil { + return nil, err + } + + // emit events against all burnt tokens. + event.Amount = burnt + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgBurnResponse{}, nil +} + +func (s msgServer) OperatorBurn(c context.Context, req *collection.MsgOperatorBurn) (*collection.MsgOperatorBurnResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.Operator)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.Operator), collection.PermissionBurn); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + // legacy: emit events against the original request. + event := collection.EventBurned{ + ContractId: req.ContractId, + Operator: req.Operator, + From: req.From, + Amount: req.Amount, + } + if e := collection.NewEventBurnFTFrom(event); e != nil { + ctx.EventManager().EmitEvent(*e) + } + ctx.EventManager().EmitEvents(collection.NewEventBurnNFTFrom(event)) + + burnt, err := s.keeper.BurnCoins(ctx, req.ContractId, sdk.AccAddress(req.From), req.Amount) + if err != nil { + return nil, err + } + + // emit events against all burnt tokens. + event.Amount = burnt + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgOperatorBurnResponse{}, nil +} + +func (s msgServer) BurnFT(c context.Context, req *collection.MsgBurnFT) (*collection.MsgBurnFTResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.From), collection.PermissionBurn); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + burnt, err := s.keeper.BurnCoins(ctx, req.ContractId, sdk.AccAddress(req.From), req.Amount) + if err != nil { + return nil, err + } + + event := collection.EventBurned{ + ContractId: req.ContractId, + Operator: req.From, + From: req.From, + Amount: burnt, + } + if e := collection.NewEventBurnFT(event); e != nil { + ctx.EventManager().EmitEvent(*e) + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgBurnFTResponse{}, nil +} + +func (s msgServer) BurnFTFrom(c context.Context, req *collection.MsgBurnFTFrom) (*collection.MsgBurnFTFromResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.Proxy)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.Proxy), collection.PermissionBurn); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + burnt, err := s.keeper.BurnCoins(ctx, req.ContractId, sdk.AccAddress(req.From), req.Amount) + if err != nil { + return nil, err + } + + event := collection.EventBurned{ + ContractId: req.ContractId, + Operator: req.Proxy, + From: req.From, + Amount: burnt, + } + if e := collection.NewEventBurnFTFrom(event); e != nil { + ctx.EventManager().EmitEvent(*e) + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgBurnFTFromResponse{}, nil +} + +func (s msgServer) BurnNFT(c context.Context, req *collection.MsgBurnNFT) (*collection.MsgBurnNFTResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.From), collection.PermissionBurn); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + coins := make([]collection.Coin, 0, len(req.TokenIds)) + for _, id := range req.TokenIds { + coins = append(coins, collection.NewCoin(id, sdk.OneInt())) + } + + // legacy: emit events against the original request. + event := collection.EventBurned{ + ContractId: req.ContractId, + Operator: req.From, + From: req.From, + Amount: coins, + } + ctx.EventManager().EmitEvents(collection.NewEventBurnNFT(event)) + + burnt, err := s.keeper.BurnCoins(ctx, req.ContractId, sdk.AccAddress(req.From), coins) + if err != nil { + return nil, err + } + + // emit events against all burnt tokens. + event.Amount = burnt + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgBurnNFTResponse{}, nil +} + +func (s msgServer) BurnNFTFrom(c context.Context, req *collection.MsgBurnNFTFrom) (*collection.MsgBurnNFTFromResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.Proxy)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + if _, err := s.keeper.GetGrant(ctx, req.ContractId, sdk.AccAddress(req.Proxy), collection.PermissionBurn); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + coins := make([]collection.Coin, 0, len(req.TokenIds)) + for _, id := range req.TokenIds { + coins = append(coins, collection.NewCoin(id, sdk.OneInt())) + } + + // legacy: emit events against the original request. + event := collection.EventBurned{ + ContractId: req.ContractId, + Operator: req.Proxy, + From: req.From, + Amount: coins, + } + ctx.EventManager().EmitEvents(collection.NewEventBurnNFTFrom(event)) + + burnt, err := s.keeper.BurnCoins(ctx, req.ContractId, sdk.AccAddress(req.From), coins) + if err != nil { + return nil, err + } + + // emit events against all burnt tokens. + event.Amount = burnt + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgBurnNFTFromResponse{}, nil +} + +func (s msgServer) ModifyContract(c context.Context, req *collection.MsgModifyContract) (*collection.MsgModifyContractResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + operator := sdk.AccAddress(req.Operator) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, operator, collection.PermissionModify); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + if err := s.keeper.ModifyContract(ctx, req.ContractId, operator, req.Changes); err != nil { + return nil, err + } + + event := collection.EventModifiedContract{ + ContractId: req.ContractId, + Operator: req.Operator, + Changes: req.Changes, + } + ctx.EventManager().EmitEvents(collection.NewEventModifyCollection(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgModifyContractResponse{}, nil +} + +func (s msgServer) ModifyTokenClass(c context.Context, req *collection.MsgModifyTokenClass) (*collection.MsgModifyTokenClassResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + operator := sdk.AccAddress(req.Operator) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, operator, collection.PermissionModify); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + if err := s.keeper.ModifyTokenClass(ctx, req.ContractId, req.ClassId, operator, req.Changes); err != nil { + return nil, err + } + + event := collection.EventModifiedTokenClass{ + ContractId: req.ContractId, + Operator: req.Operator, + ClassId: req.ClassId, + Changes: req.Changes, + } + class, err := s.keeper.GetTokenClass(ctx, req.ContractId, req.ClassId) + if err != nil { + panic(err) + } + if _, ok := class.(*collection.FTClass); ok { + ctx.EventManager().EmitEvents(collection.NewEventModifyTokenOfFTClass(event)) + } + if _, ok := class.(*collection.NFTClass); ok { + ctx.EventManager().EmitEvents(collection.NewEventModifyTokenType(event)) + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgModifyTokenClassResponse{}, nil +} + +func (s msgServer) ModifyNFT(c context.Context, req *collection.MsgModifyNFT) (*collection.MsgModifyNFTResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + operator := sdk.AccAddress(req.Operator) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, operator, collection.PermissionModify); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + if err := s.keeper.ModifyNFT(ctx, req.ContractId, req.TokenId, operator, req.Changes); err != nil { + return nil, err + } + + event := collection.EventModifiedNFT{ + ContractId: req.ContractId, + Operator: req.Operator, + TokenId: req.TokenId, + Changes: req.Changes, + } + ctx.EventManager().EmitEvents(collection.NewEventModifyTokenOfNFT(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return &collection.MsgModifyNFTResponse{}, nil +} + +func (s msgServer) Modify(c context.Context, req *collection.MsgModify) (*collection.MsgModifyResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + operator := sdk.AccAddress(req.Owner) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, operator, collection.PermissionModify); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + // copied from daphne + modify := func(tokenType, tokenIndex string) error { + changes := make([]collection.Attribute, len(req.Changes)) + for i, change := range req.Changes { + changes[i] = collection.Attribute{ + Key: change.Field, + Value: change.Field, + } + } + + classID := tokenType + tokenID := classID + tokenIndex + if tokenType != "" { + if tokenIndex != "" { + if collection.ValidateNFTID(tokenID) == nil { + event := collection.EventModifiedNFT{ + ContractId: req.ContractId, + Operator: operator.String(), + TokenId: tokenID, + Changes: changes, + } + ctx.EventManager().EmitEvents(collection.NewEventModifyTokenOfNFT(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return s.keeper.ModifyNFT(ctx, req.ContractId, tokenID, operator, changes) + } + + event := collection.EventModifiedTokenClass{ + ContractId: req.ContractId, + Operator: operator.String(), + ClassId: classID, + Changes: changes, + } + + ctx.EventManager().EmitEvents(collection.NewEventModifyTokenOfFTClass(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return s.keeper.ModifyTokenClass(ctx, req.ContractId, classID, operator, changes) + } + + event := collection.EventModifiedTokenClass{ + ContractId: req.ContractId, + Operator: operator.String(), + ClassId: classID, + Changes: changes, + } + ctx.EventManager().EmitEvents(collection.NewEventModifyTokenType(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return s.keeper.ModifyTokenClass(ctx, req.ContractId, classID, operator, changes) + } + if req.TokenIndex == "" { + event := collection.EventModifiedContract{ + ContractId: req.ContractId, + Operator: operator.String(), + Changes: changes, + } + ctx.EventManager().EmitEvents(collection.NewEventModifyCollection(event)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + return s.keeper.ModifyContract(ctx, req.ContractId, operator, changes) + } + + return sdkerrors.ErrInvalidRequest.Wrap("token index without type") + } + + if err := modify(req.TokenType, req.TokenIndex); err != nil { + return nil, err + } + + return &collection.MsgModifyResponse{}, nil +} + +func (s msgServer) Grant(c context.Context, req *collection.MsgGrant) (*collection.MsgGrantResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + granter := sdk.AccAddress(req.Granter) + grantee := sdk.AccAddress(req.Grantee) + + if _, err := s.keeper.GetGrant(ctx, req.ContractId, granter, req.Permission); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrapf("%s is not authorized for %s", granter, req.Permission) + } + if _, err := s.keeper.GetGrant(ctx, req.ContractId, grantee, req.Permission); err == nil { + return nil, sdkerrors.ErrInvalidRequest.Wrapf("%s is already granted for %s", grantee, req.Permission) + } + + s.keeper.Grant(ctx, req.ContractId, granter, grantee, req.Permission) + + event := collection.EventGrant{ + ContractId: req.ContractId, + Granter: granter.String(), + Grantee: grantee.String(), + Permission: req.Permission, + } + ctx.EventManager().EmitEvent(collection.NewEventGrantPermToken(event)) + // it emits typed event inside s.keeper.Grant() + + return &collection.MsgGrantResponse{}, nil +} + +func (s msgServer) Abandon(c context.Context, req *collection.MsgAbandon) (*collection.MsgAbandonResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + grantee := sdk.AccAddress(req.Grantee) + + if _, err := s.keeper.GetGrant(ctx, req.ContractId, grantee, req.Permission); err != nil { + return nil, sdkerrors.ErrNotFound.Wrapf("%s is not authorized for %s", grantee, req.Permission) + } + + s.keeper.Abandon(ctx, req.ContractId, grantee, req.Permission) + + event := collection.EventAbandon{ + ContractId: req.ContractId, + Grantee: grantee.String(), + Permission: req.Permission, + } + ctx.EventManager().EmitEvent(collection.NewEventRevokePermToken(event)) + // it emits typed event inside s.keeper.Abandon() + + return &collection.MsgAbandonResponse{}, nil +} + +func (s msgServer) GrantPermission(c context.Context, req *collection.MsgGrantPermission) (*collection.MsgGrantPermissionResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + granter := sdk.AccAddress(req.From) + grantee := sdk.AccAddress(req.To) + permission := collection.Permission(collection.LegacyPermissionFromString(req.Permission)) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, granter, permission); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrapf("%s is not authorized for %s", granter, permission) + } + if _, err := s.keeper.GetGrant(ctx, req.ContractId, grantee, permission); err == nil { + return nil, sdkerrors.ErrInvalidRequest.Wrapf("%s is already granted for %s", grantee, permission) + } + + s.keeper.Grant(ctx, req.ContractId, granter, grantee, permission) + + event := collection.EventGrant{ + ContractId: req.ContractId, + Granter: granter.String(), + Grantee: grantee.String(), + Permission: permission, + } + ctx.EventManager().EmitEvent(collection.NewEventGrantPermToken(event)) + // it emits typed event inside s.keeper.Grant() + + return &collection.MsgGrantPermissionResponse{}, nil +} + +func (s msgServer) RevokePermission(c context.Context, req *collection.MsgRevokePermission) (*collection.MsgRevokePermissionResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + grantee := sdk.AccAddress(req.From) + permission := collection.Permission(collection.LegacyPermissionFromString(req.Permission)) + if _, err := s.keeper.GetGrant(ctx, req.ContractId, grantee, permission); err != nil { + return nil, sdkerrors.ErrNotFound.Wrapf("%s is not authorized for %s", grantee, permission) + } + + s.keeper.Abandon(ctx, req.ContractId, grantee, permission) + + event := collection.EventAbandon{ + ContractId: req.ContractId, + Grantee: grantee.String(), + Permission: permission, + } + ctx.EventManager().EmitEvent(collection.NewEventRevokePermToken(event)) + // it emits typed event inside s.keeper.Abandon() + + return &collection.MsgRevokePermissionResponse{}, nil +} + +func (s msgServer) Attach(c context.Context, req *collection.MsgAttach) (*collection.MsgAttachResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + event := collection.EventAttached{ + ContractId: req.ContractId, + Operator: req.From, + Holder: req.From, + Subject: req.TokenId, + Target: req.ToTokenId, + } + newRoot := s.keeper.GetRoot(ctx, req.ContractId, req.ToTokenId) + ctx.EventManager().EmitEvent(collection.NewEventAttachToken(event, newRoot)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + if err := s.keeper.Attach(ctx, req.ContractId, sdk.AccAddress(req.From), req.TokenId, req.ToTokenId); err != nil { + return nil, err + } + + return &collection.MsgAttachResponse{}, nil +} + +func (s msgServer) Detach(c context.Context, req *collection.MsgDetach) (*collection.MsgDetachResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + // legacy + if err := s.keeper.hasNFT(ctx, req.ContractId, req.TokenId); err != nil { + return nil, err + } + oldRoot := s.keeper.GetRoot(ctx, req.ContractId, req.TokenId) + + event := collection.EventDetached{ + ContractId: req.ContractId, + Operator: req.From, + Holder: req.From, + Subject: req.TokenId, + } + ctx.EventManager().EmitEvent(collection.NewEventDetachToken(event, oldRoot)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + if err := s.keeper.Detach(ctx, req.ContractId, sdk.AccAddress(req.From), req.TokenId); err != nil { + return nil, err + } + + return &collection.MsgDetachResponse{}, nil +} + +func (s msgServer) OperatorAttach(c context.Context, req *collection.MsgOperatorAttach) (*collection.MsgOperatorAttachResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.Owner), sdk.AccAddress(req.Operator)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + event := collection.EventAttached{ + ContractId: req.ContractId, + Operator: req.Operator, + Holder: req.Owner, + Subject: req.Subject, + Target: req.Target, + } + newRoot := s.keeper.GetRoot(ctx, req.ContractId, req.Target) + ctx.EventManager().EmitEvent(collection.NewEventAttachFrom(event, newRoot)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + if err := s.keeper.Attach(ctx, req.ContractId, sdk.AccAddress(req.Owner), req.Subject, req.Target); err != nil { + return nil, err + } + + return &collection.MsgOperatorAttachResponse{}, nil +} + +func (s msgServer) OperatorDetach(c context.Context, req *collection.MsgOperatorDetach) (*collection.MsgOperatorDetachResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.Owner), sdk.AccAddress(req.Operator)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + // legacy + if err := s.keeper.hasNFT(ctx, req.ContractId, req.Subject); err != nil { + return nil, err + } + oldRoot := s.keeper.GetRoot(ctx, req.ContractId, req.Subject) + + event := collection.EventDetached{ + ContractId: req.ContractId, + Operator: req.Operator, + Holder: req.Owner, + Subject: req.Subject, + } + ctx.EventManager().EmitEvent(collection.NewEventDetachFrom(event, oldRoot)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + if err := s.keeper.Detach(ctx, req.ContractId, sdk.AccAddress(req.Owner), req.Subject); err != nil { + return nil, err + } + + return &collection.MsgOperatorDetachResponse{}, nil +} + +func (s msgServer) AttachFrom(c context.Context, req *collection.MsgAttachFrom) (*collection.MsgAttachFromResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.Proxy)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + event := collection.EventAttached{ + ContractId: req.ContractId, + Operator: req.Proxy, + Holder: req.From, + Subject: req.TokenId, + Target: req.ToTokenId, + } + newRoot := s.keeper.GetRoot(ctx, req.ContractId, req.ToTokenId) + ctx.EventManager().EmitEvent(collection.NewEventAttachFrom(event, newRoot)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + if err := s.keeper.Attach(ctx, req.ContractId, sdk.AccAddress(req.From), req.TokenId, req.ToTokenId); err != nil { + return nil, err + } + + return &collection.MsgAttachFromResponse{}, nil +} + +func (s msgServer) DetachFrom(c context.Context, req *collection.MsgDetachFrom) (*collection.MsgDetachFromResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + if _, err := s.keeper.GetAuthorization(ctx, req.ContractId, sdk.AccAddress(req.From), sdk.AccAddress(req.Proxy)); err != nil { + return nil, sdkerrors.ErrUnauthorized.Wrap(err.Error()) + } + + // legacy + if err := s.keeper.hasNFT(ctx, req.ContractId, req.TokenId); err != nil { + return nil, err + } + oldRoot := s.keeper.GetRoot(ctx, req.ContractId, req.TokenId) + + event := collection.EventDetached{ + ContractId: req.ContractId, + Operator: req.Proxy, + Holder: req.From, + Subject: req.TokenId, + } + ctx.EventManager().EmitEvent(collection.NewEventDetachFrom(event, oldRoot)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + if err := s.keeper.Detach(ctx, req.ContractId, sdk.AccAddress(req.From), req.TokenId); err != nil { + return nil, err + } + + return &collection.MsgDetachFromResponse{}, nil +} diff --git a/x/collection/keeper/msg_server_test.go b/x/collection/keeper/msg_server_test.go new file mode 100644 index 0000000000..91aff35a02 --- /dev/null +++ b/x/collection/keeper/msg_server_test.go @@ -0,0 +1,1698 @@ +package keeper_test + +import ( + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +func (s *KeeperTestSuite) TestMsgSend() { + testCases := map[string]struct { + contractID string + amount sdk.Int + valid bool + }{ + "valid request": { + contractID: s.contractID, + amount: s.balance, + valid: true, + }, + "insufficient funds": { + contractID: "deadbeef", + amount: s.balance, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgSend{ + ContractId: tc.contractID, + From: s.vendor.String(), + To: s.customer.String(), + Amount: collection.NewCoins( + collection.NewFTCoin(s.ftClassID, tc.amount), + ), + } + res, err := s.msgServer.Send(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgOperatorSend() { + testCases := map[string]struct { + operator sdk.AccAddress + from sdk.AccAddress + amount sdk.Int + valid bool + }{ + "valid request": { + operator: s.operator, + from: s.customer, + amount: s.balance, + valid: true, + }, + "not approved": { + operator: s.vendor, + from: s.customer, + amount: s.balance, + }, + "insufficient funds": { + operator: s.operator, + from: s.customer, + amount: s.balance.Add(sdk.OneInt()), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgOperatorSend{ + ContractId: s.contractID, + Operator: tc.operator.String(), + From: tc.from.String(), + To: s.vendor.String(), + Amount: collection.NewCoins( + collection.NewFTCoin(s.ftClassID, tc.amount), + ), + } + res, err := s.msgServer.OperatorSend(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgTransferFT() { + testCases := map[string]struct { + contractID string + amount sdk.Int + valid bool + }{ + "valid request": { + contractID: s.contractID, + amount: s.balance, + valid: true, + }, + "insufficient funds": { + contractID: "deadbeef", + amount: s.balance, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgTransferFT{ + ContractId: tc.contractID, + From: s.vendor.String(), + To: s.customer.String(), + Amount: collection.NewCoins( + collection.NewFTCoin(s.ftClassID, tc.amount), + ), + } + res, err := s.msgServer.TransferFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgTransferFTFrom() { + testCases := map[string]struct { + proxy sdk.AccAddress + from sdk.AccAddress + amount sdk.Int + valid bool + }{ + "valid request": { + proxy: s.operator, + from: s.customer, + amount: s.balance, + valid: true, + }, + "not approved": { + proxy: s.vendor, + from: s.customer, + amount: s.balance, + }, + "insufficient funds": { + proxy: s.operator, + from: s.customer, + amount: s.balance.Add(sdk.OneInt()), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgTransferFTFrom{ + ContractId: s.contractID, + Proxy: tc.proxy.String(), + From: tc.from.String(), + To: s.vendor.String(), + Amount: collection.NewCoins( + collection.NewFTCoin(s.ftClassID, tc.amount), + ), + } + res, err := s.msgServer.TransferFTFrom(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgTransferNFT() { + testCases := map[string]struct { + tokenID string + valid bool + }{ + "valid request": { + tokenID: collection.NewNFTID(s.nftClassID, 1), + valid: true, + }, + "insufficient funds": { + tokenID: collection.NewNFTID("deadbeef", 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgTransferNFT{ + ContractId: s.contractID, + From: s.customer.String(), + To: s.vendor.String(), + TokenIds: []string{tc.tokenID}, + } + res, err := s.msgServer.TransferNFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgTransferNFTFrom() { + tokenID := collection.NewNFTID(s.nftClassID, 1) + testCases := map[string]struct { + proxy sdk.AccAddress + from sdk.AccAddress + tokenID string + valid bool + }{ + "valid request": { + proxy: s.operator, + from: s.customer, + tokenID: tokenID, + valid: true, + }, + "not approved": { + proxy: s.vendor, + from: s.customer, + tokenID: tokenID, + }, + "insufficient funds": { + proxy: s.operator, + from: s.customer, + tokenID: collection.NewNFTID("deadbeef", 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgTransferNFTFrom{ + ContractId: s.contractID, + Proxy: tc.proxy.String(), + From: tc.from.String(), + To: s.vendor.String(), + TokenIds: []string{tc.tokenID}, + } + res, err := s.msgServer.TransferNFTFrom(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgAuthorizeOperator() { + testCases := map[string]struct { + holder sdk.AccAddress + operator sdk.AccAddress + valid bool + }{ + "valid request": { + holder: s.customer, + operator: s.vendor, + valid: true, + }, + "already approved": { + holder: s.customer, + operator: s.operator, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgAuthorizeOperator{ + ContractId: s.contractID, + Holder: tc.holder.String(), + Operator: tc.operator.String(), + } + res, err := s.msgServer.AuthorizeOperator(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgRevokeOperator() { + testCases := map[string]struct { + holder sdk.AccAddress + operator sdk.AccAddress + valid bool + }{ + "valid request": { + holder: s.customer, + operator: s.operator, + valid: true, + }, + "no authorization": { + holder: s.customer, + operator: s.vendor, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgRevokeOperator{ + ContractId: s.contractID, + Holder: tc.holder.String(), + Operator: tc.operator.String(), + } + res, err := s.msgServer.RevokeOperator(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgApprove() { + testCases := map[string]struct { + approver sdk.AccAddress + proxy sdk.AccAddress + valid bool + }{ + "valid request": { + approver: s.customer, + proxy: s.vendor, + valid: true, + }, + "already approved": { + approver: s.customer, + proxy: s.operator, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgApprove{ + ContractId: s.contractID, + Approver: tc.approver.String(), + Proxy: tc.proxy.String(), + } + res, err := s.msgServer.Approve(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgDisapprove() { + testCases := map[string]struct { + approver sdk.AccAddress + proxy sdk.AccAddress + valid bool + }{ + "valid request": { + approver: s.customer, + proxy: s.operator, + valid: true, + }, + "no authorization": { + approver: s.customer, + proxy: s.vendor, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgDisapprove{ + ContractId: s.contractID, + Approver: tc.approver.String(), + Proxy: tc.proxy.String(), + } + res, err := s.msgServer.Disapprove(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgCreateContract() { + testCases := map[string]struct { + owner sdk.AccAddress + valid bool + }{ + "valid request": { + owner: s.vendor, + valid: true, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgCreateContract{ + Owner: tc.owner.String(), + } + res, err := s.msgServer.CreateContract(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgCreateFTClass() { + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + supply sdk.Int + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.vendor, + supply: sdk.ZeroInt(), + valid: true, + }, + "valid request with supply": { + contractID: s.contractID, + operator: s.vendor, + supply: sdk.OneInt(), + valid: true, + }, + "no permission": { + contractID: s.contractID, + operator: s.customer, + supply: sdk.ZeroInt(), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgCreateFTClass{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + To: s.customer.String(), + Supply: tc.supply, + } + res, err := s.msgServer.CreateFTClass(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgCreateNFTClass() { + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.vendor, + valid: true, + }, + "no permission": { + contractID: s.contractID, + operator: s.customer, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgCreateNFTClass{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + } + res, err := s.msgServer.CreateNFTClass(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgIssueFT() { + testCases := map[string]struct { + contractID string + owner sdk.AccAddress + amount sdk.Int + valid bool + }{ + "valid request": { + contractID: s.contractID, + owner: s.vendor, + amount: sdk.ZeroInt(), + valid: true, + }, + "valid request with supply": { + contractID: s.contractID, + owner: s.vendor, + amount: sdk.OneInt(), + valid: true, + }, + "no permission": { + contractID: s.contractID, + owner: s.customer, + amount: sdk.ZeroInt(), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgIssueFT{ + ContractId: tc.contractID, + Owner: tc.owner.String(), + Amount: tc.amount, + } + res, err := s.msgServer.IssueFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgIssueNFT() { + testCases := map[string]struct { + contractID string + owner sdk.AccAddress + valid bool + }{ + "valid request": { + contractID: s.contractID, + owner: s.vendor, + valid: true, + }, + "no permission": { + contractID: s.contractID, + owner: s.customer, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgIssueNFT{ + ContractId: tc.contractID, + Owner: tc.owner.String(), + } + res, err := s.msgServer.IssueNFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgMintFT() { + amount := collection.NewCoins( + collection.NewFTCoin(s.ftClassID, sdk.OneInt()), + ) + testCases := map[string]struct { + contractID string + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid request": { + contractID: s.contractID, + from: s.vendor, + amount: amount, + valid: true, + }, + "no permission": { + contractID: s.contractID, + from: s.customer, + amount: amount, + }, + "no class of the token": { + contractID: s.contractID, + from: s.vendor, + amount: collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgMintFT{ + ContractId: tc.contractID, + From: tc.from.String(), + To: s.customer.String(), + Amount: tc.amount, + } + res, err := s.msgServer.MintFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgMintNFT() { + params := []collection.MintNFTParam{{ + TokenType: s.nftClassID, + }} + testCases := map[string]struct { + contractID string + from sdk.AccAddress + params []collection.MintNFTParam + valid bool + }{ + "valid request": { + contractID: s.contractID, + from: s.vendor, + params: params, + valid: true, + }, + "no permission": { + contractID: s.contractID, + from: s.customer, + params: params, + }, + "no class of the token": { + contractID: s.contractID, + from: s.vendor, + params: []collection.MintNFTParam{{ + TokenType: "deadbeef", + }}, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgMintNFT{ + ContractId: tc.contractID, + From: tc.from.String(), + To: s.customer.String(), + Params: tc.params, + } + res, err := s.msgServer.MintNFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgBurn() { + amount := collection.NewCoins( + collection.NewFTCoin(s.ftClassID, s.balance), + ) + testCases := map[string]struct { + contractID string + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid request": { + contractID: s.contractID, + from: s.vendor, + amount: amount, + valid: true, + }, + "no permission": { + contractID: s.contractID, + from: s.customer, + amount: amount, + }, + "insufficient funds": { + contractID: s.contractID, + from: s.vendor, + amount: collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgBurn{ + ContractId: tc.contractID, + From: tc.from.String(), + Amount: tc.amount, + } + res, err := s.msgServer.Burn(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgOperatorBurn() { + amount := collection.NewCoins( + collection.NewFTCoin(s.ftClassID, s.balance), + ) + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.operator, + from: s.customer, + amount: amount, + valid: true, + }, + "no authorization": { + contractID: s.contractID, + operator: s.vendor, + from: s.customer, + amount: amount, + }, + "no permission": { + contractID: s.contractID, + operator: s.stranger, + from: s.customer, + amount: amount, + }, + "insufficient funds": { + contractID: s.contractID, + operator: s.operator, + from: s.customer, + amount: collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgOperatorBurn{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + From: tc.from.String(), + Amount: tc.amount, + } + res, err := s.msgServer.OperatorBurn(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgBurnFT() { + amount := collection.NewCoins( + collection.NewFTCoin(s.ftClassID, s.balance), + ) + testCases := map[string]struct { + contractID string + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid request": { + contractID: s.contractID, + from: s.vendor, + amount: amount, + valid: true, + }, + "no permission": { + contractID: s.contractID, + from: s.customer, + amount: amount, + }, + "insufficient funds": { + contractID: s.contractID, + from: s.vendor, + amount: collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgBurnFT{ + ContractId: tc.contractID, + From: tc.from.String(), + Amount: tc.amount, + } + res, err := s.msgServer.BurnFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgBurnFTFrom() { + amount := collection.NewCoins( + collection.NewFTCoin(s.ftClassID, s.balance), + ) + testCases := map[string]struct { + contractID string + proxy sdk.AccAddress + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid request": { + contractID: s.contractID, + proxy: s.operator, + from: s.customer, + amount: amount, + valid: true, + }, + "no authorization": { + contractID: s.contractID, + proxy: s.vendor, + from: s.customer, + amount: amount, + }, + "no permission": { + contractID: s.contractID, + proxy: s.stranger, + from: s.customer, + amount: amount, + }, + "insufficient funds": { + contractID: s.contractID, + proxy: s.operator, + from: s.customer, + amount: collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgBurnFTFrom{ + ContractId: tc.contractID, + Proxy: tc.proxy.String(), + From: tc.from.String(), + Amount: tc.amount, + } + res, err := s.msgServer.BurnFTFrom(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgBurnNFT() { + tokenIDs := []string{ + collection.NewNFTID(s.nftClassID, s.numNFTs*2+1), + } + testCases := map[string]struct { + contractID string + from sdk.AccAddress + tokenIDs []string + valid bool + }{ + "valid request": { + contractID: s.contractID, + from: s.vendor, + tokenIDs: tokenIDs, + valid: true, + }, + "no permission": { + contractID: s.contractID, + from: s.customer, + tokenIDs: tokenIDs, + }, + "insufficient funds": { + contractID: s.contractID, + from: s.vendor, + tokenIDs: []string{ + collection.NewNFTID("deadbeef", 1), + }, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgBurnNFT{ + ContractId: tc.contractID, + From: tc.from.String(), + TokenIds: tc.tokenIDs, + } + res, err := s.msgServer.BurnNFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgBurnNFTFrom() { + tokenIDs := []string{ + collection.NewNFTID(s.nftClassID, 1), + } + testCases := map[string]struct { + contractID string + proxy sdk.AccAddress + from sdk.AccAddress + tokenIDs []string + valid bool + }{ + "valid request": { + contractID: s.contractID, + proxy: s.operator, + from: s.customer, + tokenIDs: tokenIDs, + valid: true, + }, + "no authorization": { + contractID: s.contractID, + proxy: s.vendor, + from: s.customer, + tokenIDs: tokenIDs, + }, + "no permission": { + contractID: s.contractID, + proxy: s.stranger, + from: s.customer, + tokenIDs: tokenIDs, + }, + "insufficient funds": { + contractID: s.contractID, + proxy: s.operator, + from: s.customer, + tokenIDs: []string{ + collection.NewNFTID("deadbeef", 1), + }, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgBurnNFTFrom{ + ContractId: tc.contractID, + Proxy: tc.proxy.String(), + From: tc.from.String(), + TokenIds: tc.tokenIDs, + } + res, err := s.msgServer.BurnNFTFrom(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgModifyContract() { + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.vendor, + valid: true, + }, + "no permission": { + contractID: s.contractID, + operator: s.customer, + }, + "contract not found": { + contractID: "deadbeef", + operator: s.vendor, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + changes := []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: "fox", + }} + req := &collection.MsgModifyContract{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Changes: changes, + } + res, err := s.msgServer.ModifyContract(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgModifyTokenClass() { + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + classID string + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.vendor, + classID: s.nftClassID, + valid: true, + }, + "no permission": { + contractID: s.contractID, + operator: s.customer, + classID: s.nftClassID, + }, + "token class not found": { + contractID: s.contractID, + operator: s.vendor, + classID: "deadbeef", + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + changes := []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: "arctic fox", + }} + req := &collection.MsgModifyTokenClass{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + ClassId: tc.classID, + Changes: changes, + } + res, err := s.msgServer.ModifyTokenClass(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgModifyNFT() { + tokenID := collection.NewNFTID(s.nftClassID, 1) + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + tokenID string + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.vendor, + tokenID: tokenID, + valid: true, + }, + "no permission": { + contractID: s.contractID, + operator: s.customer, + tokenID: tokenID, + }, + "token not found": { + contractID: s.contractID, + operator: s.vendor, + tokenID: collection.NewNFTID("deadbeef", 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + changes := []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: "fennec fox 1", + }} + req := &collection.MsgModifyNFT{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + TokenId: tc.tokenID, + Changes: changes, + } + res, err := s.msgServer.ModifyNFT(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgModify() { + tokenIndex := collection.NewNFTID(s.nftClassID, 1)[8:] + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + tokenType string + tokenIndex string + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.vendor, + valid: true, + }, + "no permission": { + contractID: s.contractID, + operator: s.customer, + tokenType: s.nftClassID, + tokenIndex: tokenIndex, + }, + "nft not found": { + contractID: s.contractID, + operator: s.vendor, + tokenType: s.nftClassID, + tokenIndex: collection.NewNFTID(s.nftClassID, s.numNFTs*3+1)[8:], + }, + "ft class not found": { + contractID: s.contractID, + operator: s.vendor, + tokenType: "00bab10c", + tokenIndex: collection.NewFTID("00bab10c")[8:], + }, + "nft class not found": { + contractID: s.contractID, + operator: s.vendor, + tokenType: "deadbeef", + }, + "token index without type": { + contractID: s.contractID, + operator: s.vendor, + tokenIndex: "deadbeef", + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + changes := []collection.Change{{ + Field: collection.AttributeKeyName.String(), + Value: "test", + }} + req := &collection.MsgModify{ + ContractId: tc.contractID, + Owner: tc.operator.String(), + TokenType: tc.tokenType, + TokenIndex: tc.tokenIndex, + Changes: changes, + } + res, err := s.msgServer.Modify(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgGrant() { + testCases := map[string]struct { + granter sdk.AccAddress + grantee sdk.AccAddress + permission collection.Permission + valid bool + }{ + "valid request": { + granter: s.vendor, + grantee: s.operator, + permission: collection.PermissionModify, + valid: true, + }, + "already granted": { + granter: s.vendor, + grantee: s.operator, + permission: collection.PermissionMint, + }, + "granter has no permission": { + granter: s.customer, + grantee: s.operator, + permission: collection.PermissionModify, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgGrant{ + ContractId: s.contractID, + Granter: tc.granter.String(), + Grantee: tc.grantee.String(), + Permission: tc.permission, + } + res, err := s.msgServer.Grant(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgAbandon() { + testCases := map[string]struct { + grantee sdk.AccAddress + permission collection.Permission + valid bool + }{ + "valid request": { + grantee: s.operator, + permission: collection.PermissionMint, + valid: true, + }, + "not granted yet": { + grantee: s.operator, + permission: collection.PermissionModify, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgAbandon{ + ContractId: s.contractID, + Grantee: tc.grantee.String(), + Permission: tc.permission, + } + res, err := s.msgServer.Abandon(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgGrantPermission() { + testCases := map[string]struct { + granter sdk.AccAddress + grantee sdk.AccAddress + permission string + valid bool + }{ + "valid request": { + granter: s.vendor, + grantee: s.operator, + permission: collection.LegacyPermissionModify.String(), + valid: true, + }, + "already granted": { + granter: s.vendor, + grantee: s.operator, + permission: collection.LegacyPermissionMint.String(), + }, + "granter has no permission": { + granter: s.customer, + grantee: s.operator, + permission: collection.LegacyPermissionModify.String(), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgGrantPermission{ + ContractId: s.contractID, + From: tc.granter.String(), + To: tc.grantee.String(), + Permission: tc.permission, + } + res, err := s.msgServer.GrantPermission(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgRevokePermission() { + testCases := map[string]struct { + from sdk.AccAddress + permission string + valid bool + }{ + "valid request": { + from: s.operator, + permission: collection.LegacyPermissionMint.String(), + valid: true, + }, + "not granted yet": { + from: s.operator, + permission: collection.LegacyPermissionModify.String(), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgRevokePermission{ + ContractId: s.contractID, + From: tc.from.String(), + Permission: tc.permission, + } + res, err := s.msgServer.RevokePermission(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgAttach() { + testCases := map[string]struct { + contractID string + subjectID string + targetID string + valid bool + }{ + "valid request": { + contractID: s.contractID, + subjectID: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1), + targetID: collection.NewNFTID(s.nftClassID, 1), + valid: true, + }, + "not owner of the token": { + contractID: s.contractID, + subjectID: collection.NewNFTID(s.nftClassID, s.numNFTs+1), + targetID: collection.NewNFTID(s.nftClassID, 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgAttach{ + ContractId: tc.contractID, + From: s.customer.String(), + TokenId: tc.subjectID, + ToTokenId: tc.targetID, + } + res, err := s.msgServer.Attach(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgDetach() { + testCases := map[string]struct { + contractID string + subjectID string + valid bool + }{ + "valid request": { + contractID: s.contractID, + subjectID: collection.NewNFTID(s.nftClassID, 2), + valid: true, + }, + "not owner of the token": { + contractID: s.contractID, + subjectID: collection.NewNFTID(s.nftClassID, s.numNFTs+2), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgDetach{ + ContractId: tc.contractID, + From: s.customer.String(), + TokenId: tc.subjectID, + } + res, err := s.msgServer.Detach(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgOperatorAttach() { + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + subjectID string + targetID string + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.operator, + subjectID: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1), + targetID: collection.NewNFTID(s.nftClassID, 1), + valid: true, + }, + "not authorized": { + contractID: s.contractID, + operator: s.vendor, + subjectID: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1), + targetID: collection.NewNFTID(s.nftClassID, 1), + }, + "not owner of the token": { + contractID: s.contractID, + operator: s.operator, + subjectID: collection.NewNFTID(s.nftClassID, s.numNFTs+1), + targetID: collection.NewNFTID(s.nftClassID, 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgOperatorAttach{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Owner: s.customer.String(), + Subject: tc.subjectID, + Target: tc.targetID, + } + res, err := s.msgServer.OperatorAttach(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgOperatorDetach() { + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + subjectID string + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.operator, + subjectID: collection.NewNFTID(s.nftClassID, 2), + valid: true, + }, + "not authorized": { + contractID: s.contractID, + operator: s.vendor, + subjectID: collection.NewNFTID(s.nftClassID, 2), + }, + "not owner of the token": { + contractID: s.contractID, + operator: s.operator, + subjectID: collection.NewNFTID(s.nftClassID, s.numNFTs+2), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgOperatorDetach{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Owner: s.customer.String(), + Subject: tc.subjectID, + } + res, err := s.msgServer.OperatorDetach(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgAttachFrom() { + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + subjectID string + targetID string + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.operator, + subjectID: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1), + targetID: collection.NewNFTID(s.nftClassID, 1), + valid: true, + }, + "not authorized": { + contractID: s.contractID, + operator: s.vendor, + subjectID: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1), + targetID: collection.NewNFTID(s.nftClassID, 1), + }, + "not owner of the token": { + contractID: s.contractID, + operator: s.operator, + subjectID: collection.NewNFTID(s.nftClassID, s.numNFTs+1), + targetID: collection.NewNFTID(s.nftClassID, 1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgAttachFrom{ + ContractId: tc.contractID, + Proxy: tc.operator.String(), + From: s.customer.String(), + TokenId: tc.subjectID, + ToTokenId: tc.targetID, + } + res, err := s.msgServer.AttachFrom(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} + +func (s *KeeperTestSuite) TestMsgDetachFrom() { + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + subjectID string + valid bool + }{ + "valid request": { + contractID: s.contractID, + operator: s.operator, + subjectID: collection.NewNFTID(s.nftClassID, 2), + valid: true, + }, + "not authorized": { + contractID: s.contractID, + operator: s.vendor, + subjectID: collection.NewNFTID(s.nftClassID, 2), + }, + "not owner of the token": { + contractID: s.contractID, + operator: s.operator, + subjectID: collection.NewNFTID(s.nftClassID, s.numNFTs+2), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + req := &collection.MsgDetachFrom{ + ContractId: tc.contractID, + Proxy: tc.operator.String(), + From: s.customer.String(), + TokenId: tc.subjectID, + } + res, err := s.msgServer.DetachFrom(sdk.WrapSDKContext(ctx), req) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(res) + }) + } +} diff --git a/x/collection/keeper/nft.go b/x/collection/keeper/nft.go new file mode 100644 index 0000000000..c3e6d3d612 --- /dev/null +++ b/x/collection/keeper/nft.go @@ -0,0 +1,309 @@ +package keeper + +import ( + gogotypes "github.com/gogo/protobuf/types" + + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" + "github.com/line/lbm-sdk/x/collection" +) + +func (k Keeper) hasNFT(ctx sdk.Context, contractID string, tokenID string) error { + store := ctx.KVStore(k.storeKey) + key := nftKey(contractID, tokenID) + if !store.Has(key) { + return sdkerrors.ErrNotFound.Wrapf("nft not exists: %s", tokenID) + } + return nil +} + +func (k Keeper) GetNFT(ctx sdk.Context, contractID string, tokenID string) (*collection.NFT, error) { + store := ctx.KVStore(k.storeKey) + key := nftKey(contractID, tokenID) + bz := store.Get(key) + if bz == nil { + return nil, sdkerrors.ErrNotFound.Wrapf("nft not exists: %s", tokenID) + } + + var token collection.NFT + k.cdc.MustUnmarshal(bz, &token) + + return &token, nil +} + +func (k Keeper) setNFT(ctx sdk.Context, contractID string, token collection.NFT) { + store := ctx.KVStore(k.storeKey) + key := nftKey(contractID, token.Id) + + bz, err := token.Marshal() + if err != nil { + panic(err) + } + store.Set(key, bz) +} + +func (k Keeper) deleteNFT(ctx sdk.Context, contractID string, tokenID string) { + store := ctx.KVStore(k.storeKey) + key := nftKey(contractID, tokenID) + store.Delete(key) +} + +func (k Keeper) pruneNFT(ctx sdk.Context, contractID string, tokenID string) []string { + burnt := []string{} + for _, child := range k.GetChildren(ctx, contractID, tokenID) { + k.deleteChild(ctx, contractID, tokenID, child) + k.deleteParent(ctx, contractID, child) + k.deleteNFT(ctx, contractID, child) + burnt = append(burnt, child) + + pruned := k.pruneNFT(ctx, contractID, child) + burnt = append(burnt, pruned...) + } + return burnt +} + +func (k Keeper) Attach(ctx sdk.Context, contractID string, owner sdk.AccAddress, subject, target string) error { + // validate subject + if !k.GetBalance(ctx, contractID, owner, subject).IsPositive() { + return sdkerrors.ErrInvalidRequest.Wrapf("%s is not owner of %s", owner, subject) + } + + // validate target + if err := k.hasNFT(ctx, contractID, target); err != nil { + return err + } + + root := k.GetRoot(ctx, contractID, target) + if owner != k.getOwner(ctx, contractID, root) { + return sdkerrors.ErrInvalidRequest.Wrapf("%s is not owner of %s", owner, target) + } + if root == subject { + return sdkerrors.ErrInvalidRequest.Wrap("cycles not allowed") + } + + // update subject + k.deleteOwner(ctx, contractID, subject) + k.setParent(ctx, contractID, subject, target) + + // update target + k.setChild(ctx, contractID, target, subject) + + // finally, check the invariant + if err := k.validateDepthAndWidth(ctx, contractID, root); err != nil { + return err + } + + // legacy + k.emitEventOnDescendants(ctx, contractID, subject, collection.NewEventOperationRootChanged) + + return nil +} + +func (k Keeper) Detach(ctx sdk.Context, contractID string, owner sdk.AccAddress, subject string) error { + if err := k.hasNFT(ctx, contractID, subject); err != nil { + return err + } + + parent, err := k.GetParent(ctx, contractID, subject) + if err != nil { + return err + } + + if owner != k.GetRootOwner(ctx, contractID, subject) { + return sdkerrors.ErrInvalidRequest.Wrapf("%s is not owner of %s", owner, subject) + } + + // update subject + k.deleteParent(ctx, contractID, subject) + k.setOwner(ctx, contractID, subject, owner) + + // update parent + k.deleteChild(ctx, contractID, *parent, subject) + + // legacy + k.emitEventOnDescendants(ctx, contractID, subject, collection.NewEventOperationRootChanged) + + return nil +} + +func (k Keeper) iterateAncestors(ctx sdk.Context, contractID string, tokenID string, fn func(tokenID string) error) error { + var err error + for id := &tokenID; err == nil; id, err = k.GetParent(ctx, contractID, *id) { + if fnErr := fn(*id); fnErr != nil { + return fnErr + } + } + + return nil +} + +func (k Keeper) GetRootOwner(ctx sdk.Context, contractID string, tokenID string) sdk.AccAddress { + rootID := k.GetRoot(ctx, contractID, tokenID) + return k.getOwner(ctx, contractID, rootID) +} + +func (k Keeper) getOwner(ctx sdk.Context, contractID string, tokenID string) sdk.AccAddress { + store := ctx.KVStore(k.storeKey) + key := ownerKey(contractID, tokenID) + bz := store.Get(key) + if bz == nil { + panic("owner must exist") + } + + var owner sdk.AccAddress + if err := owner.Unmarshal(bz); err != nil { + panic(err) + } + return owner +} + +func (k Keeper) setOwner(ctx sdk.Context, contractID string, tokenID string, owner sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + key := ownerKey(contractID, tokenID) + + bz, err := owner.Marshal() + if err != nil { + panic(err) + } + store.Set(key, bz) +} + +func (k Keeper) deleteOwner(ctx sdk.Context, contractID string, tokenID string) { + store := ctx.KVStore(k.storeKey) + key := ownerKey(contractID, tokenID) + store.Delete(key) +} + +func (k Keeper) GetParent(ctx sdk.Context, contractID string, tokenID string) (*string, error) { + store := ctx.KVStore(k.storeKey) + key := parentKey(contractID, tokenID) + bz := store.Get(key) + if bz == nil { + return nil, sdkerrors.ErrNotFound.Wrapf("%s has no parent", tokenID) + } + + var parent gogotypes.StringValue + k.cdc.MustUnmarshal(bz, &parent) + return &parent.Value, nil +} + +func (k Keeper) setParent(ctx sdk.Context, contractID string, tokenID, parentID string) { + store := ctx.KVStore(k.storeKey) + key := parentKey(contractID, tokenID) + + val := &gogotypes.StringValue{Value: parentID} + bz := k.cdc.MustMarshal(val) + store.Set(key, bz) +} + +func (k Keeper) deleteParent(ctx sdk.Context, contractID string, tokenID string) { + store := ctx.KVStore(k.storeKey) + key := parentKey(contractID, tokenID) + store.Delete(key) +} + +func (k Keeper) GetChildren(ctx sdk.Context, contractID string, tokenID string) []string { + var children []string + k.iterateChildren(ctx, contractID, tokenID, func(childID string) (stop bool) { + children = append(children, childID) + return false + }) + return children +} + +func (k Keeper) iterateChildren(ctx sdk.Context, contractID string, tokenID string, fn func(childID string) (stop bool)) { + k.iterateChildrenImpl(ctx, childKeyPrefixByTokenID(contractID, tokenID), func(_ string, _ string, childID string) (stop bool) { + return fn(childID) + }) +} + +func (k Keeper) iterateDescendants(ctx sdk.Context, contractID string, tokenID string, fn func(descendantID string, depth int) (stop bool)) { + k.iterateDescendantsImpl(ctx, contractID, tokenID, 1, fn) +} + +func (k Keeper) iterateDescendantsImpl(ctx sdk.Context, contractID string, tokenID string, depth int, fn func(descendantID string, depth int) (stop bool)) { + k.iterateChildren(ctx, contractID, tokenID, func(childID string) (stop bool) { + if stop := fn(childID, depth); stop { + return true + } + + k.iterateDescendantsImpl(ctx, contractID, childID, depth+1, fn) + return false + }) +} + +func (k Keeper) setChild(ctx sdk.Context, contractID string, tokenID, childID string) { + store := ctx.KVStore(k.storeKey) + key := childKey(contractID, tokenID, childID) + store.Set(key, []byte{}) +} + +func (k Keeper) deleteChild(ctx sdk.Context, contractID string, tokenID, childID string) { + store := ctx.KVStore(k.storeKey) + key := childKey(contractID, tokenID, childID) + store.Delete(key) +} + +func (k Keeper) GetRoot(ctx sdk.Context, contractID string, tokenID string) string { + id := tokenID + k.iterateAncestors(ctx, contractID, tokenID, func(tokenID string) error { + id = tokenID + return nil + }) + + return id +} + +// Deprecated +func (k Keeper) setLegacyToken(ctx sdk.Context, contractID string, tokenID string) { + store := ctx.KVStore(k.storeKey) + key := legacyTokenKey(contractID, tokenID) + store.Set(key, []byte{}) +} + +// Deprecated +func (k Keeper) deleteLegacyToken(ctx sdk.Context, contractID string, tokenID string) { + store := ctx.KVStore(k.storeKey) + key := legacyTokenKey(contractID, tokenID) + store.Delete(key) +} + +// Deprecated +func (k Keeper) setLegacyTokenType(ctx sdk.Context, contractID string, tokenType string) { + store := ctx.KVStore(k.storeKey) + key := legacyTokenTypeKey(contractID, tokenType) + store.Set(key, []byte{}) +} + +// Deprecated +func (k Keeper) emitEventOnDescendants(ctx sdk.Context, contractID string, tokenID string, generator func(contractID string, descendantID string) sdk.Event) { + k.iterateDescendants(ctx, contractID, tokenID, func(descendantID string, _ int) (stop bool) { + event := generator(contractID, descendantID) + ctx.EventManager().EmitEvent(event) + return false + }) +} + +// Deprecated +func (k Keeper) validateDepthAndWidth(ctx sdk.Context, contractID string, tokenID string) error { + widths := map[int]int{0: 1} + k.iterateDescendants(ctx, contractID, tokenID, func(descendantID string, depth int) (stop bool) { + widths[depth]++ + return false + }) + + params := k.GetParams(ctx) + + depth := len(widths) + if legacyDepth := depth - 1; legacyDepth > int(params.DepthLimit) { + return sdkerrors.ErrInvalidRequest.Wrapf("resulting depth exceeds its limit: %d", params.DepthLimit) + } + + for _, width := range widths { + if width > int(params.WidthLimit) { + return sdkerrors.ErrInvalidRequest.Wrapf("resulting width exceeds its limit: %d", params.WidthLimit) + } + } + + return nil +} diff --git a/x/collection/keeper/nft_test.go b/x/collection/keeper/nft_test.go new file mode 100644 index 0000000000..2d7a5672f7 --- /dev/null +++ b/x/collection/keeper/nft_test.go @@ -0,0 +1,101 @@ +package keeper_test + +import ( + "github.com/line/lbm-sdk/x/collection" +) + +func (s *KeeperTestSuite) TestAttach() { + testCases := map[string]struct { + contractID string + subject string + target string + valid bool + }{ + "valid request": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1), + target: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit), + valid: true, + }, + "not owner of subject": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, s.numNFTs+1), + target: collection.NewNFTID(s.nftClassID, 1), + }, + "target not found": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1), + target: collection.NewNFTID(s.nftClassID, s.numNFTs*3+1), + }, + "result exceeds the limit": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+2), + target: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit), + }, + "not owner of target": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, collection.DefaultDepthLimit+1), + target: collection.NewNFTID(s.nftClassID, s.numNFTs+1), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + err := s.keeper.Attach(ctx, tc.contractID, s.customer, tc.subject, tc.target) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + parent, err := s.keeper.GetParent(ctx, tc.contractID, tc.subject) + s.Require().NoError(err) + s.Require().Equal(*parent, tc.target) + }) + } +} + +func (s *KeeperTestSuite) TestDetach() { + testCases := map[string]struct { + contractID string + subject string + valid bool + }{ + "valid request": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, 2), + valid: true, + }, + "subject not found": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, s.numNFTs*3+1), + }, + "subject has no parent": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, 1), + }, + "not owner of subject": { + contractID: s.contractID, + subject: collection.NewNFTID(s.nftClassID, s.numNFTs+2), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + err := s.keeper.Detach(ctx, tc.contractID, s.customer, tc.subject) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + parent, err := s.keeper.GetParent(ctx, tc.contractID, tc.subject) + s.Require().Error(err) + s.Require().Nil(parent) + }) + } +} diff --git a/x/collection/keeper/param.go b/x/collection/keeper/param.go new file mode 100644 index 0000000000..fb81060df6 --- /dev/null +++ b/x/collection/keeper/param.go @@ -0,0 +1,32 @@ +package keeper + +import ( + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" + "github.com/line/lbm-sdk/x/collection" +) + +func (k Keeper) GetParams(ctx sdk.Context) collection.Params { + store := ctx.KVStore(k.storeKey) + key := paramsKey + bz := store.Get(key) + if bz == nil { + panic(sdkerrors.ErrNotFound.Wrap("params does not exist")) + } + + var params collection.Params + k.cdc.MustUnmarshal(bz, ¶ms) + + return params +} + +func (k Keeper) setParams(ctx sdk.Context, params collection.Params) { + store := ctx.KVStore(k.storeKey) + key := paramsKey + + bz, err := params.Marshal() + if err != nil { + panic(err) + } + store.Set(key, bz) +} diff --git a/x/collection/keeper/send.go b/x/collection/keeper/send.go new file mode 100644 index 0000000000..fe7c358212 --- /dev/null +++ b/x/collection/keeper/send.go @@ -0,0 +1,132 @@ +package keeper + +import ( + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" + "github.com/line/lbm-sdk/x/collection" +) + +func (k Keeper) SendCoins(ctx sdk.Context, contractID string, from, to sdk.AccAddress, amount []collection.Coin) error { + if err := k.subtractCoins(ctx, contractID, from, amount); err != nil { + return err + } + if err := k.addCoins(ctx, contractID, to, amount); err != nil { + return err + } + + return nil +} + +func (k Keeper) addCoins(ctx sdk.Context, contractID string, address sdk.AccAddress, amount []collection.Coin) error { + for _, coin := range amount { + balance := k.GetBalance(ctx, contractID, address, coin.TokenId) + newBalance := balance.Add(coin.Amount) + k.setBalance(ctx, contractID, address, coin.TokenId, newBalance) + + if err := collection.ValidateNFTID(coin.TokenId); err == nil { + k.setOwner(ctx, contractID, coin.TokenId, address) + + // legacy + k.emitEventOnDescendants(ctx, contractID, coin.TokenId, collection.NewEventOperationTransferNFT) + } + } + + // create account if recipient does not exist. + k.createAccountOnAbsence(ctx, address) + + return nil +} + +func (k Keeper) subtractCoins(ctx sdk.Context, contractID string, address sdk.AccAddress, amount []collection.Coin) error { + for _, coin := range amount { + balance := k.GetBalance(ctx, contractID, address, coin.TokenId) + newBalance := balance.Sub(coin.Amount) + if newBalance.IsNegative() { + return sdkerrors.ErrInsufficientFunds.Wrapf("%s is smaller than %s", balance, coin.Amount) + } + k.setBalance(ctx, contractID, address, coin.TokenId, newBalance) + } + + return nil +} + +func (k Keeper) GetBalance(ctx sdk.Context, contractID string, address sdk.AccAddress, tokenID string) sdk.Int { + store := ctx.KVStore(k.storeKey) + key := balanceKey(contractID, address, tokenID) + bz := store.Get(key) + if bz == nil { + return sdk.ZeroInt() + } + + var balance sdk.Int + if err := balance.Unmarshal(bz); err != nil { + panic(err) + } + return balance +} + +func (k Keeper) setBalance(ctx sdk.Context, contractID string, address sdk.AccAddress, tokenID string, balance sdk.Int) { + store := ctx.KVStore(k.storeKey) + key := balanceKey(contractID, address, tokenID) + + if balance.IsZero() { + store.Delete(key) + } else { + bz, err := balance.Marshal() + if err != nil { + panic(err) + } + store.Set(key, bz) + } +} + +func (k Keeper) AuthorizeOperator(ctx sdk.Context, contractID string, holder, operator sdk.AccAddress) error { + if _, err := k.GetContract(ctx, contractID); err != nil { + return sdkerrors.ErrNotFound.Wrapf("contract does not exist: %s", contractID) + } + if _, err := k.GetAuthorization(ctx, contractID, holder, operator); err == nil { + return sdkerrors.ErrInvalidRequest.Wrap("Already authorized") + } + + k.setAuthorization(ctx, contractID, holder, operator) + + // create account if operator does not exist. + k.createAccountOnAbsence(ctx, operator) + + return nil +} + +func (k Keeper) RevokeOperator(ctx sdk.Context, contractID string, holder, operator sdk.AccAddress) error { + if _, err := k.GetContract(ctx, contractID); err != nil { + return sdkerrors.ErrNotFound.Wrapf("contract does not exist: %s", contractID) + } + if _, err := k.GetAuthorization(ctx, contractID, holder, operator); err != nil { + return err + } + + k.deleteAuthorization(ctx, contractID, holder, operator) + return nil +} + +func (k Keeper) GetAuthorization(ctx sdk.Context, contractID string, holder, operator sdk.AccAddress) (*collection.Authorization, error) { + store := ctx.KVStore(k.storeKey) + if store.Has(authorizationKey(contractID, operator, holder)) { + return &collection.Authorization{ + Holder: holder.String(), + Operator: operator.String(), + }, nil + } + return nil, sdkerrors.ErrNotFound.Wrapf("no authorization by %s to %s", holder, operator) +} + +func (k Keeper) setAuthorization(ctx sdk.Context, contractID string, holder, operator sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + key := authorizationKey(contractID, operator, holder) + store.Set(key, []byte{}) +} + +func (k Keeper) deleteAuthorization(ctx sdk.Context, contractID string, holder, operator sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + key := authorizationKey(contractID, operator, holder) + store.Delete(key) +} diff --git a/x/collection/keeper/send_test.go b/x/collection/keeper/send_test.go new file mode 100644 index 0000000000..18b97255db --- /dev/null +++ b/x/collection/keeper/send_test.go @@ -0,0 +1,125 @@ +package keeper_test + +import ( + "fmt" + + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +func (s *KeeperTestSuite) TestSendCoins() { + testCases := map[string]struct { + amount collection.Coin + valid bool + }{ + "valid send (fungible token)": { + amount: collection.NewFTCoin(s.ftClassID, s.balance), + valid: true, + }, + "valid send (non-fungible token)": { + amount: collection.NewNFTCoin(s.nftClassID, 1), + valid: true, + }, + "insufficient tokens": { + amount: collection.NewFTCoin(s.ftClassID, s.balance.Add(sdk.OneInt())), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + tokenID := tc.amount.TokenId + customerBalance := s.keeper.GetBalance(ctx, s.contractID, s.customer, tokenID) + operatorBalance := s.keeper.GetBalance(ctx, s.contractID, s.operator, tokenID) + + err := s.keeper.SendCoins(ctx, s.contractID, s.customer, s.operator, collection.NewCoins(tc.amount)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + + newCustomerBalance := s.keeper.GetBalance(ctx, s.contractID, s.customer, tokenID) + newOperatorBalance := s.keeper.GetBalance(ctx, s.contractID, s.operator, tokenID) + s.Require().True(customerBalance.Sub(tc.amount.Amount).Equal(newCustomerBalance)) + s.Require().True(operatorBalance.Add(tc.amount.Amount).Equal(newOperatorBalance)) + }) + } +} + +func (s *KeeperTestSuite) TestAuthorizeOperator() { + // make sure the dummy contract does not exist + dummyContractID := "deadbeef" + _, err := s.keeper.GetContract(s.ctx, dummyContractID) + s.Require().Error(err) + + contractDescriptions := map[string]string{ + s.contractID: "valid", + dummyContractID: "not-exists", + } + userDescriptions := map[sdk.AccAddress]string{ + s.vendor: "vendor", + s.operator: "operator", + s.customer: "customer", + } + for id, idDesc := range contractDescriptions { + for operator, operatorDesc := range userDescriptions { + for from, fromDesc := range userDescriptions { + name := fmt.Sprintf("ContractID: %s, Operator: %s, From: %s", idDesc, operatorDesc, fromDesc) + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + _, idErr := s.keeper.GetContract(ctx, id) + _, authErr := s.keeper.GetAuthorization(ctx, id, from, operator) + err := s.keeper.AuthorizeOperator(ctx, id, from, operator) + if idErr == nil && authErr != nil { + s.Require().NoError(err) + _, authErr = s.keeper.GetAuthorization(ctx, id, from, operator) + s.Require().NoError(authErr) + } else { + s.Require().Error(err) + } + }) + } + } + } +} + +func (s *KeeperTestSuite) TestRevokeOperator() { + // make sure the dummy contract does not exist + dummyContractID := "deadbeef" + _, err := s.keeper.GetContract(s.ctx, dummyContractID) + s.Require().Error(err) + + contractDescriptions := map[string]string{ + s.contractID: "valid", + dummyContractID: "not-exists", + } + userDescriptions := map[sdk.AccAddress]string{ + s.vendor: "vendor", + s.operator: "operator", + s.customer: "customer", + } + for id, idDesc := range contractDescriptions { + for operator, operatorDesc := range userDescriptions { + for from, fromDesc := range userDescriptions { + name := fmt.Sprintf("ContractID: %s, Operator: %s, From: %s", idDesc, operatorDesc, fromDesc) + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + _, idErr := s.keeper.GetContract(ctx, id) + _, authErr := s.keeper.GetAuthorization(ctx, id, from, operator) + err := s.keeper.RevokeOperator(ctx, id, from, operator) + if idErr == nil && authErr == nil { + s.Require().NoError(err) + _, authErr = s.keeper.GetAuthorization(ctx, id, from, operator) + s.Require().Error(authErr) + } else { + s.Require().Error(err) + } + }) + } + } + } +} diff --git a/x/collection/keeper/supply.go b/x/collection/keeper/supply.go new file mode 100644 index 0000000000..80880c91c3 --- /dev/null +++ b/x/collection/keeper/supply.go @@ -0,0 +1,514 @@ +package keeper + +import ( + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" + "github.com/line/lbm-sdk/x/collection" +) + +func (k Keeper) CreateContract(ctx sdk.Context, creator sdk.AccAddress, contract collection.Contract) string { + contractID := k.createContract(ctx, contract) + + event := collection.EventCreatedContract{ + ContractId: contractID, + Name: contract.Name, + Meta: contract.Meta, + BaseImgUri: contract.BaseImgUri, + } + ctx.EventManager().EmitEvent(collection.NewEventCreateCollection(event, creator)) + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + + eventGrant := collection.EventGrant{ + ContractId: contractID, + Grantee: creator.String(), + } + ctx.EventManager().EmitEvent(collection.NewEventGrantPermTokenHead(eventGrant)) + for _, permission := range collection.Permission_value { + p := collection.Permission(permission) + if p == collection.PermissionUnspecified { + continue + } + + eventGrant.Permission = p + ctx.EventManager().EmitEvent(collection.NewEventGrantPermTokenBody(eventGrant)) + k.Grant(ctx, contractID, "", creator, collection.Permission(permission)) + } + + return contractID +} + +func (k Keeper) createContract(ctx sdk.Context, contract collection.Contract) string { + contractID := k.classKeeper.NewID(ctx) + contract.ContractId = contractID + k.setContract(ctx, contract) + + // set the next class ids + nextIDs := collection.DefaultNextClassIDs(contractID) + k.setNextClassIDs(ctx, nextIDs) + + return contractID +} + +func (k Keeper) GetContract(ctx sdk.Context, contractID string) (*collection.Contract, error) { + store := ctx.KVStore(k.storeKey) + key := contractKey(contractID) + bz := store.Get(key) + if bz == nil { + return nil, sdkerrors.ErrNotFound.Wrapf("no such a contract: %s", contractID) + } + + var contract collection.Contract + if err := contract.Unmarshal(bz); err != nil { + panic(err) + } + return &contract, nil +} + +func (k Keeper) setContract(ctx sdk.Context, contract collection.Contract) { + store := ctx.KVStore(k.storeKey) + key := contractKey(contract.ContractId) + + bz, err := contract.Marshal() + if err != nil { + panic(err) + } + store.Set(key, bz) +} + +func (k Keeper) CreateTokenClass(ctx sdk.Context, contractID string, class collection.TokenClass) (*string, error) { + if _, err := k.GetContract(ctx, contractID); err != nil { + return nil, err + } + + nextClassIDs := k.getNextClassIDs(ctx, contractID) + class.SetId(&nextClassIDs) + k.setNextClassIDs(ctx, nextClassIDs) + + if err := class.ValidateBasic(); err != nil { + return nil, err + } + k.setTokenClass(ctx, contractID, class) + + if nftClass, ok := class.(*collection.NFTClass); ok { + k.setNextTokenID(ctx, contractID, nftClass.Id, sdk.OneUint()) + + // legacy + k.setLegacyTokenType(ctx, contractID, nftClass.Id) + } + + if ftClass, ok := class.(*collection.FTClass); ok { + // legacy + k.setLegacyToken(ctx, contractID, collection.NewFTID(ftClass.Id)) + } + + id := class.GetId() + return &id, nil +} + +func (k Keeper) GetTokenClass(ctx sdk.Context, contractID, classID string) (collection.TokenClass, error) { + store := ctx.KVStore(k.storeKey) + key := classKey(contractID, classID) + bz := store.Get(key) + if bz == nil { + return nil, sdkerrors.ErrNotFound.Wrapf("no such a class in contract %s: %s", contractID, classID) + } + + var class collection.TokenClass + if err := k.cdc.UnmarshalInterface(bz, &class); err != nil { + panic(err) + } + return class, nil +} + +func (k Keeper) setTokenClass(ctx sdk.Context, contractID string, class collection.TokenClass) { + store := ctx.KVStore(k.storeKey) + key := classKey(contractID, class.GetId()) + + bz, err := k.cdc.MarshalInterface(class) + if err != nil { + panic(err) + } + store.Set(key, bz) +} + +func (k Keeper) getNextClassIDs(ctx sdk.Context, contractID string) collection.NextClassIDs { + store := ctx.KVStore(k.storeKey) + key := nextClassIDKey(contractID) + bz := store.Get(key) + if bz == nil { + panic(sdkerrors.ErrNotFound.Wrapf("no next class ids of contract %s", contractID)) + } + + var class collection.NextClassIDs + if err := class.Unmarshal(bz); err != nil { + panic(err) + } + return class +} + +func (k Keeper) setNextClassIDs(ctx sdk.Context, ids collection.NextClassIDs) { + store := ctx.KVStore(k.storeKey) + key := nextClassIDKey(ids.ContractId) + + bz, err := ids.Marshal() + if err != nil { + panic(err) + } + store.Set(key, bz) +} + +func (k Keeper) MintFT(ctx sdk.Context, contractID string, to sdk.AccAddress, amount []collection.Coin) error { + for _, coin := range amount { + if err := collection.ValidateFTID(coin.TokenId); err != nil { + return err + } + + classID := collection.SplitTokenID(coin.TokenId) + class, err := k.GetTokenClass(ctx, contractID, classID) + if err != nil { + return err + } + + ftClass, ok := class.(*collection.FTClass) + if !ok { + return sdkerrors.ErrInvalidType.Wrapf("not a class of fungible token: %s", classID) + } + + if !ftClass.Mintable { + return sdkerrors.ErrInvalidRequest.Wrapf("class is not mintable") + } + + k.mintFT(ctx, contractID, to, classID, coin.Amount) + } + + return nil +} + +func (k Keeper) mintFT(ctx sdk.Context, contractID string, to sdk.AccAddress, classID string, amount sdk.Int) { + tokenID := collection.NewFTID(classID) + k.setBalance(ctx, contractID, to, tokenID, amount) + + // update statistics + supply := k.GetSupply(ctx, contractID, classID) + k.setSupply(ctx, contractID, classID, supply.Add(amount)) + + minted := k.GetMinted(ctx, contractID, classID) + k.setMinted(ctx, contractID, classID, minted.Add(amount)) +} + +func (k Keeper) MintNFT(ctx sdk.Context, contractID string, to sdk.AccAddress, params []collection.MintNFTParam) ([]collection.NFT, error) { + tokens := make([]collection.NFT, 0, len(params)) + for _, param := range params { + classID := param.TokenType + class, err := k.GetTokenClass(ctx, contractID, classID) + if err != nil { + return nil, err + } + + if _, ok := class.(*collection.NFTClass); !ok { + return nil, sdkerrors.ErrInvalidType.Wrapf("not a class of non-fungible token: %s", classID) + } + + nextTokenID := k.getNextTokenID(ctx, contractID, classID) + k.setNextTokenID(ctx, contractID, classID, nextTokenID.Incr()) + tokenID := collection.NewNFTID(classID, int(nextTokenID.Uint64())) + + amount := sdk.OneInt() + + k.setBalance(ctx, contractID, to, tokenID, amount) + k.setOwner(ctx, contractID, tokenID, to) + + token := collection.NFT{ + Id: tokenID, + Name: param.Name, + Meta: param.Meta, + } + k.setNFT(ctx, contractID, token) + + // update statistics + supply := k.GetSupply(ctx, contractID, classID) + k.setSupply(ctx, contractID, classID, supply.Add(amount)) + + minted := k.GetMinted(ctx, contractID, classID) + k.setMinted(ctx, contractID, classID, minted.Add(amount)) + + tokens = append(tokens, token) + + // legacy + k.setLegacyToken(ctx, contractID, tokenID) + } + + return tokens, nil +} + +func (k Keeper) BurnCoins(ctx sdk.Context, contractID string, from sdk.AccAddress, amount []collection.Coin) ([]collection.Coin, error) { + if err := k.subtractCoins(ctx, contractID, from, amount); err != nil { + return nil, err + } + + burntAmount := []collection.Coin{} + for _, coin := range amount { + burntAmount = append(burntAmount, coin) + if err := collection.ValidateNFTID(coin.TokenId); err == nil { + // legacy + k.emitEventOnDescendants(ctx, contractID, coin.TokenId, collection.NewEventOperationBurnNFT) + + k.deleteOwner(ctx, contractID, coin.TokenId) + k.deleteNFT(ctx, contractID, coin.TokenId) + pruned := k.pruneNFT(ctx, contractID, coin.TokenId) + + for _, id := range pruned { + burntAmount = append(burntAmount, collection.NewCoin(id, sdk.OneInt())) + } + + // legacy + k.deleteLegacyToken(ctx, contractID, coin.TokenId) + } + } + + // update statistics + for _, coin := range burntAmount { + classID := collection.SplitTokenID(coin.TokenId) + supply := k.GetSupply(ctx, contractID, classID) + k.setSupply(ctx, contractID, classID, supply.Sub(coin.Amount)) + + burnt := k.GetBurnt(ctx, contractID, classID) + k.setBurnt(ctx, contractID, classID, burnt.Add(coin.Amount)) + } + + return burntAmount, nil +} + +func (k Keeper) getNextTokenID(ctx sdk.Context, contractID string, classID string) sdk.Uint { + store := ctx.KVStore(k.storeKey) + key := nextTokenIDKey(contractID, classID) + bz := store.Get(key) + if bz == nil { + panic(sdkerrors.ErrNotFound.Wrapf("no next token id of token class %s", classID)) + } + + var id sdk.Uint + if err := id.Unmarshal(bz); err != nil { + panic(err) + } + return id +} + +func (k Keeper) setNextTokenID(ctx sdk.Context, contractID string, classID string, tokenID sdk.Uint) { + store := ctx.KVStore(k.storeKey) + key := nextTokenIDKey(contractID, classID) + + bz, err := tokenID.Marshal() + if err != nil { + panic(err) + } + store.Set(key, bz) +} + +func (k Keeper) ModifyContract(ctx sdk.Context, contractID string, operator sdk.AccAddress, changes []collection.Attribute) error { + contract, err := k.GetContract(ctx, contractID) + if err != nil { + return err + } + + modifiers := map[string]func(string){ + collection.AttributeKeyName.String(): func(name string) { + contract.Name = name + }, + collection.AttributeKeyBaseImgURI.String(): func(uri string) { + contract.Name = uri + }, + collection.AttributeKeyMeta.String(): func(meta string) { + contract.Meta = meta + }, + } + for _, change := range changes { + modifiers[change.Key](change.Value) + } + + k.setContract(ctx, *contract) + + event := collection.EventModifiedContract{ + ContractId: contractID, + Operator: operator.String(), + Changes: changes, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + return nil +} + +func (k Keeper) ModifyTokenClass(ctx sdk.Context, contractID string, classID string, operator sdk.AccAddress, changes []collection.Attribute) error { + class, err := k.GetTokenClass(ctx, contractID, classID) + if err != nil { + return err + } + + modifiers := map[string]func(string){ + collection.AttributeKeyName.String(): func(name string) { + class.SetName(name) + }, + collection.AttributeKeyMeta.String(): func(meta string) { + class.SetMeta(meta) + }, + } + for _, change := range changes { + modifiers[change.Key](change.Value) + } + + k.setTokenClass(ctx, contractID, class) + + event := collection.EventModifiedTokenClass{ + ContractId: contractID, + ClassId: class.GetId(), + Operator: operator.String(), + Changes: changes, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + return nil +} + +func (k Keeper) ModifyNFT(ctx sdk.Context, contractID string, tokenID string, operator sdk.AccAddress, changes []collection.Attribute) error { + token, err := k.GetNFT(ctx, contractID, tokenID) + if err != nil { + return err + } + + modifiers := map[string]func(string){ + collection.AttributeKeyName.String(): func(name string) { + token.Name = name + }, + collection.AttributeKeyMeta.String(): func(meta string) { + token.Meta = meta + }, + } + for _, change := range changes { + modifiers[change.Key](change.Value) + } + + k.setNFT(ctx, contractID, *token) + + event := collection.EventModifiedNFT{ + ContractId: contractID, + TokenId: tokenID, + Operator: operator.String(), + Changes: changes, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } + return nil +} + +func (k Keeper) Grant(ctx sdk.Context, contractID string, granter, grantee sdk.AccAddress, permission collection.Permission) { + k.grant(ctx, contractID, grantee, permission) + + event := collection.EventGrant{ + ContractId: contractID, + Granter: granter.String(), + Grantee: grantee.String(), + Permission: permission, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } +} + +func (k Keeper) grant(ctx sdk.Context, contractID string, grantee sdk.AccAddress, permission collection.Permission) { + k.setGrant(ctx, contractID, grantee, permission) + + // create account if grantee does not exist. + k.createAccountOnAbsence(ctx, grantee) +} + +func (k Keeper) Abandon(ctx sdk.Context, contractID string, grantee sdk.AccAddress, permission collection.Permission) { + k.deleteGrant(ctx, contractID, grantee, permission) + + event := collection.EventAbandon{ + ContractId: contractID, + Grantee: grantee.String(), + Permission: permission, + } + if err := ctx.EventManager().EmitTypedEvent(&event); err != nil { + panic(err) + } +} + +func (k Keeper) GetGrant(ctx sdk.Context, contractID string, grantee sdk.AccAddress, permission collection.Permission) (*collection.Grant, error) { + store := ctx.KVStore(k.storeKey) + if store.Has(grantKey(contractID, grantee, permission)) { + return &collection.Grant{ + Grantee: grantee.String(), + Permission: permission, + }, nil + } + return nil, sdkerrors.ErrNotFound.Wrapf("no %s permission granted on %s", permission, grantee) +} + +func (k Keeper) setGrant(ctx sdk.Context, contractID string, grantee sdk.AccAddress, permission collection.Permission) { + store := ctx.KVStore(k.storeKey) + key := grantKey(contractID, grantee, permission) + store.Set(key, []byte{}) +} + +func (k Keeper) deleteGrant(ctx sdk.Context, contractID string, grantee sdk.AccAddress, permission collection.Permission) { + store := ctx.KVStore(k.storeKey) + key := grantKey(contractID, grantee, permission) + store.Delete(key) +} + +func (k Keeper) getStatistic(ctx sdk.Context, keyPrefix []byte, contractID string, classID string) sdk.Int { + store := ctx.KVStore(k.storeKey) + amount := sdk.ZeroInt() + bz := store.Get(statisticKey(keyPrefix, contractID, classID)) + if bz != nil { + if err := amount.Unmarshal(bz); err != nil { + panic(err) + } + } + + return amount +} + +func (k Keeper) setStatistic(ctx sdk.Context, keyPrefix []byte, contractID string, classID string, amount sdk.Int) { + store := ctx.KVStore(k.storeKey) + key := statisticKey(keyPrefix, contractID, classID) + if amount.IsZero() { + store.Delete(key) + } else { + bz, err := amount.Marshal() + if err != nil { + panic(err) + } + store.Set(key, bz) + } +} + +func (k Keeper) GetSupply(ctx sdk.Context, contractID string, classID string) sdk.Int { + return k.getStatistic(ctx, supplyKeyPrefix, contractID, classID) +} + +func (k Keeper) GetMinted(ctx sdk.Context, contractID string, classID string) sdk.Int { + return k.getStatistic(ctx, mintedKeyPrefix, contractID, classID) +} + +func (k Keeper) GetBurnt(ctx sdk.Context, contractID string, classID string) sdk.Int { + return k.getStatistic(ctx, burntKeyPrefix, contractID, classID) +} + +func (k Keeper) setSupply(ctx sdk.Context, contractID string, classID string, amount sdk.Int) { + k.setStatistic(ctx, supplyKeyPrefix, contractID, classID, amount) +} + +func (k Keeper) setMinted(ctx sdk.Context, contractID string, classID string, amount sdk.Int) { + k.setStatistic(ctx, mintedKeyPrefix, contractID, classID, amount) +} + +func (k Keeper) setBurnt(ctx sdk.Context, contractID string, classID string, amount sdk.Int) { + k.setStatistic(ctx, burntKeyPrefix, contractID, classID, amount) +} diff --git a/x/collection/keeper/supply_test.go b/x/collection/keeper/supply_test.go new file mode 100644 index 0000000000..6691ac9e1b --- /dev/null +++ b/x/collection/keeper/supply_test.go @@ -0,0 +1,272 @@ +package keeper_test + +import ( + "fmt" + + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +func (s *KeeperTestSuite) TestCreateContract() { + ctx, _ := s.ctx.CacheContext() + + input := collection.Contract{ + Name: "tibetian fox", + Meta: "Tibetian Fox", + BaseImgUri: "file:///tibetian_fox.png", + } + id := s.keeper.CreateContract(ctx, s.vendor, input) + s.Require().NotEmpty(id) + + output, err := s.keeper.GetContract(ctx, id) + s.Require().NoError(err) + s.Require().NotNil(output) + + s.Require().Equal(id, output.ContractId) + s.Require().Equal(input.Name, output.Name) + s.Require().Equal(input.Meta, output.Meta) + s.Require().Equal(input.BaseImgUri, output.BaseImgUri) +} + +func (s *KeeperTestSuite) TestCreateTokenClass() { + testCases := map[string]struct { + contractID string + class collection.TokenClass + valid bool + }{ + "valid fungible token class": { + contractID: s.contractID, + class: &collection.FTClass{}, + valid: true, + }, + "valid non-fungible token class": { + contractID: s.contractID, + class: &collection.NFTClass{}, + valid: true, + }, + "invalid contract id": { + class: &collection.FTClass{}, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + id, err := s.keeper.CreateTokenClass(ctx, tc.contractID, tc.class) + if !tc.valid { + s.Require().Error(err) + s.Require().Nil(id) + return + } + s.Require().NoError(err) + s.Require().NotNil(id) + + class, err := s.keeper.GetTokenClass(ctx, tc.contractID, *id) + s.Require().NoError(err) + s.Require().NoError(class.ValidateBasic()) + }) + } +} + +func (s *KeeperTestSuite) TestMintFT() { + testCases := map[string]struct { + contractID string + amount collection.Coin + valid bool + }{ + "valid request": { + contractID: s.contractID, + amount: collection.NewFTCoin(s.ftClassID, sdk.OneInt()), + valid: true, + }, + "invalid token id": { + contractID: s.contractID, + amount: collection.NewNFTCoin(s.ftClassID, 1), + }, + "class not found": { + contractID: s.contractID, + amount: collection.NewFTCoin("00bab10c", sdk.OneInt()), + }, + "not a class id of ft": { + contractID: s.contractID, + amount: collection.NewFTCoin(s.nftClassID, sdk.OneInt()), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + err := s.keeper.MintFT(ctx, tc.contractID, s.stranger, collection.NewCoins(tc.amount)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + }) + } +} + +func (s *KeeperTestSuite) TestMintNFT() { + testCases := map[string]struct { + contractID string + params []collection.MintNFTParam + valid bool + }{ + "valid request": { + contractID: s.contractID, + params: []collection.MintNFTParam{{TokenType: s.nftClassID}}, + valid: true, + }, + "class not found": { + contractID: s.contractID, + params: []collection.MintNFTParam{{TokenType: "deadbeef"}}, + }, + "not a class id of nft": { + contractID: s.contractID, + params: []collection.MintNFTParam{{TokenType: s.ftClassID}}, + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + _, err := s.keeper.MintNFT(ctx, tc.contractID, s.stranger, tc.params) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + }) + } +} + +func (s *KeeperTestSuite) TestBurnCoins() { + testCases := map[string]struct { + contractID string + amount collection.Coin + valid bool + }{ + "valid request": { + contractID: s.contractID, + amount: collection.NewFTCoin(s.ftClassID, sdk.OneInt()), + valid: true, + }, + "invalid token id": { + contractID: s.contractID, + amount: collection.NewNFTCoin(s.ftClassID, 1), + }, + "class not found": { + contractID: s.contractID, + amount: collection.NewFTCoin("00bab10c", sdk.OneInt()), + }, + "not a class id of ft": { + contractID: s.contractID, + amount: collection.NewFTCoin(s.nftClassID, sdk.OneInt()), + }, + } + + for name, tc := range testCases { + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + _, err := s.keeper.BurnCoins(ctx, tc.contractID, s.vendor, collection.NewCoins(tc.amount)) + if !tc.valid { + s.Require().Error(err) + return + } + s.Require().NoError(err) + }) + } +} + +func (s *KeeperTestSuite) TestModifyContract() { + contractDescriptions := map[string]string{ + s.contractID: "valid", + "deadbeef": "not-exist", + } + changes := []collection.Attribute{ + {Key: collection.AttributeKeyName.String(), Value: "fox"}, + {Key: collection.AttributeKeyBaseImgURI.String(), Value: "file:///fox.png"}, + {Key: collection.AttributeKeyMeta.String(), Value: "Fox"}, + } + + for contractID, contractDesc := range contractDescriptions { + name := fmt.Sprintf("Contract: %s", contractDesc) + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + err := s.keeper.ModifyContract(ctx, contractID, s.vendor, changes) + if contractID == s.contractID { + s.Require().NoError(err) + } else { + s.Require().Error(err) + } + }) + } +} + +func (s *KeeperTestSuite) TestModifyTokenClass() { + contractDescriptions := map[string]string{ + s.contractID: "valid", + "deadbeef": "not-exist", + } + classDescriptions := map[string]string{ + s.nftClassID: "valid", + "deadbeef": "not-exist", + } + changes := []collection.Attribute{ + {Key: collection.AttributeKeyName.String(), Value: "arctic fox"}, + {Key: collection.AttributeKeyMeta.String(), Value: "Arctic Fox"}, + } + + for contractID, contractDesc := range contractDescriptions { + for classID, classDesc := range classDescriptions { + name := fmt.Sprintf("Contract: %s, Class: %s", contractDesc, classDesc) + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + err := s.keeper.ModifyTokenClass(ctx, contractID, classID, s.vendor, changes) + if contractID == s.contractID && classID == s.nftClassID { + s.Require().NoError(err) + } else { + s.Require().Error(err) + } + }) + } + } +} + +func (s *KeeperTestSuite) TestModifyNFT() { + contractDescriptions := map[string]string{ + s.contractID: "valid", + "deadbeef": "not-exist", + } + validTokenID := collection.NewNFTID(s.nftClassID, 1) + tokenDescriptions := map[string]string{ + validTokenID: "valid", + collection.NewNFTID("deadbeef", 1): "not-exist", + } + changes := []collection.Attribute{ + {Key: collection.AttributeKeyName.String(), Value: "fennec fox 1"}, + {Key: collection.AttributeKeyMeta.String(), Value: "Fennec Fox 1"}, + } + + for contractID, contractDesc := range contractDescriptions { + for tokenID, tokenDesc := range tokenDescriptions { + name := fmt.Sprintf("Contract: %s, Token: %s", contractDesc, tokenDesc) + s.Run(name, func() { + ctx, _ := s.ctx.CacheContext() + + err := s.keeper.ModifyNFT(ctx, contractID, tokenID, s.vendor, changes) + if contractID == s.contractID && tokenID == validTokenID { + s.Require().NoError(err) + } else { + s.Require().Error(err) + } + }) + } + } +} diff --git a/x/collection/keys.go b/x/collection/keys.go new file mode 100644 index 0000000000..50fad8ae7d --- /dev/null +++ b/x/collection/keys.go @@ -0,0 +1,9 @@ +package collection + +const ( + // ModuleName is the module name constant used in many places + ModuleName = "collection" + + // StoreKey defines the primary module store key + StoreKey = ModuleName +) diff --git a/x/collection/module/module.go b/x/collection/module/module.go new file mode 100644 index 0000000000..01435f334b --- /dev/null +++ b/x/collection/module/module.go @@ -0,0 +1,150 @@ +package module + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + abci "github.com/line/ostracon/abci/types" + "github.com/spf13/cobra" + + "github.com/line/lbm-sdk/client" + "github.com/line/lbm-sdk/codec" + codectypes "github.com/line/lbm-sdk/codec/types" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/types/module" + "github.com/line/lbm-sdk/x/collection" + + "github.com/line/lbm-sdk/x/collection/client/cli" + "github.com/line/lbm-sdk/x/collection/keeper" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic defines the basic application module used by the collection module. +type AppModuleBasic struct{} + +// Name returns the ModuleName +func (AppModuleBasic) Name() string { + return collection.ModuleName +} + +// RegisterLegacyAminoCodec registers the collection types on the LegacyAmino codec +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {} + +// DefaultGenesis returns default genesis state as raw bytes for the collection +// module. +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(collection.DefaultGenesisState()) +} + +// ValidateGenesis performs genesis state validation for the collection module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { + var data collection.GenesisState + if err := cdc.UnmarshalJSON(bz, &data); err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", collection.ModuleName, err) + } + + return collection.ValidateGenesis(data) +} + +// RegisterRESTRoutes registers all REST query handlers +func (AppModuleBasic) RegisterRESTRoutes(clientCtx client.Context, r *mux.Router) {} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the collection module. +func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { + if err := collection.RegisterQueryHandlerClient(context.Background(), mux, collection.NewQueryClient(clientCtx)); err != nil { + panic(err) + } +} + +// GetQueryCmd returns the cli query commands for this module +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return cli.NewQueryCmd() +} + +// GetTxCmd returns the transaction commands for this module +func (AppModuleBasic) GetTxCmd() *cobra.Command { + return cli.NewTxCmd() +} + +func (b AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) { + collection.RegisterInterfaces(registry) +} + +//____________________________________________________________________________ + +// AppModule implements an application module for the collection module. +type AppModule struct { + AppModuleBasic + + keeper keeper.Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(cdc codec.Codec, keeper keeper.Keeper) AppModule { + return AppModule{ + keeper: keeper, + } +} + +// RegisterInvariants does nothing, there are no invariants to enforce +func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// Route returns the message routing key for the collection module. +func (am AppModule) Route() sdk.Route { return sdk.Route{} } + +// QuerierRoute returns the route we respond to for abci queries +func (AppModule) QuerierRoute() string { return "" } + +// LegacyQuerierHandler registers a query handler to respond to the module-specific queries +func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier { + return nil +} + +// RegisterServices registers a GRPC query service to respond to the +// module-specific GRPC queries. +func (am AppModule) RegisterServices(cfg module.Configurator) { + collection.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServer(am.keeper)) + collection.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServer(am.keeper)) + + // m := keeper.NewMigrator(am.keeper) + // migrations := map[uint64]func(sdk.Context) error{} + // for ver, handler := range migrations { + // if err := cfg.RegisterMigration(collection.ModuleName, ver, handler); err != nil { + // panic(fmt.Sprintf("failed to migrate x/%s from version %d to %d: %v", collection.ModuleName, ver, ver+1, err)) + // } + // } +} + +// InitGenesis performs genesis initialization for the collection module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState collection.GenesisState + cdc.MustUnmarshalJSON(data, &genesisState) + am.keeper.InitGenesis(ctx, &genesisState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the collection +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + gs := am.keeper.ExportGenesis(ctx) + return cdc.MustMarshalJSON(gs) +} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return 1 } + +// BeginBlock performs a no-op. +func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// EndBlock performs a no-op. +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/collection/msgs.go b/x/collection/msgs.go new file mode 100644 index 0000000000..920e60c0fa --- /dev/null +++ b/x/collection/msgs.go @@ -0,0 +1,1361 @@ +package collection + +import ( + "fmt" + "regexp" + "unicode/utf8" + + sdk "github.com/line/lbm-sdk/types" + sdkerrors "github.com/line/lbm-sdk/types/errors" + "github.com/line/lbm-sdk/x/token/class" +) + +const ( + lengthClassID = 8 + + nameLengthLimit = 20 + baseImgURILengthLimit = 1000 + metaLengthLimit = 1000 + changesLimit = 100 +) + +var ( + patternAll = fmt.Sprintf(`[0-9a-f]{%d}`, lengthClassID) + patternZero = fmt.Sprintf(`0{%d}`, lengthClassID) + + patternClassID = patternAll + patternLegacyFTClassID = fmt.Sprintf(`0[0-9a-f]{%d}`, lengthClassID-1) + patternLegacyNFTClassID = fmt.Sprintf(`[1-9a-f][0-9a-f]{%d}`, lengthClassID-1) + + // regexps for class ids + reClassID = regexp.MustCompile(fmt.Sprintf(`^%s$`, patternClassID)) + reLegacyFTClassID = regexp.MustCompile(fmt.Sprintf(`^%s$`, patternLegacyFTClassID)) + reLegacyNFTClassID = regexp.MustCompile(fmt.Sprintf(`^%s$`, patternLegacyNFTClassID)) + + // regexps for token ids + reTokenID = regexp.MustCompile(fmt.Sprintf(`^%s%s$`, patternClassID, patternAll)) + reFTID = regexp.MustCompile(fmt.Sprintf(`^%s%s$`, patternClassID, patternZero)) + reLegacyNFTID = regexp.MustCompile(fmt.Sprintf(`^%s%s$`, patternLegacyNFTClassID, patternAll)) +) + +func validateAmount(amount sdk.Int) error { + if !amount.IsPositive() { + return sdkerrors.ErrInvalidRequest.Wrapf("amount must be positive: %s", amount) + } + return nil +} + +// deprecated +func validateCoins(amount []Coin) error { + return validateCoinsWithIDValidator(amount, ValidateTokenID) +} + +// deprecated +func validateCoinsWithIDValidator(amount []Coin, validator func(string) error) error { + for _, amt := range amount { + if err := validator(amt.TokenId); err != nil { + return err + } + if err := validateAmount(amt.Amount); err != nil { + return err + } + } + return nil +} + +func NewFTID(classID string) string { + return newTokenID(classID, sdk.ZeroUint()) +} + +func NewNFTID(classID string, number int) string { + return newTokenID(classID, sdk.NewUint(uint64(number))) +} + +func newTokenID(classID string, number sdk.Uint) string { + numberFormat := "%0" + fmt.Sprintf("%d", lengthClassID) + "x" + return classID + fmt.Sprintf(numberFormat, number.Uint64()) +} + +func SplitTokenID(tokenID string) (classID string) { + return tokenID[:lengthClassID] +} + +func ValidateContractID(id string) error { + return class.ValidateID(id) +} + +func ValidateClassID(id string) error { + return validateID(id, reClassID) +} + +// Deprecated: do not use (no successor). +func ValidateLegacyFTClassID(id string) error { + return validateID(id, reLegacyFTClassID) +} + +// Deprecated: do not use (no successor). +func ValidateLegacyNFTClassID(id string) error { + return validateID(id, reLegacyNFTClassID) +} + +func ValidateTokenID(id string) error { + return validateID(id, reTokenID) +} + +func ValidateFTID(id string) error { + return validateID(id, reFTID) +} + +func ValidateNFTID(id string) error { + if err := ValidateTokenID(id); err != nil { + return err + } + if err := ValidateFTID(id); err == nil { + return sdkerrors.ErrInvalidRequest.Wrapf("invalid id: %s", id) + } + return nil +} + +// Deprecated: do not use (no successor). +func ValidateLegacyNFTID(id string) error { + return validateID(id, reLegacyNFTID) +} + +func validateID(id string, reg *regexp.Regexp) error { + if !reg.MatchString(id) { + return sdkerrors.ErrInvalidRequest.Wrapf("invalid id: %s", id) + } + return nil +} + +func validateName(name string) error { + return validateStringSize(name, nameLengthLimit, "name") +} + +func validateBaseImgURI(baseImgURI string) error { + return validateStringSize(baseImgURI, baseImgURILengthLimit, "base_img_uri") +} + +func validateMeta(meta string) error { + return validateStringSize(meta, metaLengthLimit, "meta") +} + +func validateStringSize(str string, limit int, name string) error { + if length := utf8.RuneCountInString(str); length > limit { + return sdkerrors.ErrInvalidRequest.Wrapf("%s cannot exceed %d in length: current %d", name, limit, length) + } + return nil +} + +func validateDecimals(decimals int32) error { + if decimals < 0 || decimals > 18 { + return sdkerrors.ErrInvalidRequest.Wrapf("invalid decimals: %d", decimals) + } + return nil +} + +func validateLegacyPermission(permission string) error { + return ValidatePermission(Permission(LegacyPermissionFromString(permission))) +} + +func ValidatePermission(permission Permission) error { + if p := Permission_value[Permission_name[int32(permission)]]; p == 0 { + return sdkerrors.ErrInvalidRequest.Wrapf("invalid permission: %s", permission) + } + return nil +} + +func validateChanges(changes []Attribute, validator func(change Attribute) error) error { + if len(changes) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("empty changes") + } + if len(changes) > changesLimit { + return sdkerrors.ErrInvalidRequest.Wrapf("the number of changes exceeds the limit: %d > %d", len(changes), changesLimit) + } + seenKeys := map[string]bool{} + for _, change := range changes { + if seenKeys[change.Key] { + return sdkerrors.ErrInvalidRequest.Wrapf("duplicate keys: %s", change.Key) + } + seenKeys[change.Key] = true + + if err := validator(change); err != nil { + return err + } + } + + return nil +} + +func validateContractChange(change Attribute) error { + validators := map[AttributeKey]func(string) error{ + AttributeKeyName: validateName, + AttributeKeyBaseImgURI: validateBaseImgURI, + AttributeKeyMeta: validateMeta, + } + + return validateChange(change, validators) +} + +func validateTokenClassChange(change Attribute) error { + validators := map[AttributeKey]func(string) error{ + AttributeKeyName: validateName, + AttributeKeyMeta: validateMeta, + } + + return validateChange(change, validators) +} + +func validateNFTChange(change Attribute) error { + validators := map[AttributeKey]func(string) error{ + AttributeKeyName: validateName, + AttributeKeyMeta: validateMeta, + } + + return validateChange(change, validators) +} + +func validateChange(change Attribute, validators map[AttributeKey]func(string) error) error { + validator, ok := validators[AttributeKeyFromString(change.Key)] + if !ok { + return sdkerrors.ErrInvalidRequest.Wrapf("invalid field: %s", change.Key) + } + return validator(change.Value) +} + +var _ sdk.Msg = (*MsgSend)(nil) + +// ValidateBasic implements Msg. +func (m MsgSend) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if err := m.Amount.ValidateBasic(); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgSend) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgOperatorSend)(nil) + +// ValidateBasic implements Msg. +func (m MsgOperatorSend) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if err := m.Amount.ValidateBasic(); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgOperatorSend) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgTransferFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgTransferFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if err := validateCoins(m.Amount); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgTransferFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgTransferFTFrom)(nil) + +// ValidateBasic implements Msg. +func (m MsgTransferFTFrom) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Proxy); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid proxy address: %s", m.Proxy) + } + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if err := validateCoins(m.Amount); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgTransferFTFrom) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Proxy) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgTransferNFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgTransferNFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if len(m.TokenIds) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("token ids cannot be empty") + } + for _, id := range m.TokenIds { + if err := ValidateTokenID(id); err != nil { + return err + } + } + + return nil +} + +// GetSigners implements Msg +func (m MsgTransferNFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgTransferNFTFrom)(nil) + +// ValidateBasic implements Msg. +func (m MsgTransferNFTFrom) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Proxy); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid proxy address: %s", m.Proxy) + } + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if len(m.TokenIds) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("token ids cannot be empty") + } + for _, id := range m.TokenIds { + if err := ValidateTokenID(id); err != nil { + return err + } + } + + return nil +} + +// GetSigners implements Msg +func (m MsgTransferNFTFrom) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Proxy) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgAuthorizeOperator)(nil) + +// ValidateBasic implements Msg. +func (m MsgAuthorizeOperator) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Holder); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid holder address: %s", m.Holder) + } + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + + return nil +} + +// GetSigners implements Msg +func (m MsgAuthorizeOperator) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Holder) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgRevokeOperator)(nil) + +// ValidateBasic implements Msg. +func (m MsgRevokeOperator) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Holder); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid holder address: %s", m.Holder) + } + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + + return nil +} + +// GetSigners implements Msg +func (m MsgRevokeOperator) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Holder) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgApprove)(nil) + +// ValidateBasic implements Msg. +func (m MsgApprove) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Approver); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid approver address: %s", m.Approver) + } + if err := sdk.ValidateAccAddress(m.Proxy); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid proxy address: %s", m.Proxy) + } + + return nil +} + +// GetSigners implements Msg +func (m MsgApprove) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Approver) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgDisapprove)(nil) + +// ValidateBasic implements Msg. +func (m MsgDisapprove) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Approver); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid approver address: %s", m.Approver) + } + if err := sdk.ValidateAccAddress(m.Proxy); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid proxy address: %s", m.Proxy) + } + + return nil +} + +// GetSigners implements Msg +func (m MsgDisapprove) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Approver) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgCreateContract)(nil) + +// ValidateBasic implements Msg. +func (m MsgCreateContract) ValidateBasic() error { + if err := sdk.ValidateAccAddress(m.Owner); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid owner address: %s", m.Owner) + } + + if err := validateName(m.Name); err != nil { + return err + } + + if err := validateBaseImgURI(m.BaseImgUri); err != nil { + return err + } + + if err := validateMeta(m.Meta); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgCreateContract) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Owner) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgCreateFTClass)(nil) + +// ValidateBasic implements Msg. +func (m MsgCreateFTClass) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + + if err := validateName(m.Name); err != nil { + return err + } + + if err := validateMeta(m.Meta); err != nil { + return err + } + + if err := validateDecimals(m.Decimals); err != nil { + return err + } + + if m.Supply.IsNil() || m.Supply.IsNegative() { + return sdkerrors.ErrInvalidRequest.Wrap("supply cannot be negative") + } + if m.Supply.IsPositive() { + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + } + + return nil +} + +// GetSigners implements Msg +func (m MsgCreateFTClass) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgCreateNFTClass)(nil) + +// ValidateBasic implements Msg. +func (m MsgCreateNFTClass) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + + if err := validateName(m.Name); err != nil { + return err + } + + if err := validateMeta(m.Meta); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgCreateNFTClass) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgIssueFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgIssueFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Owner); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid owner address: %s", m.Owner) + } + + if len(m.Name) == 0 { + return sdkerrors.ErrInvalidRequest.Wrapf("empty name") + } + if err := validateName(m.Name); err != nil { + return err + } + + if err := validateMeta(m.Meta); err != nil { + return err + } + + if err := validateDecimals(m.Decimals); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + // daphne compat. + if m.Amount.Equal(sdk.OneInt()) && m.Decimals == 0 && !m.Mintable { + return sdkerrors.ErrInvalidRequest.Wrap("invalid issue of ft") + } + + return nil +} + +// GetSigners implements Msg +func (m MsgIssueFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Owner) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgIssueNFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgIssueNFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := validateName(m.Name); err != nil { + return err + } + + if err := validateMeta(m.Meta); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Owner); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid owner address: %s", m.Owner) + } + + return nil +} + +// GetSigners implements Msg +func (m MsgIssueNFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Owner) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgMintFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgMintFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if err := validateCoins(m.Amount); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgMintFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgMintNFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgMintNFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if len(m.Params) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("mint params cannot be empty") + } + for _, param := range m.Params { + classID := param.TokenType + if err := ValidateLegacyNFTClassID(classID); err != nil { + return err + } + + if err := validateName(param.Name); err != nil { + return err + } + + if err := validateMeta(param.Meta); err != nil { + return err + } + } + + return nil +} + +// GetSigners implements Msg +func (m MsgMintNFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgBurn)(nil) + +// ValidateBasic implements Msg. +func (m MsgBurn) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := m.Amount.ValidateBasic(); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgBurn) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgOperatorBurn)(nil) + +// ValidateBasic implements Msg. +func (m MsgOperatorBurn) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := m.Amount.ValidateBasic(); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgOperatorBurn) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgBurnFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgBurnFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := validateCoins(m.Amount); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgBurnFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgBurnFTFrom)(nil) + +// ValidateBasic implements Msg. +func (m MsgBurnFTFrom) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Proxy); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid proxy address: %s", m.Proxy) + } + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := validateCoins(m.Amount); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgBurnFTFrom) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Proxy) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgBurnNFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgBurnNFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if len(m.TokenIds) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("token ids cannot be empty") + } + for _, id := range m.TokenIds { + if err := ValidateLegacyNFTID(id); err != nil { + return err + } + } + + return nil +} + +// GetSigners implements Msg +func (m MsgBurnNFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgBurnNFTFrom)(nil) + +// ValidateBasic implements Msg. +func (m MsgBurnNFTFrom) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Proxy); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid proxy address: %s", m.Proxy) + } + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if len(m.TokenIds) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("token ids cannot be empty") + } + for _, id := range m.TokenIds { + if err := ValidateLegacyNFTID(id); err != nil { + return err + } + } + + return nil +} + +// GetSigners implements Msg +func (m MsgBurnNFTFrom) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Proxy) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgModifyContract)(nil) + +// ValidateBasic implements Msg. +func (m MsgModifyContract) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + + if err := validateChanges(m.Changes, validateContractChange); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgModifyContract) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgModifyTokenClass)(nil) + +// ValidateBasic implements Msg. +func (m MsgModifyTokenClass) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + + if err := ValidateClassID(m.ClassId); err != nil { + return err + } + + if err := validateChanges(m.Changes, validateTokenClassChange); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgModifyTokenClass) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgModifyNFT)(nil) + +// ValidateBasic implements Msg. +func (m MsgModifyNFT) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + + if err := ValidateNFTID(m.TokenId); err != nil { + return err + } + + if err := validateChanges(m.Changes, validateNFTChange); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgModifyNFT) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgModify)(nil) + +// ValidateBasic implements Msg. +func (m MsgModify) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Owner); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid owner address: %s", m.Owner) + } + + if len(m.TokenType) != 0 { + classID := m.TokenType + if err := ValidateClassID(classID); err != nil { + return err + } + if err := ValidateLegacyFTClassID(classID); err == nil && len(m.TokenIndex) == 0 { + // smells + return sdkerrors.ErrInvalidRequest.Wrap("fungible token type without index") + } + } + + if len(m.TokenIndex) != 0 { + tokenID := m.TokenType + m.TokenIndex + if err := ValidateTokenID(tokenID); err != nil { + return err + } + } + + validator := validateTokenClassChange + if len(m.TokenType) == 0 { + if len(m.TokenIndex) == 0 { + validator = validateContractChange + } else { + return sdkerrors.ErrInvalidRequest.Wrap("token index without type") + } + } + if len(m.Changes) == 0 { + return sdkerrors.ErrInvalidRequest.Wrap("empty changes") + } + if len(m.Changes) > changesLimit { + return sdkerrors.ErrInvalidRequest.Wrapf("the number of changes exceeds the limit: %d > %d", len(m.Changes), changesLimit) + } + seenKeys := map[string]bool{} + for _, change := range m.Changes { + if seenKeys[change.Field] { + return sdkerrors.ErrInvalidRequest.Wrapf("duplicate keys: %s", change.Field) + } + seenKeys[change.Field] = true + + attribute := Attribute{ + Key: change.Field, + Value: change.Value, + } + if err := validator(attribute); err != nil { + return err + } + } + + return nil +} + +// GetSigners implements Msg +func (m MsgModify) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Owner) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgGrant)(nil) + +// ValidateBasic implements Msg. +func (m MsgGrant) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Granter); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid granter address: %s", m.Granter) + } + if err := sdk.ValidateAccAddress(m.Grantee); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid grantee address: %s", m.Grantee) + } + + if err := ValidatePermission(m.Permission); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgGrant) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Granter) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgAbandon)(nil) + +// ValidateBasic implements Msg. +func (m MsgAbandon) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Grantee); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid grantee address: %s", m.Grantee) + } + + if err := ValidatePermission(m.Permission); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgAbandon) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Grantee) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgGrantPermission)(nil) + +// ValidateBasic implements Msg. +func (m MsgGrantPermission) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + if err := sdk.ValidateAccAddress(m.To); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", m.To) + } + + if err := validateLegacyPermission(m.Permission); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgGrantPermission) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgRevokePermission)(nil) + +// ValidateBasic implements Msg. +func (m MsgRevokePermission) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := validateLegacyPermission(m.Permission); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgRevokePermission) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgAttach)(nil) + +// ValidateBasic implements Msg. +func (m MsgAttach) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := ValidateTokenID(m.TokenId); err != nil { + return err + } + if err := ValidateTokenID(m.ToTokenId); err != nil { + return err + } + + if m.TokenId == m.ToTokenId { + return sdkerrors.ErrInvalidRequest.Wrap("cannot attach token to itself") + } + + return nil +} + +// GetSigners implements Msg +func (m MsgAttach) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgDetach)(nil) + +// ValidateBasic implements Msg. +func (m MsgDetach) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := ValidateTokenID(m.TokenId); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgDetach) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.From) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgOperatorAttach)(nil) + +// ValidateBasic implements Msg. +func (m MsgOperatorAttach) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + if err := sdk.ValidateAccAddress(m.Owner); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid owner address: %s", m.Owner) + } + + if err := ValidateTokenID(m.Subject); err != nil { + return err + } + if err := ValidateTokenID(m.Target); err != nil { + return err + } + + if m.Subject == m.Target { + return sdkerrors.ErrInvalidRequest.Wrap("cannot attach token to itself") + } + + return nil +} + +// GetSigners implements Msg +func (m MsgOperatorAttach) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgOperatorDetach)(nil) + +// ValidateBasic implements Msg. +func (m MsgOperatorDetach) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Operator); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid operator address: %s", m.Operator) + } + if err := sdk.ValidateAccAddress(m.Owner); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid owner address: %s", m.Owner) + } + + if err := ValidateTokenID(m.Subject); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgOperatorDetach) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Operator) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgAttachFrom)(nil) + +// ValidateBasic implements Msg. +func (m MsgAttachFrom) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Proxy); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid proxy address: %s", m.Proxy) + } + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := ValidateTokenID(m.TokenId); err != nil { + return err + } + if err := ValidateTokenID(m.ToTokenId); err != nil { + return err + } + + if m.TokenId == m.ToTokenId { + return sdkerrors.ErrInvalidRequest.Wrap("cannot attach token to itself") + } + + return nil +} + +// GetSigners implements Msg +func (m MsgAttachFrom) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Proxy) + return []sdk.AccAddress{signer} +} + +var _ sdk.Msg = (*MsgDetachFrom)(nil) + +// ValidateBasic implements Msg. +func (m MsgDetachFrom) ValidateBasic() error { + if err := ValidateContractID(m.ContractId); err != nil { + return err + } + + if err := sdk.ValidateAccAddress(m.Proxy); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid proxy address: %s", m.Proxy) + } + if err := sdk.ValidateAccAddress(m.From); err != nil { + return sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", m.From) + } + + if err := ValidateTokenID(m.TokenId); err != nil { + return err + } + + return nil +} + +// GetSigners implements Msg +func (m MsgDetachFrom) GetSigners() []sdk.AccAddress { + signer := sdk.AccAddress(m.Proxy) + return []sdk.AccAddress{signer} +} diff --git a/x/collection/msgs_test.go b/x/collection/msgs_test.go new file mode 100644 index 0000000000..1028c4a7e3 --- /dev/null +++ b/x/collection/msgs_test.go @@ -0,0 +1,2672 @@ +package collection_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/line/lbm-sdk/crypto/keys/secp256k1" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/lbm-sdk/x/collection" +) + +func TestMsgSend(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + amount := collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ) + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + to sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid msg": { + contractID: contractID, + from: addrs[0], + to: addrs[1], + amount: amount, + valid: true, + }, + "empty from": { + contractID: contractID, + to: addrs[1], + amount: amount, + }, + "invalid contract id": { + from: addrs[0], + to: addrs[1], + amount: amount, + }, + "invalid to": { + contractID: contractID, + from: addrs[0], + amount: amount, + }, + "empty amount": { + contractID: contractID, + from: addrs[0], + to: addrs[1], + }, + "invalid token id": { + contractID: contractID, + from: addrs[0], + to: addrs[1], + amount: []collection.Coin{{ + Amount: sdk.OneInt(), + }}, + }, + "duplicate token ids": { + contractID: contractID, + from: addrs[0], + to: addrs[1], + amount: []collection.Coin{amount[0], amount[0]}, + }, + "invalid amount": { + contractID: contractID, + from: addrs[0], + to: addrs[1], + amount: []collection.Coin{{ + TokenId: amount[0].TokenId, + }}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgSend{ + ContractId: tc.contractID, + From: tc.from.String(), + To: tc.to.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgOperatorSend(t *testing.T) { + addrs := make([]sdk.AccAddress, 3) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + amount := collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ) + + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + from sdk.AccAddress + to sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid msg": { + contractID: contractID, + operator: addrs[0], + from: addrs[1], + to: addrs[2], + amount: amount, + valid: true, + }, + "invalid operator": { + contractID: contractID, + from: addrs[1], + to: addrs[2], + amount: amount, + }, + "invalid contract id": { + operator: addrs[0], + from: addrs[1], + to: addrs[2], + amount: amount, + }, + "empty from": { + contractID: contractID, + operator: addrs[0], + to: addrs[1], + amount: amount, + }, + "invalid to": { + contractID: contractID, + operator: addrs[0], + from: addrs[1], + amount: amount, + }, + "empty amount": { + contractID: contractID, + operator: addrs[0], + from: addrs[1], + to: addrs[2], + }, + } + + for name, tc := range testCases { + msg := collection.MsgOperatorSend{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + From: tc.from.String(), + To: tc.to.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgTransferFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + amount := collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ) + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + to sdk.AccAddress + amount []collection.Coin + valid bool + panic bool + }{ + "valid msg": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + amount: amount, + valid: true, + }, + "empty from": { + contractID: "deadbeef", + to: addrs[1], + amount: amount, + }, + "invalid contract id": { + from: addrs[0], + to: addrs[1], + amount: amount, + }, + "invalid to": { + contractID: "deadbeef", + from: addrs[0], + amount: amount, + }, + "nil amount": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + amount: []collection.Coin{{ + TokenId: collection.NewFTID("00bab10c"), + }}, + panic: true, + }, + "zero amount": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + amount: []collection.Coin{{ + TokenId: collection.NewFTID("00bab10c"), + Amount: sdk.ZeroInt(), + }}, + }, + "invalid token id": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + amount: []collection.Coin{{ + Amount: sdk.OneInt(), + }}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgTransferFT{ + ContractId: tc.contractID, + From: tc.from.String(), + To: tc.to.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + if tc.panic { + require.Panics(t, func() { msg.ValidateBasic() }, name) + continue + } + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgTransferFTFrom(t *testing.T) { + addrs := make([]sdk.AccAddress, 3) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + amount := collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ) + + testCases := map[string]struct { + contractID string + proxy sdk.AccAddress + from sdk.AccAddress + to sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + to: addrs[2], + amount: amount, + valid: true, + }, + "invalid proxy": { + contractID: "deadbeef", + from: addrs[1], + to: addrs[2], + amount: amount, + }, + "invalid contract id": { + proxy: addrs[0], + from: addrs[1], + to: addrs[2], + amount: amount, + }, + "empty from": { + contractID: "deadbeef", + proxy: addrs[0], + to: addrs[1], + amount: amount, + }, + "invalid to": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + amount: amount, + }, + "invalid amount": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + to: addrs[2], + amount: []collection.Coin{{ + Amount: sdk.OneInt(), + }}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgTransferFTFrom{ + ContractId: tc.contractID, + Proxy: tc.proxy.String(), + From: tc.from.String(), + To: tc.to.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.proxy}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgTransferNFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + ids := []string{collection.NewNFTID("deadbeef", 1)} + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + to sdk.AccAddress + ids []string + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + ids: ids, + valid: true, + }, + "empty from": { + contractID: "deadbeef", + to: addrs[1], + ids: ids, + }, + "invalid contract id": { + from: addrs[0], + to: addrs[1], + ids: ids, + }, + "invalid to": { + contractID: "deadbeef", + from: addrs[0], + ids: ids, + }, + "empty token ids": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + }, + "invalid token ids": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + ids: []string{""}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgTransferNFT{ + ContractId: tc.contractID, + From: tc.from.String(), + To: tc.to.String(), + TokenIds: tc.ids, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgTransferNFTFrom(t *testing.T) { + addrs := make([]sdk.AccAddress, 3) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + ids := []string{collection.NewNFTID("deadbeef", 1)} + + testCases := map[string]struct { + contractID string + proxy sdk.AccAddress + from sdk.AccAddress + to sdk.AccAddress + ids []string + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + to: addrs[2], + ids: ids, + valid: true, + }, + "invalid proxy": { + contractID: "deadbeef", + from: addrs[1], + to: addrs[2], + ids: ids, + }, + "invalid contract id": { + proxy: addrs[0], + from: addrs[1], + to: addrs[2], + ids: ids, + }, + "empty from": { + contractID: "deadbeef", + proxy: addrs[0], + to: addrs[1], + ids: ids, + }, + "invalid to": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + ids: ids, + }, + "empty ids": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + to: addrs[2], + }, + "invalid id": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + to: addrs[2], + ids: []string{""}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgTransferNFTFrom{ + ContractId: tc.contractID, + Proxy: tc.proxy.String(), + From: tc.from.String(), + To: tc.to.String(), + TokenIds: tc.ids, + } + + require.Equal(t, []sdk.AccAddress{tc.proxy}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgAuthorizeOperator(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + testCases := map[string]struct { + contractID string + holder sdk.AccAddress + operator sdk.AccAddress + valid bool + }{ + "valid msg": { + contractID: contractID, + holder: addrs[0], + operator: addrs[1], + valid: true, + }, + "invalid contract id": { + holder: addrs[0], + operator: addrs[1], + }, + "invalid holder": { + contractID: contractID, + operator: addrs[1], + }, + "empty operator": { + contractID: contractID, + holder: addrs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgAuthorizeOperator{ + ContractId: tc.contractID, + Holder: tc.holder.String(), + Operator: tc.operator.String(), + } + + require.Equal(t, []sdk.AccAddress{tc.holder}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgRevokeOperator(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + testCases := map[string]struct { + contractID string + holder sdk.AccAddress + operator sdk.AccAddress + valid bool + }{ + "valid msg": { + contractID: contractID, + holder: addrs[0], + operator: addrs[1], + valid: true, + }, + "invalid contract id": { + holder: addrs[0], + operator: addrs[1], + }, + "invalid holder": { + contractID: contractID, + operator: addrs[1], + }, + "empty operator": { + contractID: contractID, + holder: addrs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgRevokeOperator{ + ContractId: tc.contractID, + Holder: tc.holder.String(), + Operator: tc.operator.String(), + } + + require.Equal(t, []sdk.AccAddress{tc.holder}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgApprove(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + testCases := map[string]struct { + contractID string + approver sdk.AccAddress + proxy sdk.AccAddress + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + approver: addrs[0], + proxy: addrs[1], + valid: true, + }, + "invalid contract id": { + approver: addrs[0], + proxy: addrs[1], + }, + "invalid approver": { + contractID: "deadbeef", + proxy: addrs[1], + }, + "empty proxy": { + contractID: "deadbeef", + approver: addrs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgApprove{ + ContractId: tc.contractID, + Approver: tc.approver.String(), + Proxy: tc.proxy.String(), + } + + require.Equal(t, []sdk.AccAddress{tc.approver}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgDisapprove(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + testCases := map[string]struct { + contractID string + approver sdk.AccAddress + proxy sdk.AccAddress + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + approver: addrs[0], + proxy: addrs[1], + valid: true, + }, + "invalid contract id": { + approver: addrs[0], + proxy: addrs[1], + }, + "invalid approver": { + contractID: "deadbeef", + proxy: addrs[1], + }, + "empty proxy": { + contractID: "deadbeef", + approver: addrs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgDisapprove{ + ContractId: tc.contractID, + Approver: tc.approver.String(), + Proxy: tc.proxy.String(), + } + + require.Equal(t, []sdk.AccAddress{tc.approver}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgCreateContract(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + name := "tibetian fox" + uri := "file:///tibetian_fox.png" + meta := "Tibetian fox" + testCases := map[string]struct { + owner sdk.AccAddress + name string + baseImgURI string + meta string + valid bool + }{ + "valid msg": { + owner: addrs[0], + name: name, + baseImgURI: uri, + meta: meta, + valid: true, + }, + "invalid owner": { + name: name, + baseImgURI: uri, + meta: meta, + }, + "long name": { + owner: addrs[0], + name: string(make([]rune, 21)), + baseImgURI: uri, + meta: meta, + }, + "invalid base image uri": { + owner: addrs[0], + name: name, + baseImgURI: string(make([]rune, 1001)), + meta: meta, + }, + "invalid meta": { + owner: addrs[0], + name: name, + baseImgURI: uri, + meta: string(make([]rune, 1001)), + }, + } + + for name, tc := range testCases { + msg := collection.MsgCreateContract{ + Owner: tc.owner.String(), + Name: tc.name, + BaseImgUri: tc.baseImgURI, + Meta: tc.meta, + } + + require.Equal(t, []sdk.AccAddress{tc.owner}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgIssueFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + name := "tibetian fox" + meta := "Tibetian Fox" + decimals := int32(8) + testCases := map[string]struct { + contractID string + owner sdk.AccAddress + to sdk.AccAddress + name string + meta string + decimals int32 + mintable bool + amount sdk.Int + valid bool + }{ + "valid msg": { + contractID: contractID, + owner: addrs[0], + to: addrs[1], + name: name, + meta: meta, + decimals: decimals, + amount: sdk.OneInt(), + valid: true, + }, + "invalid contract id": { + owner: addrs[0], + to: addrs[1], + name: name, + meta: meta, + decimals: decimals, + amount: sdk.OneInt(), + }, + "invalid owner": { + contractID: contractID, + to: addrs[1], + name: name, + meta: meta, + decimals: decimals, + amount: sdk.OneInt(), + }, + "empty to": { + contractID: contractID, + owner: addrs[0], + name: name, + meta: meta, + decimals: decimals, + amount: sdk.OneInt(), + }, + "empty name": { + contractID: contractID, + owner: addrs[0], + to: addrs[1], + meta: meta, + decimals: decimals, + amount: sdk.OneInt(), + }, + "long name": { + contractID: contractID, + owner: addrs[0], + to: addrs[1], + name: string(make([]rune, 21)), + meta: meta, + decimals: decimals, + amount: sdk.OneInt(), + valid: false, + }, + "invalid meta": { + contractID: contractID, + owner: addrs[0], + to: addrs[1], + name: name, + meta: string(make([]rune, 1001)), + decimals: decimals, + amount: sdk.OneInt(), + }, + "invalid decimals": { + contractID: contractID, + owner: addrs[0], + to: addrs[1], + name: name, + meta: meta, + decimals: 19, + amount: sdk.OneInt(), + }, + "daphne compat": { + contractID: contractID, + owner: addrs[0], + to: addrs[1], + name: name, + meta: meta, + amount: sdk.OneInt(), + }, + } + + for name, tc := range testCases { + msg := collection.MsgIssueFT{ + ContractId: tc.contractID, + Owner: tc.owner.String(), + To: tc.to.String(), + Name: tc.name, + Meta: tc.meta, + Decimals: tc.decimals, + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.owner}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgIssueNFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + name := "tibetian fox" + meta := "Tibetian Fox" + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + name string + meta string + valid bool + }{ + "valid msg": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: meta, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + name: name, + meta: meta, + }, + "invalid operator": { + contractID: contractID, + name: name, + meta: meta, + }, + "long name": { + contractID: contractID, + operator: addrs[0], + name: string(make([]rune, 21)), + meta: meta, + }, + "invalid meta": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: string(make([]rune, 1001)), + }, + } + + for name, tc := range testCases { + msg := collection.MsgIssueNFT{ + ContractId: tc.contractID, + Owner: tc.operator.String(), + Name: tc.name, + Meta: tc.meta, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgCreateFTClass(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + name := "tibetian fox" + meta := "Tibetian Fox" + decimals := int32(8) + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + name string + meta string + decimals int32 + to sdk.AccAddress + supply sdk.Int + valid bool + }{ + "valid msg": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: meta, + decimals: decimals, + supply: sdk.ZeroInt(), + valid: true, + }, + "valid msg with supply": { + contractID: contractID, + operator: addrs[0], + to: addrs[1], + name: name, + meta: meta, + decimals: decimals, + supply: sdk.OneInt(), + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + name: name, + meta: meta, + decimals: decimals, + supply: sdk.ZeroInt(), + }, + "invalid operator": { + contractID: contractID, + name: name, + meta: meta, + decimals: decimals, + supply: sdk.ZeroInt(), + }, + "long name": { + contractID: contractID, + operator: addrs[0], + name: string(make([]rune, 21)), + meta: meta, + decimals: decimals, + supply: sdk.ZeroInt(), + valid: false, + }, + "invalid meta": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: string(make([]rune, 1001)), + decimals: decimals, + supply: sdk.ZeroInt(), + }, + "invalid decimals": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: meta, + decimals: 19, + supply: sdk.ZeroInt(), + }, + "positive supply with invalid to": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: meta, + decimals: decimals, + supply: sdk.OneInt(), + }, + "invalid supply": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: meta, + decimals: decimals, + }, + } + + for name, tc := range testCases { + msg := collection.MsgCreateFTClass{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + To: tc.to.String(), + Name: tc.name, + Meta: tc.meta, + Decimals: tc.decimals, + Supply: tc.supply, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgCreateNFTClass(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + name := "tibetian fox" + meta := "Tibetian Fox" + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + name string + meta string + valid bool + }{ + "valid msg": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: meta, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + name: name, + meta: meta, + }, + "invalid operator": { + contractID: contractID, + name: name, + meta: meta, + }, + "long name": { + contractID: contractID, + operator: addrs[0], + name: string(make([]rune, 21)), + meta: meta, + }, + "invalid meta": { + contractID: contractID, + operator: addrs[0], + name: name, + meta: string(make([]rune, 1001)), + }, + } + + for name, tc := range testCases { + msg := collection.MsgCreateNFTClass{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Name: tc.name, + Meta: tc.meta, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgMintFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + amount := collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ) + + contractID := "deadbeef" + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + to sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid msg": { + contractID: contractID, + operator: addrs[0], + to: addrs[1], + amount: amount, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + to: addrs[1], + amount: amount, + }, + "invalid operator": { + contractID: contractID, + to: addrs[1], + amount: amount, + }, + "empty to": { + contractID: contractID, + operator: addrs[0], + amount: amount, + }, + "invalid token id": { + contractID: contractID, + operator: addrs[0], + to: addrs[1], + amount: []collection.Coin{{ + Amount: sdk.OneInt(), + }}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgMintFT{ + ContractId: tc.contractID, + From: tc.operator.String(), + To: tc.to.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgMintNFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + params := []collection.MintNFTParam{{ + TokenType: "deadbeef", + Name: "tibetian fox", + Meta: "Tibetian Fox", + }} + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + to sdk.AccAddress + params []collection.MintNFTParam + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + operator: addrs[0], + to: addrs[1], + params: params, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + to: addrs[1], + params: params, + }, + "invalid operator": { + contractID: "deadbeef", + to: addrs[1], + params: params, + }, + "empty to": { + contractID: "deadbeef", + operator: addrs[0], + params: params, + }, + "empty params": { + contractID: "deadbeef", + operator: addrs[0], + to: addrs[1], + }, + "param of invalid token type": { + contractID: "deadbeef", + operator: addrs[0], + to: addrs[1], + params: []collection.MintNFTParam{{}}, + }, + "param of invalid name": { + contractID: "deadbeef", + operator: addrs[0], + to: addrs[1], + params: []collection.MintNFTParam{{ + TokenType: "deadbeef", + Name: string(make([]rune, 21)), + }}, + }, + "param of invalid meta": { + contractID: "deadbeef", + operator: addrs[0], + to: addrs[1], + params: []collection.MintNFTParam{{ + TokenType: "deadbeef", + Meta: string(make([]rune, 1001)), + }}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgMintNFT{ + ContractId: tc.contractID, + From: tc.operator.String(), + To: tc.to.String(), + Params: tc.params, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgBurn(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + amount := collection.NewCoins( + collection.NewNFTCoin("deadbeef", 1), + ) + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid msg": { + contractID: contractID, + from: addrs[0], + amount: amount, + valid: true, + }, + "invalid contract id": { + from: addrs[0], + amount: amount, + }, + "invalid from": { + contractID: contractID, + amount: amount, + }, + "empty amount": { + contractID: contractID, + from: addrs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgBurn{ + ContractId: tc.contractID, + From: tc.from.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgOperatorBurn(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + amount := collection.NewCoins( + collection.NewNFTCoin("deadbeef", 1), + ) + + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid msg": { + contractID: contractID, + operator: addrs[0], + from: addrs[1], + amount: amount, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + from: addrs[1], + amount: amount, + }, + "invalid operator": { + contractID: contractID, + from: addrs[1], + amount: amount, + }, + "empty from": { + contractID: contractID, + operator: addrs[0], + amount: amount, + }, + "empty amount": { + contractID: contractID, + operator: addrs[0], + from: addrs[1], + }, + } + + for name, tc := range testCases { + msg := collection.MsgOperatorBurn{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + From: tc.from.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgBurnFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + amount := collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ) + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + from: addrs[0], + amount: amount, + valid: true, + }, + "invalid contract id": { + from: addrs[0], + amount: amount, + }, + "invalid from": { + contractID: "deadbeef", + amount: amount, + }, + "invalid token id": { + contractID: "deadbeef", + from: addrs[0], + amount: []collection.Coin{{ + Amount: sdk.OneInt(), + }}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgBurnFT{ + ContractId: tc.contractID, + From: tc.from.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgBurnFTFrom(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + amount := collection.NewCoins( + collection.NewFTCoin("00bab10c", sdk.OneInt()), + ) + + testCases := map[string]struct { + contractID string + grantee sdk.AccAddress + from sdk.AccAddress + amount []collection.Coin + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + grantee: addrs[0], + from: addrs[1], + amount: amount, + valid: true, + }, + "invalid contract id": { + grantee: addrs[0], + from: addrs[1], + amount: amount, + }, + "invalid grantee": { + contractID: "deadbeef", + from: addrs[1], + amount: amount, + }, + "empty from": { + contractID: "deadbeef", + grantee: addrs[0], + amount: amount, + }, + "invalid token id": { + contractID: "deadbeef", + grantee: addrs[0], + from: addrs[1], + amount: []collection.Coin{{ + Amount: sdk.OneInt(), + }}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgBurnFTFrom{ + ContractId: tc.contractID, + Proxy: tc.grantee.String(), + From: tc.from.String(), + Amount: tc.amount, + } + + require.Equal(t, []sdk.AccAddress{tc.grantee}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgBurnNFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + ids := []string{collection.NewNFTID("deadbeef", 1)} + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + ids []string + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + from: addrs[0], + ids: ids, + valid: true, + }, + "invalid contract id": { + from: addrs[0], + ids: ids, + }, + "invalid from": { + contractID: "deadbeef", + ids: ids, + }, + "empty ids": { + contractID: "deadbeef", + from: addrs[0], + }, + "invalid id": { + contractID: "deadbeef", + from: addrs[0], + ids: []string{""}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgBurnNFT{ + ContractId: tc.contractID, + From: tc.from.String(), + TokenIds: tc.ids, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgBurnNFTFrom(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + ids := []string{collection.NewNFTID("deadbeef", 1)} + + testCases := map[string]struct { + contractID string + grantee sdk.AccAddress + from sdk.AccAddress + ids []string + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + grantee: addrs[0], + from: addrs[1], + ids: ids, + valid: true, + }, + "invalid contract id": { + grantee: addrs[0], + from: addrs[1], + ids: ids, + }, + "invalid grantee": { + contractID: "deadbeef", + from: addrs[1], + ids: ids, + }, + "empty from": { + contractID: "deadbeef", + grantee: addrs[0], + ids: ids, + }, + "empty ids": { + contractID: "deadbeef", + grantee: addrs[0], + from: addrs[1], + }, + "invalid id": { + contractID: "deadbeef", + grantee: addrs[0], + from: addrs[0], + ids: []string{""}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgBurnNFTFrom{ + ContractId: tc.contractID, + Proxy: tc.grantee.String(), + From: tc.from.String(), + TokenIds: tc.ids, + } + + require.Equal(t, []sdk.AccAddress{tc.grantee}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgModifyContract(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + changes := []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: "fox", + }} + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + changes []collection.Attribute + valid bool + }{ + "valid contract modification": { + contractID: contractID, + operator: addrs[0], + changes: changes, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + changes: changes, + }, + "invalid operator": { + contractID: contractID, + changes: changes, + }, + "invalid key of change": { + contractID: contractID, + operator: addrs[0], + changes: []collection.Attribute{{Value: "fox"}}, + }, + "invalid value of change": { + contractID: contractID, + operator: addrs[0], + changes: []collection.Attribute{{Key: "symbol"}}, + }, + "empty changes": { + contractID: contractID, + operator: addrs[0], + }, + "too many changes": { + contractID: contractID, + operator: addrs[0], + changes: make([]collection.Attribute, 101), + }, + "duplicated changes": { + contractID: contractID, + operator: addrs[0], + changes: []collection.Attribute{changes[0], changes[0]}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgModifyContract{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Changes: tc.changes, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgModifyTokenClass(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + classID := "deadbeef" + changes := []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: "tibetian fox", + }} + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + classID string + changes []collection.Attribute + valid bool + }{ + "valid modification": { + contractID: contractID, + operator: addrs[0], + classID: classID, + changes: changes, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + classID: classID, + changes: changes, + }, + "invalid operator": { + contractID: contractID, + classID: classID, + changes: changes, + }, + "invalid class id": { + contractID: contractID, + operator: addrs[0], + changes: changes, + }, + "invalid key of change": { + contractID: contractID, + operator: addrs[0], + classID: classID, + changes: []collection.Attribute{{Value: "tibetian fox"}}, + }, + "invalid value of change": { + contractID: contractID, + operator: addrs[0], + classID: classID, + changes: []collection.Attribute{{Key: "symbol"}}, + }, + "empty changes": { + contractID: contractID, + operator: addrs[0], + classID: classID, + }, + "too many changes": { + contractID: contractID, + operator: addrs[0], + classID: classID, + changes: make([]collection.Attribute, 101), + }, + "duplicated changes": { + contractID: contractID, + operator: addrs[0], + classID: classID, + changes: []collection.Attribute{changes[0], changes[0]}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgModifyTokenClass{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + ClassId: tc.classID, + Changes: tc.changes, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgModifyNFT(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + classID := "deadbeef" + tokenID := collection.NewNFTID(classID, 1) + changes := []collection.Attribute{{ + Key: collection.AttributeKeyName.String(), + Value: "tibetian fox", + }} + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + tokenID string + changes []collection.Attribute + valid bool + }{ + "valid modification": { + contractID: contractID, + operator: addrs[0], + tokenID: tokenID, + changes: changes, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + tokenID: tokenID, + changes: changes, + }, + "invalid operator": { + contractID: contractID, + tokenID: tokenID, + changes: changes, + }, + "invalid token id": { + contractID: contractID, + operator: addrs[0], + changes: changes, + }, + "invalid key of change": { + contractID: contractID, + operator: addrs[0], + tokenID: tokenID, + changes: []collection.Attribute{{Value: "tibetian fox"}}, + }, + "invalid value of change": { + contractID: contractID, + operator: addrs[0], + tokenID: tokenID, + changes: []collection.Attribute{{Key: "symbol"}}, + }, + "empty changes": { + contractID: contractID, + operator: addrs[0], + tokenID: tokenID, + }, + "too many changes": { + contractID: contractID, + operator: addrs[0], + tokenID: tokenID, + changes: make([]collection.Attribute, 101), + }, + "duplicated changes": { + contractID: contractID, + operator: addrs[0], + tokenID: tokenID, + changes: []collection.Attribute{changes[0], changes[0]}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgModifyNFT{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + TokenId: tc.tokenID, + Changes: tc.changes, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgModify(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + changes := []collection.Change{{Field: "name", Value: "New test"}} + testCases := map[string]struct { + contractID string + owner sdk.AccAddress + tokenType string + tokenIndex string + changes []collection.Change + valid bool + }{ + "valid contract modification": { + contractID: "deadbeef", + owner: addrs[0], + changes: changes, + valid: true, + }, + "valid token class modification": { + contractID: "deadbeef", + tokenType: "deadbeef", + owner: addrs[0], + changes: changes, + valid: true, + }, + "valid nft modification": { + contractID: "deadbeef", + tokenType: "deadbeef", + tokenIndex: "deadbeef", + owner: addrs[0], + changes: changes, + valid: true, + }, + "invalid contract id": { + owner: addrs[0], + changes: changes, + }, + "invalid owner": { + contractID: "deadbeef", + changes: changes, + }, + "invalid key of change": { + contractID: "deadbeef", + owner: addrs[0], + changes: []collection.Change{{Value: "tt"}}, + }, + "invalid value of change": { + contractID: "deadbeef", + owner: addrs[0], + changes: []collection.Change{{Field: "symbol"}}, + }, + "empty changes": { + contractID: "deadbeef", + owner: addrs[0], + }, + "too many changes": { + contractID: "deadbeef", + owner: addrs[0], + changes: make([]collection.Change, 101), + }, + "duplicated changes": { + contractID: "deadbeef", + owner: addrs[0], + changes: []collection.Change{changes[0], changes[0]}, + }, + } + + for name, tc := range testCases { + msg := collection.MsgModify{ + ContractId: tc.contractID, + TokenType: tc.tokenType, + TokenIndex: tc.tokenIndex, + Owner: tc.owner.String(), + Changes: tc.changes, + } + + require.Equal(t, []sdk.AccAddress{tc.owner}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgGrant(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + testCases := map[string]struct { + contractID string + granter sdk.AccAddress + grantee sdk.AccAddress + permission collection.Permission + valid bool + }{ + "valid msg": { + contractID: contractID, + granter: addrs[0], + grantee: addrs[1], + permission: collection.PermissionMint, + valid: true, + }, + "invalid contract id": { + granter: addrs[0], + grantee: addrs[1], + permission: collection.PermissionMint, + }, + "empty granter": { + contractID: contractID, + grantee: addrs[1], + permission: collection.PermissionMint, + }, + "invalid grantee": { + contractID: contractID, + granter: addrs[0], + permission: collection.PermissionMint, + }, + "invalid permission": { + contractID: contractID, + granter: addrs[0], + grantee: addrs[1], + }, + } + + for name, tc := range testCases { + msg := collection.MsgGrant{ + ContractId: tc.contractID, + Granter: tc.granter.String(), + Grantee: tc.grantee.String(), + Permission: tc.permission, + } + + require.Equal(t, []sdk.AccAddress{tc.granter}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgAbandon(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + testCases := map[string]struct { + contractID string + grantee sdk.AccAddress + permission collection.Permission + valid bool + }{ + "valid msg": { + contractID: contractID, + grantee: addrs[0], + permission: collection.PermissionMint, + valid: true, + }, + "invalid contract id": { + grantee: addrs[0], + permission: collection.PermissionMint, + }, + "invalid grantee": { + contractID: contractID, + permission: collection.PermissionMint, + }, + "invalid permission": { + contractID: contractID, + grantee: addrs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgAbandon{ + ContractId: tc.contractID, + Grantee: tc.grantee.String(), + Permission: tc.permission, + } + + require.Equal(t, []sdk.AccAddress{tc.grantee}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgGrantPermission(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + to sdk.AccAddress + permission string + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + permission: collection.LegacyPermissionMint.String(), + valid: true, + }, + "invalid contract id": { + from: addrs[0], + to: addrs[1], + permission: collection.LegacyPermissionMint.String(), + }, + "empty from": { + contractID: "deadbeef", + to: addrs[1], + permission: collection.LegacyPermissionMint.String(), + }, + "invalid to": { + contractID: "deadbeef", + from: addrs[0], + permission: collection.LegacyPermissionMint.String(), + }, + "invalid permission": { + contractID: "deadbeef", + from: addrs[0], + to: addrs[1], + }, + } + + for name, tc := range testCases { + msg := collection.MsgGrantPermission{ + ContractId: tc.contractID, + From: tc.from.String(), + To: tc.to.String(), + Permission: tc.permission, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgRevokePermission(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + permission string + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + from: addrs[0], + permission: collection.LegacyPermissionMint.String(), + valid: true, + }, + "invalid contract id": { + from: addrs[0], + permission: collection.LegacyPermissionMint.String(), + }, + "invalid from": { + contractID: "deadbeef", + permission: collection.LegacyPermissionMint.String(), + }, + "invalid permission": { + contractID: "deadbeef", + from: addrs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgRevokePermission{ + ContractId: tc.contractID, + From: tc.from.String(), + Permission: tc.permission, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgAttach(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + tokenIDs := []string{ + collection.NewNFTID("deadbeef", 1), + collection.NewNFTID("fee1dead", 1), + } + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + tokenID string + toTokenID string + valid bool + }{ + "valid msg": { + contractID: contractID, + from: addrs[0], + tokenID: tokenIDs[0], + toTokenID: tokenIDs[1], + valid: true, + }, + "empty from": { + contractID: contractID, + tokenID: tokenIDs[0], + toTokenID: tokenIDs[1], + }, + "invalid contract id": { + from: addrs[0], + tokenID: tokenIDs[0], + toTokenID: tokenIDs[1], + }, + "invalid token id": { + contractID: contractID, + from: addrs[0], + toTokenID: tokenIDs[1], + }, + "invalid to id": { + contractID: contractID, + from: addrs[0], + tokenID: tokenIDs[0], + }, + "to itself": { + contractID: contractID, + from: addrs[0], + tokenID: tokenIDs[0], + toTokenID: tokenIDs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgAttach{ + ContractId: tc.contractID, + From: tc.from.String(), + TokenId: tc.tokenID, + ToTokenId: tc.toTokenID, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgDetach(t *testing.T) { + addrs := make([]sdk.AccAddress, 1) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + tokenID := collection.NewNFTID("deadbeef", 1) + + testCases := map[string]struct { + contractID string + from sdk.AccAddress + tokenID string + valid bool + }{ + "valid msg": { + contractID: contractID, + from: addrs[0], + tokenID: tokenID, + valid: true, + }, + "empty from": { + contractID: contractID, + tokenID: tokenID, + }, + "invalid contract id": { + from: addrs[0], + tokenID: tokenID, + }, + "invalid token id": { + contractID: contractID, + from: addrs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgDetach{ + ContractId: tc.contractID, + From: tc.from.String(), + TokenId: tc.tokenID, + } + + require.Equal(t, []sdk.AccAddress{tc.from}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgOperatorAttach(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + tokenIDs := []string{ + collection.NewNFTID("deadbeef", 1), + collection.NewNFTID("fee1dead", 1), + } + + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + owner sdk.AccAddress + subject string + target string + valid bool + }{ + "valid msg": { + contractID: contractID, + operator: addrs[0], + owner: addrs[1], + subject: tokenIDs[0], + target: tokenIDs[1], + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + owner: addrs[1], + subject: tokenIDs[0], + target: tokenIDs[1], + }, + "empty operator": { + contractID: contractID, + owner: addrs[1], + subject: tokenIDs[0], + target: tokenIDs[1], + }, + "empty owner": { + contractID: contractID, + operator: addrs[0], + subject: tokenIDs[0], + target: tokenIDs[1], + }, + "invalid token id": { + contractID: contractID, + operator: addrs[0], + owner: addrs[1], + target: tokenIDs[1], + }, + "invalid to id": { + contractID: contractID, + operator: addrs[0], + owner: addrs[1], + subject: tokenIDs[0], + }, + "to itself": { + contractID: contractID, + operator: addrs[0], + owner: addrs[1], + subject: tokenIDs[0], + target: tokenIDs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgOperatorAttach{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Owner: tc.owner.String(), + Subject: tc.subject, + Target: tc.target, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgOperatorDetach(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + contractID := "deadbeef" + tokenID := collection.NewNFTID("deadbeef", 1) + + testCases := map[string]struct { + contractID string + operator sdk.AccAddress + owner sdk.AccAddress + subject string + valid bool + }{ + "valid msg": { + contractID: contractID, + operator: addrs[0], + owner: addrs[1], + subject: tokenID, + valid: true, + }, + "invalid contract id": { + operator: addrs[0], + owner: addrs[1], + subject: tokenID, + }, + "empty operator": { + contractID: contractID, + owner: addrs[1], + subject: tokenID, + }, + "empty owner": { + contractID: contractID, + operator: addrs[0], + subject: tokenID, + }, + "invalid token id": { + contractID: contractID, + operator: addrs[0], + owner: addrs[1], + }, + } + + for name, tc := range testCases { + msg := collection.MsgOperatorDetach{ + ContractId: tc.contractID, + Operator: tc.operator.String(), + Owner: tc.owner.String(), + Subject: tc.subject, + } + + require.Equal(t, []sdk.AccAddress{tc.operator}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgAttachFrom(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + tokenIDs := []string{ + collection.NewNFTID("deadbeef", 1), + collection.NewNFTID("fee1dead", 1), + } + + testCases := map[string]struct { + contractID string + proxy sdk.AccAddress + from sdk.AccAddress + tokenID string + toTokenID string + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + tokenID: tokenIDs[0], + toTokenID: tokenIDs[1], + valid: true, + }, + "empty proxy": { + contractID: "deadbeef", + from: addrs[1], + tokenID: tokenIDs[0], + toTokenID: tokenIDs[1], + }, + "empty from": { + contractID: "deadbeef", + proxy: addrs[0], + tokenID: tokenIDs[0], + toTokenID: tokenIDs[1], + }, + "invalid contract id": { + proxy: addrs[0], + from: addrs[1], + tokenID: tokenIDs[0], + toTokenID: tokenIDs[1], + }, + "invalid token id": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + toTokenID: tokenIDs[1], + }, + "invalid to id": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + tokenID: tokenIDs[0], + }, + "to itself": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + tokenID: tokenIDs[0], + toTokenID: tokenIDs[0], + }, + } + + for name, tc := range testCases { + msg := collection.MsgAttachFrom{ + ContractId: tc.contractID, + Proxy: tc.proxy.String(), + From: tc.from.String(), + TokenId: tc.tokenID, + ToTokenId: tc.toTokenID, + } + + require.Equal(t, []sdk.AccAddress{tc.proxy}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +} + +func TestMsgDetachFrom(t *testing.T) { + addrs := make([]sdk.AccAddress, 2) + for i := range addrs { + addrs[i] = sdk.BytesToAccAddress(secp256k1.GenPrivKey().PubKey().Address()) + } + + tokenID := collection.NewNFTID("deadbeef", 1) + + testCases := map[string]struct { + contractID string + proxy sdk.AccAddress + from sdk.AccAddress + tokenID string + valid bool + }{ + "valid msg": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + tokenID: tokenID, + valid: true, + }, + "empty proxy": { + contractID: "deadbeef", + from: addrs[1], + tokenID: tokenID, + }, + "empty from": { + contractID: "deadbeef", + proxy: addrs[0], + tokenID: tokenID, + }, + "invalid contract id": { + proxy: addrs[0], + from: addrs[1], + tokenID: tokenID, + }, + "invalid token id": { + contractID: "deadbeef", + proxy: addrs[0], + from: addrs[1], + }, + } + + for name, tc := range testCases { + msg := collection.MsgDetachFrom{ + ContractId: tc.contractID, + Proxy: tc.proxy.String(), + From: tc.from.String(), + TokenId: tc.tokenID, + } + + require.Equal(t, []sdk.AccAddress{tc.proxy}, msg.GetSigners()) + + err := msg.ValidateBasic() + if tc.valid { + require.NoError(t, err, name) + } else { + require.Error(t, err, name) + } + } +}