├── .github └── workflows │ └── nft.yml ├── .golangci.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cosmos-sdk-image.jpg └── incubator ├── faucet ├── Makefile_Sample ├── README.md ├── alias.go ├── client │ ├── cli │ │ └── tx.go │ └── rest │ │ ├── rest.go │ │ └── tx.go ├── go.mod ├── handler.go ├── internal │ ├── keeper │ │ ├── keeper.go │ │ └── querier.go │ └── types │ │ ├── codec.go │ │ ├── errors.go │ │ ├── expected_keepers.go │ │ ├── key.go │ │ ├── msgs.go │ │ └── types.go ├── module.go ├── profile.go └── profile_testnet.go ├── nft ├── CONTRACT.md ├── Makefile ├── alias.go ├── app │ ├── app.go │ ├── export.go │ ├── genesis.go │ ├── sim_test.go │ ├── state.go │ └── test_helpers.go ├── client │ ├── cli │ │ ├── query.go │ │ └── tx.go │ └── rest │ │ ├── query.go │ │ ├── rest.go │ │ └── tx.go ├── docs │ └── spec │ │ ├── 01_concepts.md │ │ ├── 02_state.md │ │ ├── 03_messages.md │ │ ├── 04_events.md │ │ ├── 05_future_improvements.md │ │ ├── 06_appendix.md │ │ └── README.md ├── exported │ └── nft.go ├── genesis.go ├── genesis_test.go ├── go.mod ├── go.sum ├── handler.go ├── handler_test.go ├── integration_test.go ├── keeper │ ├── collection.go │ ├── collection_test.go │ ├── integration_test.go │ ├── invariants.go │ ├── keeper.go │ ├── nft.go │ ├── nft_test.go │ ├── owners.go │ ├── owners_test.go │ ├── querier.go │ └── querier_test.go ├── module.go ├── simulation │ ├── decoder.go │ ├── decoder_test.go │ ├── genesis.go │ └── operations.go ├── spec │ ├── 01_concepts.md │ ├── 02_state.md │ ├── 03_messages.md │ ├── 04_events.md │ ├── 05_future_improvements.md │ ├── 06_appendix.md │ └── README.md └── types │ ├── codec.go │ ├── collection.go │ ├── collection_test.go │ ├── errors.go │ ├── events.go │ ├── expected_keepers.go │ ├── genesis.go │ ├── keys.go │ ├── msgs.go │ ├── msgs_test.go │ ├── nft.go │ ├── nft_test.go │ ├── owners.go │ ├── owners_test.go │ ├── querier.go │ ├── test_common.go │ └── utils.go └── poa └── docs └── spec ├── 01_state.md ├── 02_state_transitions.md ├── 03_messages.md ├── 04_end_block.md ├── 05_hooks.md ├── 06_events.md ├── 07_params.md └── README.md /.github/workflows/nft.yml: -------------------------------------------------------------------------------- 1 | name: NFT 2 | on: [pull_request] 3 | jobs: 4 | unit-test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/setup-go@v2 8 | id: go 9 | with: 10 | go-version: 1.14 11 | - name: Setup env for GO 12 | # this is only used until the setup-go action is updated 13 | run: | 14 | echo "::set-env name=GOPATH::$(go env GOPATH)" 15 | echo "::add-path::$(go env GOPATH)/bin" 16 | shell: bash 17 | - uses: actions/checkout@v2 18 | - name: Unit_test 19 | run: cd incubator/nft && make test 20 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 1m 3 | 4 | linters: 5 | enable-all: true 6 | disable: 7 | - gocyclo 8 | - gochecknoinits 9 | - gochecknoglobals 10 | - dupl 11 | - unparam 12 | - lll 13 | - varcheck 14 | - funlen 15 | - godox 16 | 17 | issues: 18 | exclude-rules: 19 | - text: "Use of weak random number generator" 20 | linters: 21 | - gosec 22 | - text: "comment on exported var" 23 | linters: 24 | - golint 25 | - text: "ST1003:" 26 | linters: 27 | - stylecheck 28 | 29 | linters-settings: 30 | dogsled: 31 | max-blank-identifiers: 3 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for considering making contributions to the Cosmos modules repositories! 2 | 3 | ### Adding a Module: 4 | To get your module added to this repo make sure you meet the following requirements: 5 | 6 | 1. Your module has a unique name from any other module in the `cosmos/modules` repository 7 | 2. Your module contains a `LICENSE` AND `README` 8 | 3. Your module has thorough documentation in a `docs/` subfolder 9 | 4. Your module has its own `go.mod` and `go.sum` files 10 | 5. Your module has unit tests along with a `simapp.go` and `sim_test.go` for fuzz testing 11 | 6. Your module contains a `CODEOWNERS.md` file 12 | 13 | ### Module Specific Issues/PRs 14 | 15 | To make an issue or pull request to a specific module (e.g. `modules/poa`), prefix the issue/pr name with the module in question. For example, we can create a PR to update SDK version in poa module like so, `poa: update SDK version`. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmos Modules 2 | 3 | ![banner](cosmos-sdk-image.jpg) 4 | 5 | [![license](https://img.shields.io/github/license/cosmos/cosmos-sdk.svg)](https://github.com/cosmos/modules/blob/master/LICENSE) 6 | 7 | **Note**: This repository is meant to house modules that are created outside of the [Cosmos-SDK](https://github.com/cosmos/cosmos-sdk) repository. 8 | 9 | **Note**: Requires [Go 1.13+](https://golang.org/dl/) 10 | 11 | ## Quick Start 12 | 13 | To learn how the SDK works from a high-level perspective, go to the [SDK Intro](https://docs.cosmos.network/master/intro/overview.html). 14 | 15 | If you want to get started quickly and learn how to build on top of the SDK, please follow the [SDK Application Tutorial](https://github.com/cosmos/sdk-tutorials). You can also fork the tutorial's repo to get started building your own Cosmos SDK application. 16 | 17 | For more, please go to the [Cosmos SDK Docs](https://github.com/cosmos/cosmos-sdk/docs/README.md) 18 | 19 | To find out more about the Cosmos-SDK, you can find documentation [here](https://cosmos.network/docs/). 20 | 21 | This repo organizes modules into 3 subfolders: 22 | 23 | - `stable/`: this folder houses modules that are stable, production-ready, and well-maintained. 24 | - `incubator/`: this folder houses modules that are buildable but makes no guarantees on stability or production-readiness. Once a module meets all requirements specified in [contributing guidelines](./CONTRIBUTING.md), the owners can make a PR to move module into `stable/` folder. Must be approved by at least one `modules` maintainer for the module to be moved. 25 | - `inactive/`: Any stale module from the previous 2 folders may be moved to the `inactive` folder if it is no longer being maintained by its owners. `modules` maintainers reserve the right to move a module into this folder after public discussion in an issue and a specified grace period for module owners to restart work on module. 26 | 27 | Any changes to where modules are located will only happen on major releases of the `modules` repo to ensure we only break import paths on major releases. 28 | 29 | ### Modules maintainers 30 | 31 | While each individual module will be owned and maintained by the individual contributors of that module, there will need to be maintainers of the `modules` repo overall to coordinate moving modules between the different folders and enforcing the requirements for inclusion in the `modules` repo. 32 | 33 | For now, the maintainers of the `modules` repo will be the SDK team but we intend to eventually expand this responsibility to other members of the Cosmos community. 34 | -------------------------------------------------------------------------------- /cosmos-sdk-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmos/modules/8c1862235a75285287e7919c23c8da353bb38b82/cosmos-sdk-image.jpg -------------------------------------------------------------------------------- /incubator/faucet/Makefile_Sample: -------------------------------------------------------------------------------- 1 | PACKAGES=$(shell go list ./... | grep -v '/simulation') 2 | 3 | VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//') 4 | COMMIT := $(shell git log -1 --format='%H') 5 | 6 | ldflags = -X github.com/cosmos/cosmos-sdk/version.Name=NameService \ 7 | -X github.com/cosmos/cosmos-sdk/version.ServerName=nsd \ 8 | -X github.com/cosmos/cosmos-sdk/version.ClientName=nscli \ 9 | -X github.com/cosmos/cosmos-sdk/version.Version=$(VERSION) \ 10 | -X github.com/cosmos/cosmos-sdk/version.Commit=$(COMMIT) 11 | 12 | BUILD_FLAGS := -ldflags '$(ldflags)' 13 | 14 | include Makefile.ledger 15 | all: install 16 | 17 | install: go.sum 18 | go install -mod=readonly $(BUILD_FLAGS) ./cmd/nsd 19 | go install -mod=readonly $(BUILD_FLAGS) ./cmd/nscli 20 | 21 | installWithFaucet: go.sum 22 | go install -mod=readonly $(BUILD_FLAGS) -tags faucet ./cmd/nsd 23 | go install -mod=readonly $(BUILD_FLAGS) -tags faucet ./cmd/nscli 24 | 25 | go.sum: go.mod 26 | @echo "--> Ensure dependencies have not been modified" 27 | GO111MODULE=on go mod verify 28 | 29 | test: 30 | @go test -mod=readonly $(PACKAGES) 31 | -------------------------------------------------------------------------------- /incubator/faucet/README.md: -------------------------------------------------------------------------------- 1 | # Faucet Module 2 | 3 | This module will enable mint function. Every address can mint 100(bonded tokens) in every 24 hours by sending a mint message. 4 | 5 | For security consideration, you can add this module to your project as you want, but this module would *NOT* be active by default. unless you active it manually by adding `"-tags faucet"` when you build or install. 6 | 7 | 这个水龙头模块提供铸币功能,每一个地址都可以发送mint消息为自己铸造一定数量的币,时间间隔为24小时。 8 | 出于安全考虑,你可以随意将本模块加入到你的项目代码中,但是默认是不会生效的,除非在编译的时候加上`-tags faucet`手动激活这个模块 9 | 10 | ## Developer Tutorial 11 | 12 | Step 1: Import to your app.go 13 | ```go 14 | import ( 15 | "github.com/cosmos/modules/incubator/faucet" 16 | ) 17 | ``` 18 | 19 | Step 2: Declare faucet module and permission in app.go 20 | ```go 21 | ModuleBasics = module.NewBasicManager( 22 | ..., // the official basic modules 23 | 24 | faucet.AppModule{}, // add faucet module 25 | ) 26 | // account permissions 27 | maccPerms = map[string][]string{ 28 | staking.BondedPoolName: {supply.Burner, supply.Staking}, 29 | staking.NotBondedPoolName: {supply.Burner, supply.Staking}, 30 | faucet.ModuleName: {supply.Minter}, // add permissions for faucet 31 | } 32 | 33 | type nameServiceApp struct { 34 | *bam.BaseApp 35 | cdc *codec.Codec 36 | 37 | // Other Keepers ... ... 38 | 39 | // Declare faucet keeper here 40 | faucetKeeper faucet.Keeper 41 | 42 | // Module Manager 43 | mm *module.Manager 44 | 45 | // simulation manager 46 | sm *module.SimulationManager 47 | } 48 | ``` 49 | 50 | Step 3: Initialize faucet keeper and faucet module in func NewNameserviceApp() in app.go 51 | ```go 52 | keys := sdk.NewKVStoreKeys( 53 | bam.MainStoreKey, 54 | auth.StoreKey, 55 | staking.StoreKey, 56 | supply.StoreKey, 57 | distr.StoreKey, 58 | slashing.StoreKey, 59 | params.StoreKey, 60 | faucet.StoreKey) // add faucet key 61 | 62 | ... // some stuff in between 63 | 64 | app.faucetKeeper = faucet.NewKeeper( 65 | app.supplyKeeper, 66 | app.stakingKeeper, 67 | 10 * 1000000, // amount for mint 68 | 24 * time.Hour, // rate limit by time 69 | keys[faucet.StoreKey], 70 | app.cdc,) 71 | 72 | app.mm = module.NewManager( 73 | ..., // other modules 74 | 75 | faucet.NewAppModule(app.faucetKeeper), // add faucet module 76 | 77 | ) 78 | ``` 79 | 80 | Step 4: Enable faucet in [Makefile](Makefile_Sample) 81 | ``` 82 | installWithFaucet: go.sum 83 | go install -mod=readonly $(BUILD_FLAGS) -tags faucet ./cmd/nsd 84 | go install -mod=readonly $(BUILD_FLAGS) -tags faucet ./cmd/nscli 85 | ``` 86 | 87 | Step 5: Build your app 88 | ``` 89 | make installWithFaucet 90 | ``` 91 | 92 | Step 6: Initialize faucet and publish to blockchain. 93 | 94 | Create an account with default password "12345678", and publish it to blockchain, therefore others are able to load this account to mint coins. 95 | 96 | ``` 97 | nscli tx faucet publish --from faucet --chain-id test 98 | { 99 | "chain_id": "test", 100 | "account_number": "3", 101 | "sequence": "2", 102 | "fee": { 103 | "amount": [], 104 | "gas": "200000" 105 | }, 106 | "msgs": [ 107 | { 108 | "type": "faucet/FaucetKey", 109 | "value": { 110 | "Sender": "cosmos14pkakt8apdm0e49tzp6gy3lwe8u04ajched5qm", 111 | "Armor": "-----BEGIN TENDERMINT KEY INFO-----\ntype: Info\nversion: 0.0.0\n\nZA2tFT0KBHBpbmcSJuta6YchA8yOxBXjUwLzUeBxeECHpLU2GwILK/7OVbMF6uiX\nl/PNGiXhsPebIP3XBLEM0VlX6/whk4LtlqqvYLOduCLGh1yS0OE4SQFWIglzZWNw\nMjU2azE=\n=WAST\n-----END TENDERMINT KEY INFO-----" 112 | } 113 | } 114 | ], 115 | "memo": "" 116 | } 117 | ``` 118 | 119 | Congratulations, you are ready to mint 120 | 121 | ## Usage / 用法 122 | 123 | 1: Initialize faucet 124 | 125 | ``` 126 | nscli tx faucet init --chain-id test 127 | The faucet has been loaded successfully. 128 | ``` 129 | 130 | 2: Mint coins. 131 | 132 | ``` 133 | iMac:~ liangping$ nscli tx faucet mintfor cosmos17l3gw079cn5x9d3pqa0jk0xhrw2mt358xvw555 --from faucet --chain-id test -y 134 | { 135 | "height": "0", 136 | "txhash": "40F2AB8AD75B39532622302A71CB84523847D2E43D36B185E0CE65CE60208AB0", 137 | "raw_log": "[]" 138 | } 139 | iMac:~ liangping$ nscli query account cosmos17l3gw079cn5x9d3pqa0jk0xhrw2mt358xvw555 --chain-id test 140 | { 141 | "type": "cosmos-sdk/Account", 142 | "value": { 143 | "address": "cosmos17l3gw079cn5x9d3pqa0jk0xhrw2mt358xvw555", 144 | "coins": [ 145 | { 146 | "denom": "stake", 147 | "amount": "100000000" 148 | } 149 | ], 150 | "public_key": "", 151 | "account_number": 7, 152 | "sequence": 0 153 | } 154 | } 155 | 156 | ``` 157 | 158 | Also, you are able to mint for yourself if your address has actived/existed on blockchain. 159 | ``` 160 | $ nscli tx faucet mint --from YOUR_OTHER_ACCOUNT --chain-id test -y 161 | { 162 | "height": "0", 163 | "txhash": "40F2AB8AD75B39532622302A71CB84523847D2E43D36B185E0CE65CE60208AB0", 164 | "raw_log": "[]" 165 | } 166 | ``` 167 | 168 | ## Compatible Version 169 | 170 | cosmos-sdk v0.38.0 or above 171 | 172 | ## Contact Us 173 | 174 | Author: liangping from [Ping.pub](https://ping.pub) 175 | 176 | 18786721@qq.com 177 | 178 | If you like this module, welcome to delegate to Ping.pub on [Cosmoshub](https://cosmos.ping.pub), [IRISHub](https://iris.ping.pub), [KAVA](https://kava.ping.pub). 179 | -------------------------------------------------------------------------------- /incubator/faucet/alias.go: -------------------------------------------------------------------------------- 1 | package faucet 2 | 3 | import ( 4 | "github.com/cosmos/modules/incubator/faucet/internal/keeper" 5 | "github.com/cosmos/modules/incubator/faucet/internal/types" 6 | ) 7 | 8 | const ( 9 | ModuleName = types.ModuleName 10 | RouterKey = types.RouterKey 11 | StoreKey = types.StoreKey 12 | 13 | MAINNET = "mainnet" 14 | TESTNET = "testnet" 15 | ) 16 | 17 | var ( 18 | NewKeeper = keeper.NewKeeper 19 | NewQuerier = keeper.NewQuerier 20 | ModuleCdc = types.ModuleCdc 21 | RegisterCodec = types.RegisterCodec 22 | ) 23 | 24 | type ( 25 | Keeper = keeper.Keeper 26 | ) 27 | -------------------------------------------------------------------------------- /incubator/faucet/client/cli/tx.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "github.com/cosmos/cosmos-sdk/crypto/keys" 8 | "github.com/spf13/viper" 9 | "time" 10 | 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/cosmos/cosmos-sdk/client" 14 | "github.com/cosmos/cosmos-sdk/client/context" 15 | "github.com/cosmos/cosmos-sdk/client/flags" 16 | "github.com/cosmos/cosmos-sdk/codec" 17 | sdk "github.com/cosmos/cosmos-sdk/types" 18 | "github.com/cosmos/cosmos-sdk/x/auth" 19 | "github.com/cosmos/cosmos-sdk/x/auth/client/utils" 20 | "github.com/cosmos/modules/incubator/faucet/internal/types" 21 | ) 22 | 23 | // GetTxCmd return faucet sub-command for tx 24 | func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { 25 | faucetTxCmd := &cobra.Command{ 26 | Use: types.ModuleName, 27 | Short: "faucet transaction subcommands", 28 | DisableFlagParsing: true, 29 | SuggestionsMinimumDistance: 2, 30 | RunE: client.ValidateCmd, 31 | } 32 | 33 | faucetTxCmd.AddCommand(flags.PostCommands( 34 | GetCmdMint(cdc), 35 | GetCmdMintFor(cdc), 36 | GetCmdInitial(cdc), 37 | GetPublishKey(cdc), 38 | )...) 39 | 40 | return faucetTxCmd 41 | } 42 | 43 | // GetCmdWithdraw is the CLI command for mining coin 44 | func GetCmdMint(cdc *codec.Codec) *cobra.Command { 45 | return &cobra.Command{ 46 | Use: "mint", 47 | Short: "mint coin to sender address", 48 | Args: cobra.ExactArgs(0), 49 | RunE: func(cmd *cobra.Command, args []string) error { 50 | inBuf := bufio.NewReader(cmd.InOrStdin()) 51 | cliCtx := context.NewCLIContext().WithCodec(cdc) 52 | 53 | txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) 54 | 55 | msg := types.NewMsgMint(cliCtx.GetFromAddress(), cliCtx.GetFromAddress(), time.Now().Unix()) 56 | err := msg.ValidateBasic() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) 62 | }, 63 | } 64 | } 65 | 66 | // GetCmdWithdraw is the CLI command for mining coin 67 | func GetCmdMintFor(cdc *codec.Codec) *cobra.Command { 68 | return &cobra.Command{ 69 | Use: "mintfor [address]", 70 | Short: "mint coin for new address", 71 | Args: cobra.ExactArgs(1), 72 | RunE: func(cmd *cobra.Command, args []string) error { 73 | inBuf := bufio.NewReader(cmd.InOrStdin()) 74 | cliCtx := context.NewCLIContext().WithCodec(cdc) 75 | 76 | txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) 77 | 78 | address, _ := sdk.AccAddressFromBech32(args[0]) 79 | 80 | msg := types.NewMsgMint(cliCtx.GetFromAddress(), address, time.Now().Unix()) 81 | err := msg.ValidateBasic() 82 | if err != nil { 83 | return err 84 | } 85 | 86 | return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) 87 | }, 88 | } 89 | } 90 | 91 | func GetPublishKey(cdc *codec.Codec) *cobra.Command { 92 | return &cobra.Command{ 93 | Use: "publish", 94 | Short: "Publish current account as an public faucet. Do NOT add many coins in this account", 95 | Args: cobra.ExactArgs(0), 96 | RunE: func(cmd *cobra.Command, args []string) error { 97 | inBuf := bufio.NewReader(cmd.InOrStdin()) 98 | cliCtx := context.NewCLIContext().WithCodec(cdc) 99 | 100 | txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) 101 | kb, errkb := keys.NewKeyring(sdk.KeyringServiceName(), viper.GetString(flags.FlagKeyringBackend), viper.GetString(flags.FlagHome), inBuf) 102 | if errkb != nil { 103 | return errkb 104 | } 105 | 106 | // check local key 107 | armor, err := kb.Export(cliCtx.GetFromName()) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | msg := types.NewMsgFaucetKey(cliCtx.GetFromAddress(), armor) 113 | err = msg.ValidateBasic() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) 119 | }, 120 | } 121 | } 122 | 123 | func GetCmdInitial(cdc *codec.Codec) *cobra.Command { 124 | return &cobra.Command{ 125 | Use: "init", 126 | Short: "Initialize mint key for faucet", 127 | Args: cobra.ExactArgs(0), 128 | RunE: func(cmd *cobra.Command, args []string) error { 129 | inBuf := bufio.NewReader(cmd.InOrStdin()) 130 | cliCtx := context.NewCLIContext().WithCodec(cdc) 131 | 132 | //txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) 133 | kb, errkb := keys.NewKeyring(sdk.KeyringServiceName(), viper.GetString(flags.FlagKeyringBackend), viper.GetString(flags.FlagHome), inBuf) 134 | if errkb != nil { 135 | return errkb 136 | } 137 | 138 | // check local key 139 | _, err := kb.Get(types.ModuleName) 140 | if err == nil { 141 | return errors.New("faucet existed") 142 | } 143 | 144 | // fetch from chain 145 | res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/key", types.ModuleName), nil) 146 | if err != nil { 147 | return nil 148 | } 149 | var rkey types.FaucetKey 150 | cdc.MustUnmarshalJSON(res, &rkey) 151 | 152 | if len(rkey.Armor) == 0 { 153 | return errors.New("Faucet key has not published") 154 | } 155 | // import to keybase 156 | kb.Import(types.ModuleName, rkey.Armor) 157 | fmt.Println("The faucet has been loaded successfully.") 158 | return nil 159 | 160 | }, 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /incubator/faucet/client/rest/rest.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | 6 | "github.com/cosmos/cosmos-sdk/client/context" 7 | ) 8 | 9 | // RegisterRoutes - Central function to define routes that get registered by the main application 10 | func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, storeName string) { 11 | r.HandleFunc("/faucet/mint", mintHandlerFn(cliCtx)).Methods("POST") 12 | } 13 | -------------------------------------------------------------------------------- /incubator/faucet/client/rest/tx.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/client/context" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | "github.com/cosmos/cosmos-sdk/types/rest" 7 | "github.com/cosmos/cosmos-sdk/x/auth/client/utils" 8 | "github.com/cosmos/modules/incubator/faucet/internal/types" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // PostProposalReq defines the properties of a proposal request's body. 14 | type PostMintReq struct { 15 | BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` 16 | Minter string `json:"minter" yaml:"minter"` // Address of the minter 17 | } 18 | 19 | func mintHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { 20 | return func(w http.ResponseWriter, r *http.Request) { 21 | var req PostMintReq 22 | if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { 23 | rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request") 24 | return 25 | } 26 | 27 | baseReq := req.BaseReq.Sanitize() 28 | if !baseReq.ValidateBasic(w) { 29 | return 30 | } 31 | 32 | sender, err := sdk.AccAddressFromBech32(baseReq.From) 33 | if err != nil { 34 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 35 | return 36 | } 37 | 38 | minter, err := sdk.AccAddressFromBech32(req.Minter) 39 | if err != nil { 40 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 41 | return 42 | } 43 | 44 | // create the message 45 | msg := types.NewMsgMint(sender, minter, time.Now().Unix()) 46 | err = msg.ValidateBasic() 47 | if err != nil { 48 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 49 | return 50 | } 51 | 52 | utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg}) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /incubator/faucet/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cosmos/modules/incubator/faucet 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/cosmos/cosmos-sdk v0.38.0 7 | github.com/gorilla/mux v1.7.4 8 | github.com/spf13/cobra v0.0.5 9 | github.com/spf13/viper v1.6.2 10 | github.com/tendermint/go-amino v0.15.1 11 | github.com/tendermint/tendermint v0.33.0 12 | github.com/tendermint/tm-db v0.4.0 13 | ) 14 | -------------------------------------------------------------------------------- /incubator/faucet/handler.go: -------------------------------------------------------------------------------- 1 | package faucet 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cosmos/modules/incubator/faucet/internal/types" 6 | 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 9 | ) 10 | 11 | // NewHandler returns a handler for "faucet" type messages. 12 | func NewHandler(keeper Keeper) sdk.Handler { 13 | return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { 14 | switch msg := msg.(type) { 15 | case types.MsgMint: 16 | return handleMsgMint(ctx, keeper, msg) 17 | case types.MsgFaucetKey: 18 | return handleMsgFaucetKey(ctx, keeper, msg) 19 | default: 20 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, fmt.Sprintf("Unrecognized faucet Msg type: %v", msg.Type())) 21 | } 22 | } 23 | } 24 | 25 | // Handle a message to Mint 26 | func handleMsgMint(ctx sdk.Context, keeper Keeper, msg types.MsgMint) (*sdk.Result, error) { 27 | 28 | keeper.Logger(ctx).Info("received mint message: %s", msg) 29 | err := keeper.MintAndSend(ctx, msg.Minter, msg.Time) 30 | if err != nil { 31 | return nil, sdkerrors.Wrap(err, fmt.Sprintf(",in [%v] hours", keeper.Limit.Hours())) 32 | } 33 | 34 | return &sdk.Result{}, nil // return 35 | } 36 | 37 | // Handle a message to Mint 38 | func handleMsgFaucetKey(ctx sdk.Context, keeper Keeper, msg types.MsgFaucetKey) (*sdk.Result, error) { 39 | 40 | keeper.Logger(ctx).Info("received faucet message: %s", msg) 41 | if keeper.HasFaucetKey(ctx) { 42 | return nil, types.ErrFaucetKeyExisted 43 | } 44 | 45 | keeper.SetFaucetKey(ctx, msg.Armor) 46 | 47 | return &sdk.Result{}, nil // return 48 | } 49 | -------------------------------------------------------------------------------- /incubator/faucet/internal/keeper/keeper.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cosmos/cosmos-sdk/codec" 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | "github.com/cosmos/modules/incubator/faucet/internal/types" 8 | "github.com/tendermint/tendermint/libs/log" 9 | "time" 10 | ) 11 | 12 | const FaucetStoreKey = "DefaultFaucetStoreKey" 13 | 14 | // Keeper maintains the link to storage and exposes getter/setter methods for the various parts of the state machine 15 | type Keeper struct { 16 | SupplyKeeper types.SupplyKeeper 17 | StakingKeeper types.StakingKeeper 18 | amount int64 // set default amount for each mint. 19 | Limit time.Duration // rate limiting for mint, etc 24 * time.Hours 20 | storeKey sdk.StoreKey // Unexposed key to access store from sdk.Context 21 | cdc *codec.Codec // The wire codec for binary encoding/decoding. 22 | } 23 | 24 | // NewKeeper creates new instances of the Faucet Keeper 25 | func NewKeeper( 26 | supplyKeeper types.SupplyKeeper, 27 | stakingKeeper types.StakingKeeper, 28 | amount int64, 29 | rateLimit time.Duration, 30 | storeKey sdk.StoreKey, 31 | cdc *codec.Codec) Keeper { 32 | return Keeper{ 33 | SupplyKeeper: supplyKeeper, 34 | StakingKeeper: stakingKeeper, 35 | amount: amount, 36 | Limit: rateLimit, 37 | storeKey: storeKey, 38 | cdc: cdc, 39 | } 40 | } 41 | 42 | // Logger returns a module-specific logger. 43 | func (k Keeper) Logger(ctx sdk.Context) log.Logger { 44 | return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) 45 | } 46 | 47 | // MintAndSend mint coins and send to minter. 48 | func (k Keeper) MintAndSend(ctx sdk.Context, minter sdk.AccAddress, mintTime int64) error { 49 | 50 | mining := k.getMining(ctx, minter) 51 | 52 | // refuse mint in 24 hours 53 | if k.isPresent(ctx, minter) && 54 | time.Unix(mining.LastTime, 0).Add(k.Limit).UTC().After(time.Unix(mintTime, 0)) { 55 | return types.ErrWithdrawTooOften 56 | } 57 | 58 | denom := k.StakingKeeper.BondDenom(ctx) 59 | newCoin := sdk.NewCoin(denom, sdk.NewInt(k.amount)) 60 | mining.Total = mining.Total.Add(newCoin) 61 | mining.LastTime = mintTime 62 | k.setMining(ctx, minter, mining) 63 | 64 | k.Logger(ctx).Info("Mint coin: %s", newCoin) 65 | 66 | err := k.SupplyKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(newCoin)) 67 | if err != nil { 68 | return err 69 | } 70 | err = k.SupplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, minter, sdk.NewCoins(newCoin)) 71 | if err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | 77 | func (k Keeper) getMining(ctx sdk.Context, minter sdk.AccAddress) types.Mining { 78 | store := ctx.KVStore(k.storeKey) 79 | if !k.isPresent(ctx, minter) { 80 | denom := k.StakingKeeper.BondDenom(ctx) 81 | return types.NewMining(minter, sdk.NewCoin(denom, sdk.NewInt(0))) 82 | } 83 | bz := store.Get(minter.Bytes()) 84 | var mining types.Mining 85 | k.cdc.MustUnmarshalBinaryBare(bz, &mining) 86 | return mining 87 | } 88 | 89 | func (k Keeper) setMining(ctx sdk.Context, minter sdk.AccAddress, mining types.Mining) { 90 | if mining.Minter.Empty() { 91 | return 92 | } 93 | if !mining.Total.IsPositive() { 94 | return 95 | } 96 | store := ctx.KVStore(k.storeKey) 97 | store.Set(minter.Bytes(), k.cdc.MustMarshalBinaryBare(mining)) 98 | } 99 | 100 | // IsPresent check if the name is present in the store or not 101 | func (k Keeper) isPresent(ctx sdk.Context, minter sdk.AccAddress) bool { 102 | store := ctx.KVStore(k.storeKey) 103 | return store.Has(minter.Bytes()) 104 | } 105 | 106 | func (k Keeper) GetFaucetKey(ctx sdk.Context) types.FaucetKey { 107 | store := ctx.KVStore(k.storeKey) 108 | bz := store.Get([]byte(FaucetStoreKey)) 109 | var faucet types.FaucetKey 110 | k.cdc.MustUnmarshalBinaryBare(bz, &faucet) 111 | return faucet 112 | } 113 | 114 | func (k Keeper) SetFaucetKey(ctx sdk.Context, armor string) { 115 | store := ctx.KVStore(k.storeKey) 116 | faucet := types.NewFaucetKey(armor) 117 | store.Set([]byte(FaucetStoreKey), k.cdc.MustMarshalBinaryBare(faucet)) 118 | } 119 | 120 | func (k Keeper) HasFaucetKey(ctx sdk.Context) bool { 121 | store := ctx.KVStore(k.storeKey) 122 | return store.Has([]byte(FaucetStoreKey)) 123 | } 124 | -------------------------------------------------------------------------------- /incubator/faucet/internal/keeper/querier.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/codec" 5 | abci "github.com/tendermint/tendermint/abci/types" 6 | 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 9 | ) 10 | 11 | // query endpoints supported by the nameservice Querier 12 | const ( 13 | QueryFaucetKey = "key" 14 | ) 15 | 16 | // NewQuerier is the module level router for state queries 17 | func NewQuerier(keeper Keeper) sdk.Querier { 18 | return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) { 19 | switch path[0] { 20 | case QueryFaucetKey: 21 | return queryFaucetKey(ctx, nil, req, keeper) 22 | default: 23 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown faucet query endpoint") 24 | } 25 | } 26 | } 27 | 28 | func queryFaucetKey(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) { 29 | value := keeper.GetFaucetKey(ctx) 30 | res, err := codec.MarshalJSONIndent(keeper.cdc, value) 31 | if err != nil { 32 | return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) 33 | } 34 | 35 | return res, nil 36 | } 37 | -------------------------------------------------------------------------------- /incubator/faucet/internal/types/codec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/codec" 5 | ) 6 | 7 | // ModuleCdc is the codec for the module 8 | var ModuleCdc = codec.New() 9 | 10 | func init() { 11 | RegisterCodec(ModuleCdc) 12 | } 13 | 14 | // RegisterCodec registers concrete types on the Amino codec 15 | func RegisterCodec(cdc *codec.Codec) { 16 | cdc.RegisterConcrete(MsgMint{}, "faucet/Mint", nil) 17 | cdc.RegisterConcrete(MsgFaucetKey{}, "faucet/FaucetKey", nil) 18 | } 19 | -------------------------------------------------------------------------------- /incubator/faucet/internal/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 5 | ) 6 | 7 | var ( 8 | // ErrWithdrawTooOften withdraw too often 9 | ErrWithdrawTooOften = sdkerrors.Register(ModuleName, 100, "Each address can withdraw only once") 10 | ErrFaucetKeyEmpty = sdkerrors.Register(ModuleName, 101, "Armor should Not be empty.") 11 | ErrFaucetKeyExisted = sdkerrors.Register(ModuleName, 102, "Faucet key existed") 12 | ) 13 | -------------------------------------------------------------------------------- /incubator/faucet/internal/types/expected_keepers.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | "github.com/cosmos/cosmos-sdk/x/supply/exported" 6 | ) 7 | 8 | // SupplyKeeper is required for mining coin 9 | type SupplyKeeper interface { 10 | MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error 11 | SendCoinsFromModuleToAccount( 12 | ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, 13 | ) error 14 | GetSupply(ctx sdk.Context) (supply exported.SupplyI) 15 | } 16 | 17 | // StakingKeeper is required for getting Denom 18 | type StakingKeeper interface { 19 | BondDenom(ctx sdk.Context) string 20 | } 21 | -------------------------------------------------------------------------------- /incubator/faucet/internal/types/key.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | // ModuleName is the name of the module 5 | ModuleName = "faucet" 6 | 7 | // StoreKey to be used when creating the KVStore 8 | StoreKey = ModuleName 9 | ) 10 | -------------------------------------------------------------------------------- /incubator/faucet/internal/types/msgs.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 6 | ) 7 | 8 | // RouterKey is the module name router key 9 | const RouterKey = ModuleName // this was defined in your key.go file 10 | 11 | // MsgMint defines a mint message 12 | type MsgMint struct { 13 | Sender sdk.AccAddress 14 | Minter sdk.AccAddress 15 | Time int64 16 | } 17 | 18 | // NewMsgMint is a constructor function for NewMsgMint 19 | func NewMsgMint(sender sdk.AccAddress, minter sdk.AccAddress, mTime int64) MsgMint { 20 | return MsgMint{Sender: sender, Minter: minter, Time: mTime} 21 | } 22 | 23 | // Route should return the name of the module 24 | func (msg MsgMint) Route() string { return RouterKey } 25 | 26 | // Type should return the action 27 | func (msg MsgMint) Type() string { return "mint" } 28 | 29 | // ValidateBasic runs stateless checks on the message 30 | func (msg MsgMint) ValidateBasic() error { 31 | if msg.Minter.Empty() { 32 | return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Minter.String()) 33 | } 34 | if msg.Sender.Empty() { 35 | return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Sender.String()) 36 | } 37 | return nil 38 | } 39 | 40 | // GetSignBytes encodes the message for signing 41 | func (msg MsgMint) GetSignBytes() []byte { 42 | return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) 43 | } 44 | 45 | // GetSigners defines whose signature is required 46 | func (msg MsgMint) GetSigners() []sdk.AccAddress { 47 | return []sdk.AccAddress{msg.Sender} 48 | } 49 | 50 | // MsgMint defines a mint message 51 | type MsgFaucetKey struct { 52 | Sender sdk.AccAddress 53 | Armor string 54 | } 55 | 56 | // NewMsgFaucetKey is a constructor function for MsgFaucetKey 57 | func NewMsgFaucetKey(sender sdk.AccAddress, armor string) MsgFaucetKey { 58 | return MsgFaucetKey{Sender: sender, Armor: armor} 59 | } 60 | 61 | // Route should return the name of the module 62 | func (msg MsgFaucetKey) Route() string { return RouterKey } 63 | 64 | // Type should return the action 65 | func (msg MsgFaucetKey) Type() string { return "faucet-key" } 66 | 67 | // ValidateBasic runs stateless checks on the message 68 | func (msg MsgFaucetKey) ValidateBasic() error { 69 | if msg.Sender.Empty() { 70 | return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Sender.String()) 71 | } 72 | if len(msg.Armor) == 0 { 73 | return ErrFaucetKeyEmpty 74 | } 75 | return nil 76 | } 77 | 78 | // GetSignBytes encodes the message for signing 79 | func (msg MsgFaucetKey) GetSignBytes() []byte { 80 | return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) 81 | } 82 | 83 | // GetSigners defines whose signature is required 84 | func (msg MsgFaucetKey) GetSigners() []sdk.AccAddress { 85 | return []sdk.AccAddress{msg.Sender} 86 | } 87 | -------------------------------------------------------------------------------- /incubator/faucet/internal/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | "strings" 7 | ) 8 | 9 | // Mining is a struct that contains all the metadata of a mint 10 | type Mining struct { 11 | Minter sdk.AccAddress `json:"Minter"` 12 | LastTime int64 `json:"LastTime"` 13 | Total sdk.Coin `json:"Total"` 14 | } 15 | 16 | // NewMining returns a new Mining 17 | func NewMining(minter sdk.AccAddress, coin sdk.Coin) Mining { 18 | return Mining{ 19 | Minter: minter, 20 | LastTime: 0, 21 | Total: coin, 22 | } 23 | } 24 | 25 | // GetMinter get minter of mining 26 | func (w Mining) GetMinter() sdk.AccAddress { 27 | return w.Minter 28 | } 29 | 30 | // implement fmt.Stringer 31 | func (w Mining) String() string { 32 | return strings.TrimSpace(fmt.Sprintf(`Minter: %s, Time: %s, Total: %s`, w.Minter, w.LastTime, w.Total)) 33 | } 34 | 35 | type FaucetKey struct { 36 | Armor string `json:" armor"` 37 | } 38 | 39 | // NewFaucetKey create a instance 40 | func NewFaucetKey(armor string) FaucetKey { 41 | return FaucetKey{ 42 | Armor: armor, 43 | } 44 | } 45 | 46 | // implement fmt.Stringer 47 | func (f FaucetKey) String() string { 48 | return strings.TrimSpace(fmt.Sprintf(`Armor: %s`, f.Armor)) 49 | } 50 | -------------------------------------------------------------------------------- /incubator/faucet/module.go: -------------------------------------------------------------------------------- 1 | package faucet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gorilla/mux" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/cosmos/cosmos-sdk/client/context" 10 | "github.com/cosmos/cosmos-sdk/codec" 11 | "github.com/cosmos/cosmos-sdk/types/module" 12 | "github.com/cosmos/modules/incubator/faucet/client/cli" 13 | "github.com/cosmos/modules/incubator/faucet/client/rest" 14 | 15 | sdk "github.com/cosmos/cosmos-sdk/types" 16 | abci "github.com/tendermint/tendermint/abci/types" 17 | ) 18 | 19 | // type check to ensure the interface is properly implemented 20 | var ( 21 | _ module.AppModule = AppModule{} 22 | _ module.AppModuleBasic = AppModuleBasic{} 23 | ) 24 | 25 | // app module Basics object 26 | type AppModuleBasic struct{} 27 | 28 | func (AppModuleBasic) Name() string { 29 | return ModuleName 30 | } 31 | 32 | func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { 33 | RegisterCodec(cdc) 34 | } 35 | 36 | func (AppModuleBasic) DefaultGenesis() json.RawMessage { 37 | return nil 38 | } 39 | 40 | // Validation check of the Genesis 41 | func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { 42 | return nil 43 | } 44 | 45 | // Register rest routes 46 | func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { 47 | if profile == TESTNET { 48 | rest.RegisterRoutes(ctx, rtr, StoreKey) 49 | } 50 | } 51 | 52 | // Get the root query command of this module 53 | func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { 54 | return nil 55 | } 56 | 57 | // Get the root tx command of this module 58 | func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { 59 | if profile == TESTNET { 60 | return cli.GetTxCmd(StoreKey, cdc) 61 | } else { 62 | return nil 63 | } 64 | } 65 | 66 | type AppModule struct { 67 | AppModuleBasic 68 | keeper Keeper 69 | } 70 | 71 | // NewAppModule creates a new AppModule Object 72 | func NewAppModule(k Keeper) AppModule { 73 | return AppModule{ 74 | AppModuleBasic: AppModuleBasic{}, 75 | keeper: k, 76 | } 77 | } 78 | 79 | func (AppModule) Name() string { 80 | return ModuleName 81 | } 82 | 83 | func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} 84 | 85 | func (am AppModule) Route() string { 86 | return RouterKey 87 | } 88 | 89 | func (am AppModule) NewHandler() sdk.Handler { 90 | if profile == TESTNET { 91 | return NewHandler(am.keeper) 92 | } else { 93 | return nil 94 | } 95 | } 96 | func (am AppModule) QuerierRoute() string { 97 | return ModuleName 98 | } 99 | 100 | func (am AppModule) NewQuerierHandler() sdk.Querier { 101 | return NewQuerier(am.keeper) // unimplement 102 | } 103 | 104 | func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} 105 | 106 | func (am AppModule) EndBlock(sdk.Context, abci.RequestEndBlock) []abci.ValidatorUpdate { 107 | return []abci.ValidatorUpdate{} 108 | } 109 | 110 | func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { 111 | return nil 112 | } 113 | 114 | func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /incubator/faucet/profile.go: -------------------------------------------------------------------------------- 1 | //+build !faucet 2 | 3 | package faucet 4 | 5 | // Faucet should not be added in mainnet version 6 | const profile = MAINNET 7 | -------------------------------------------------------------------------------- /incubator/faucet/profile_testnet.go: -------------------------------------------------------------------------------- 1 | // +build faucet 2 | 3 | package faucet 4 | 5 | // faucet shoud only be added to testnet. 6 | const profile = TESTNET 7 | -------------------------------------------------------------------------------- /incubator/nft/CONTRACT.md: -------------------------------------------------------------------------------- 1 | # Contract 2 | 3 | This document outlines the "soft contract" expected by the `CODEOWNERS` and maintainers 4 | of the `x/nft` module to follow and uphold. The agreed-upon members are expected 5 | to abide by the following: 6 | 7 | - Review and provide feedback on PRs that primarily impact or modify the `x/nft` 8 | module within 30 days. 9 | - Review and provide feedback on issues related to the `x/nft` module within 30 10 | days. 11 | - Maintain compatibility with the APIs, design philosophy, architecture and the 12 | broader set of tools (e.g. simulation) within the SDK. 13 | 14 | If the broader set of `CODEOWNERS` and maintainers of the SDK deem that the stated 15 | contract has been breached by a containing member of the `x/nft` module, that 16 | member may be removed with or without warning from the `CODEOWNERS`. If the entire 17 | set of owners and maintainers are deemed to break the contract, the `x/nft` module 18 | maybe removed with or without warning from the SDK. 19 | -------------------------------------------------------------------------------- /incubator/nft/Makefile: -------------------------------------------------------------------------------- 1 | PACKAGES_NOSIMULATION=$(shell go list ./...) 2 | BINDIR ?= $(GOPATH)/bin 3 | SIMAPP = ./app 4 | 5 | 6 | ############################################################################### 7 | ### Tests & Simulation ### 8 | ############################################################################### 9 | 10 | test: test-unit 11 | 12 | test-unit: 13 | @go test -mod=readonly $(PACKAGES_NOSIMULATION) -tags='ledger test_ledger_mock' 14 | 15 | .PHONY: test test-unit 16 | 17 | test-sim-nondeterminism: 18 | @echo "Running non-determinism test..." 19 | @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ 20 | -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h 21 | 22 | test-sim-custom-genesis-fast: 23 | @echo "Running custom genesis simulation..." 24 | @echo "By default, ${HOME}/.simapp/config/genesis.json will be used." 25 | @go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation -Genesis=${HOME}/.simapp/config/genesis.json \ 26 | -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v -timeout 24h 27 | 28 | test-sim-import-export: runsim 29 | @echo "Running application import/export simulation. This may take several minutes..." 30 | @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) 50 5 TestAppImportExport 31 | 32 | test-sim-after-import: runsim 33 | @echo "Running application simulation-after-import. This may take several minutes..." 34 | @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) 50 5 TestAppSimulationAfterImport 35 | 36 | test-sim-custom-genesis-multi-seed: runsim 37 | @echo "Running multi-seed custom genesis simulation..." 38 | @echo "By default, ${HOME}/.gaiad/config/genesis.json will be used." 39 | @$(BINDIR)/runsim -Genesis=${HOME}/.simapp/config/genesis.json -SimAppPkg=$(SIMAPP) 400 5 TestFullAppSimulation 40 | 41 | test-sim-multi-seed-long: runsim 42 | @echo "Running long multi-seed application simulation. This may take awhile!" 43 | @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) 500 50 TestFullAppSimulation 44 | 45 | test-sim-multi-seed-short: runsim 46 | @echo "Running short multi-seed application simulation. This may take awhile!" 47 | @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) 50 10 TestFullAppSimulation 48 | 49 | 50 | .PHONY: \ 51 | test-sim-nondeterminism \ 52 | test-sim-custom-genesis-fast \ 53 | test-sim-import-export \ 54 | test-sim-after-import \ 55 | test-sim-custom-genesis-multi-seed \ 56 | test-sim-multi-seed-short \ 57 | test-sim-multi-seed-long 58 | 59 | lint: 60 | @echo "--> Running linter" 61 | @golangci-lint run ./... 62 | find . -name '*.go' -type f -not -path "./vendor*" -not -path "*.git*" | xargs gofmt -d -s 63 | go mod verify 64 | .PHONY: lint 65 | 66 | ############################################################################### 67 | ### Tools & Dependencies ### 68 | ############################################################################### 69 | 70 | ### 71 | # Find OS and Go environment 72 | # GO contains the Go binary 73 | # FS contains the OS file separator 74 | ### 75 | ifeq ($(OS),Windows_NT) 76 | GO := $(shell where go.exe 2> NUL) 77 | FS := "\\" 78 | else 79 | GO := $(shell command -v go 2> /dev/null) 80 | FS := "/" 81 | endif 82 | 83 | ifeq ($(GO),) 84 | $(error could not find go. Is it in PATH? $(GO)) 85 | endif 86 | 87 | tools: runsim 88 | 89 | GOPATH ?= $(shell $(GO) env GOPATH) 90 | 91 | TOOLS_DESTDIR ?= $(GOPATH)/bin 92 | RUNSIM = $(TOOLS_DESTDIR)/runsim 93 | 94 | runsim: $(RUNSIM) 95 | $(RUNSIM): 96 | @echo "Installing runsim..." 97 | @(cd /tmp && go get github.com/cosmos/tools/cmd/runsim@v1.0.0) 98 | 99 | tools-clean: 100 | rm -f $(RUNSIM) 101 | rm -f tools-stamp -------------------------------------------------------------------------------- /incubator/nft/alias.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | // autogenerated code using github.com/rigelrozanski/multitool 3 | // aliases generated for the following subdirectories: 4 | // ALIASGEN: github.com/cosmos/modules/incubator/nft/keeper 5 | // ALIASGEN: github.com/cosmos/modules/incubator/nft/types 6 | package nft 7 | 8 | import ( 9 | "github.com/cosmos/modules/incubator/nft/keeper" 10 | "github.com/cosmos/modules/incubator/nft/types" 11 | ) 12 | 13 | const ( 14 | QuerySupply = keeper.QuerySupply 15 | QueryOwner = keeper.QueryOwner 16 | QueryOwnerByDenom = keeper.QueryOwnerByDenom 17 | QueryCollection = keeper.QueryCollection 18 | QueryDenoms = keeper.QueryDenoms 19 | QueryNFT = keeper.QueryNFT 20 | ModuleName = types.ModuleName 21 | StoreKey = types.StoreKey 22 | QuerierRoute = types.QuerierRoute 23 | RouterKey = types.RouterKey 24 | ) 25 | 26 | var ( 27 | // functions aliases 28 | RegisterInvariants = keeper.RegisterInvariants 29 | AllInvariants = keeper.AllInvariants 30 | SupplyInvariant = keeper.SupplyInvariant 31 | NewKeeper = keeper.NewKeeper 32 | NewQuerier = keeper.NewQuerier 33 | RegisterCodec = types.RegisterCodec 34 | NewCollection = types.NewCollection 35 | EmptyCollection = types.EmptyCollection 36 | NewCollections = types.NewCollections 37 | ErrInvalidCollection = types.ErrInvalidCollection 38 | ErrUnknownCollection = types.ErrUnknownCollection 39 | ErrInvalidNFT = types.ErrInvalidNFT 40 | ErrNFTAlreadyExists = types.ErrNFTAlreadyExists 41 | ErrUnknownNFT = types.ErrUnknownNFT 42 | ErrEmptyMetadata = types.ErrEmptyMetadata 43 | NewGenesisState = types.NewGenesisState 44 | DefaultGenesisState = types.DefaultGenesisState 45 | ValidateGenesis = types.ValidateGenesis 46 | GetCollectionKey = types.GetCollectionKey 47 | SplitOwnerKey = types.SplitOwnerKey 48 | GetOwnersKey = types.GetOwnersKey 49 | GetOwnerKey = types.GetOwnerKey 50 | NewMsgTransferNFT = types.NewMsgTransferNFT 51 | NewMsgEditNFTMetadata = types.NewMsgEditNFTMetadata 52 | NewMsgMintNFT = types.NewMsgMintNFT 53 | NewMsgBurnNFT = types.NewMsgBurnNFT 54 | NewBaseNFT = types.NewBaseNFT 55 | NewNFTs = types.NewNFTs 56 | NewIDCollection = types.NewIDCollection 57 | NewOwner = types.NewOwner 58 | NewQueryCollectionParams = types.NewQueryCollectionParams 59 | NewQueryBalanceParams = types.NewQueryBalanceParams 60 | NewQueryNFTParams = types.NewQueryNFTParams 61 | 62 | // variable aliases 63 | ModuleCdc = types.ModuleCdc 64 | EventTypeTransfer = types.EventTypeTransfer 65 | EventTypeEditNFTMetadata = types.EventTypeEditNFTMetadata 66 | EventTypeMintNFT = types.EventTypeMintNFT 67 | EventTypeBurnNFT = types.EventTypeBurnNFT 68 | AttributeValueCategory = types.AttributeValueCategory 69 | AttributeKeySender = types.AttributeKeySender 70 | AttributeKeyRecipient = types.AttributeKeyRecipient 71 | AttributeKeyOwner = types.AttributeKeyOwner 72 | AttributeKeyNFTID = types.AttributeKeyNFTID 73 | AttributeKeyNFTTokenURI = types.AttributeKeyNFTTokenURI 74 | AttributeKeyDenom = types.AttributeKeyDenom 75 | CollectionsKeyPrefix = types.CollectionsKeyPrefix 76 | OwnersKeyPrefix = types.OwnersKeyPrefix 77 | ) 78 | 79 | type ( 80 | Keeper = keeper.Keeper 81 | Collection = types.Collection 82 | Collections = types.Collections 83 | CollectionJSON = types.CollectionJSON 84 | GenesisState = types.GenesisState 85 | MsgTransferNFT = types.MsgTransferNFT 86 | MsgEditNFTMetadata = types.MsgEditNFTMetadata 87 | MsgMintNFT = types.MsgMintNFT 88 | MsgBurnNFT = types.MsgBurnNFT 89 | BaseNFT = types.BaseNFT 90 | NFTs = types.NFTs 91 | NFTJSON = types.NFTJSON 92 | IDCollection = types.IDCollection 93 | IDCollections = types.IDCollections 94 | Owner = types.Owner 95 | QueryCollectionParams = types.QueryCollectionParams 96 | QueryBalanceParams = types.QueryBalanceParams 97 | QueryNFTParams = types.QueryNFTParams 98 | ) 99 | -------------------------------------------------------------------------------- /incubator/nft/app/export.go: -------------------------------------------------------------------------------- 1 | package simapp 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | abci "github.com/tendermint/tendermint/abci/types" 8 | tmtypes "github.com/tendermint/tendermint/types" 9 | 10 | "github.com/cosmos/cosmos-sdk/codec" 11 | sdk "github.com/cosmos/cosmos-sdk/types" 12 | "github.com/cosmos/cosmos-sdk/x/slashing" 13 | "github.com/cosmos/cosmos-sdk/x/staking" 14 | "github.com/cosmos/cosmos-sdk/x/staking/exported" 15 | ) 16 | 17 | // ExportAppStateAndValidators exports the state of the application for a genesis 18 | // file. 19 | func (app *SimApp) ExportAppStateAndValidators( 20 | forZeroHeight bool, jailWhiteList []string, 21 | ) (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { 22 | 23 | // as if they could withdraw from the start of the next block 24 | ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) 25 | 26 | if forZeroHeight { 27 | app.prepForZeroHeightGenesis(ctx, jailWhiteList) 28 | } 29 | 30 | genState := app.mm.ExportGenesis(ctx) 31 | appState, err = codec.MarshalJSONIndent(app.cdc, genState) 32 | if err != nil { 33 | return nil, nil, err 34 | } 35 | 36 | validators = staking.WriteValidators(ctx, app.StakingKeeper) 37 | return appState, validators, nil 38 | } 39 | 40 | // prepare for fresh start at zero height 41 | // NOTE zero height genesis is a temporary feature which will be deprecated 42 | // in favour of export at a block height 43 | func (app *SimApp) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string) { 44 | applyWhiteList := false 45 | 46 | //Check if there is a whitelist 47 | if len(jailWhiteList) > 0 { 48 | applyWhiteList = true 49 | } 50 | 51 | whiteListMap := make(map[string]bool) 52 | 53 | for _, addr := range jailWhiteList { 54 | _, err := sdk.ValAddressFromBech32(addr) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | whiteListMap[addr] = true 59 | } 60 | 61 | /* Just to be safe, assert the invariants on current state. */ 62 | app.CrisisKeeper.AssertInvariants(ctx) 63 | 64 | /* Handle fee distribution state. */ 65 | 66 | // withdraw all validator commission 67 | app.StakingKeeper.IterateValidators(ctx, func(_ int64, val exported.ValidatorI) (stop bool) { 68 | _, _ = app.DistrKeeper.WithdrawValidatorCommission(ctx, val.GetOperator()) 69 | return false 70 | }) 71 | 72 | // withdraw all delegator rewards 73 | dels := app.StakingKeeper.GetAllDelegations(ctx) 74 | for _, delegation := range dels { 75 | _, _ = app.DistrKeeper.WithdrawDelegationRewards(ctx, delegation.DelegatorAddress, delegation.ValidatorAddress) 76 | } 77 | 78 | // clear validator slash events 79 | app.DistrKeeper.DeleteAllValidatorSlashEvents(ctx) 80 | 81 | // clear validator historical rewards 82 | app.DistrKeeper.DeleteAllValidatorHistoricalRewards(ctx) 83 | 84 | // set context height to zero 85 | height := ctx.BlockHeight() 86 | ctx = ctx.WithBlockHeight(0) 87 | 88 | // reinitialize all validators 89 | app.StakingKeeper.IterateValidators(ctx, func(_ int64, val exported.ValidatorI) (stop bool) { 90 | 91 | // donate any unwithdrawn outstanding reward fraction tokens to the community pool 92 | scraps := app.DistrKeeper.GetValidatorOutstandingRewards(ctx, val.GetOperator()) 93 | feePool := app.DistrKeeper.GetFeePool(ctx) 94 | feePool.CommunityPool = feePool.CommunityPool.Add(scraps...) 95 | app.DistrKeeper.SetFeePool(ctx, feePool) 96 | 97 | app.DistrKeeper.Hooks().AfterValidatorCreated(ctx, val.GetOperator()) 98 | return false 99 | }) 100 | 101 | // reinitialize all delegations 102 | for _, del := range dels { 103 | app.DistrKeeper.Hooks().BeforeDelegationCreated(ctx, del.DelegatorAddress, del.ValidatorAddress) 104 | app.DistrKeeper.Hooks().AfterDelegationModified(ctx, del.DelegatorAddress, del.ValidatorAddress) 105 | } 106 | 107 | // reset context height 108 | ctx = ctx.WithBlockHeight(height) 109 | 110 | /* Handle staking state. */ 111 | 112 | // iterate through redelegations, reset creation height 113 | app.StakingKeeper.IterateRedelegations(ctx, func(_ int64, red staking.Redelegation) (stop bool) { 114 | for i := range red.Entries { 115 | red.Entries[i].CreationHeight = 0 116 | } 117 | app.StakingKeeper.SetRedelegation(ctx, red) 118 | return false 119 | }) 120 | 121 | // iterate through unbonding delegations, reset creation height 122 | app.StakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd staking.UnbondingDelegation) (stop bool) { 123 | for i := range ubd.Entries { 124 | ubd.Entries[i].CreationHeight = 0 125 | } 126 | app.StakingKeeper.SetUnbondingDelegation(ctx, ubd) 127 | return false 128 | }) 129 | 130 | // Iterate through validators by power descending, reset bond heights, and 131 | // update bond intra-tx counters. 132 | store := ctx.KVStore(app.keys[staking.StoreKey]) 133 | iter := sdk.KVStoreReversePrefixIterator(store, staking.ValidatorsKey) 134 | counter := int16(0) 135 | 136 | for ; iter.Valid(); iter.Next() { 137 | addr := sdk.ValAddress(iter.Key()[1:]) 138 | validator, found := app.StakingKeeper.GetValidator(ctx, addr) 139 | if !found { 140 | panic("expected validator, not found") 141 | } 142 | 143 | validator.UnbondingHeight = 0 144 | if applyWhiteList && !whiteListMap[addr.String()] { 145 | validator.Jailed = true 146 | } 147 | 148 | app.StakingKeeper.SetValidator(ctx, validator) 149 | counter++ 150 | } 151 | 152 | iter.Close() 153 | 154 | _ = app.StakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) 155 | 156 | /* Handle slashing state. */ 157 | 158 | // reset start height on signing infos 159 | app.SlashingKeeper.IterateValidatorSigningInfos( 160 | ctx, 161 | func(addr sdk.ConsAddress, info slashing.ValidatorSigningInfo) (stop bool) { 162 | info.StartHeight = 0 163 | app.SlashingKeeper.SetValidatorSigningInfo(ctx, addr, info) 164 | return false 165 | }, 166 | ) 167 | } 168 | -------------------------------------------------------------------------------- /incubator/nft/app/genesis.go: -------------------------------------------------------------------------------- 1 | package simapp 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // The genesis state of the blockchain is represented here as a map of raw json 8 | // messages key'd by a identifier string. 9 | // The identifier is used to determine which module genesis information belongs 10 | // to so it may be appropriately routed during init chain. 11 | // Within this application default genesis information is retrieved from 12 | // the ModuleBasicManager which populates json from each BasicModule 13 | // object provided to it during init. 14 | type GenesisState map[string]json.RawMessage 15 | 16 | // NewDefaultGenesisState generates the default state for the application. 17 | func NewDefaultGenesisState() GenesisState { 18 | return ModuleBasics.DefaultGenesis() 19 | } 20 | -------------------------------------------------------------------------------- /incubator/nft/app/state.go: -------------------------------------------------------------------------------- 1 | package simapp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "math/rand" 9 | "time" 10 | 11 | "github.com/tendermint/tendermint/crypto/secp256k1" 12 | tmtypes "github.com/tendermint/tendermint/types" 13 | 14 | "github.com/cosmos/cosmos-sdk/codec" 15 | "github.com/cosmos/cosmos-sdk/simapp" 16 | simapparams "github.com/cosmos/cosmos-sdk/simapp/params" 17 | "github.com/cosmos/cosmos-sdk/types/module" 18 | "github.com/cosmos/cosmos-sdk/x/auth" 19 | "github.com/cosmos/cosmos-sdk/x/simulation" 20 | ) 21 | 22 | // AppStateFn returns the initial application state using a genesis or the simulation parameters. 23 | // It panics if the user provides files for both of them. 24 | // If a file is not given for the genesis or the sim params, it creates a randomized one. 25 | func AppStateFn(cdc *codec.Codec, simManager *module.SimulationManager) simulation.AppStateFn { 26 | return func(r *rand.Rand, accs []simulation.Account, config simulation.Config, 27 | ) (appState json.RawMessage, simAccs []simulation.Account, chainID string, genesisTimestamp time.Time) { 28 | 29 | if simapp.FlagGenesisTimeValue == 0 { 30 | genesisTimestamp = simulation.RandTimestamp(r) 31 | } else { 32 | genesisTimestamp = time.Unix(simapp.FlagGenesisTimeValue, 0) 33 | } 34 | 35 | chainID = config.ChainID 36 | switch { 37 | case config.ParamsFile != "" && config.GenesisFile != "": 38 | panic("cannot provide both a genesis file and a params file") 39 | 40 | case config.GenesisFile != "": 41 | // override the default chain-id from simapp to set it later to the config 42 | genesisDoc, accounts := AppStateFromGenesisFileFn(r, cdc, config.GenesisFile) 43 | 44 | if simapp.FlagGenesisTimeValue == 0 { 45 | // use genesis timestamp if no custom timestamp is provided (i.e no random timestamp) 46 | genesisTimestamp = genesisDoc.GenesisTime 47 | } 48 | 49 | appState = genesisDoc.AppState 50 | chainID = genesisDoc.ChainID 51 | simAccs = accounts 52 | 53 | case config.ParamsFile != "": 54 | appParams := make(simulation.AppParams) 55 | bz, err := ioutil.ReadFile(config.ParamsFile) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | cdc.MustUnmarshalJSON(bz, &appParams) 61 | appState, simAccs = AppStateRandomizedFn(simManager, r, cdc, accs, genesisTimestamp, appParams) 62 | 63 | default: 64 | appParams := make(simulation.AppParams) 65 | appState, simAccs = AppStateRandomizedFn(simManager, r, cdc, accs, genesisTimestamp, appParams) 66 | } 67 | 68 | return appState, simAccs, chainID, genesisTimestamp 69 | } 70 | } 71 | 72 | // AppStateRandomizedFn creates calls each module's GenesisState generator function 73 | // and creates the simulation params 74 | func AppStateRandomizedFn( 75 | simManager *module.SimulationManager, r *rand.Rand, cdc *codec.Codec, 76 | accs []simulation.Account, genesisTimestamp time.Time, appParams simulation.AppParams, 77 | ) (json.RawMessage, []simulation.Account) { 78 | numAccs := int64(len(accs)) 79 | genesisState := NewDefaultGenesisState() 80 | 81 | // generate a random amount of initial stake coins and a random initial 82 | // number of bonded accounts 83 | var initialStake, numInitiallyBonded int64 84 | appParams.GetOrGenerate( 85 | cdc, simapparams.StakePerAccount, &initialStake, r, 86 | func(r *rand.Rand) { initialStake = r.Int63n(1e12) }, 87 | ) 88 | appParams.GetOrGenerate( 89 | cdc, simapparams.InitiallyBondedValidators, &numInitiallyBonded, r, 90 | func(r *rand.Rand) { numInitiallyBonded = int64(r.Intn(300)) }, 91 | ) 92 | 93 | if numInitiallyBonded > numAccs { 94 | numInitiallyBonded = numAccs 95 | } 96 | 97 | fmt.Printf( 98 | `Selected randomly generated parameters for simulated genesis: 99 | { 100 | stake_per_account: "%d", 101 | initially_bonded_validators: "%d" 102 | } 103 | `, initialStake, numInitiallyBonded, 104 | ) 105 | 106 | simState := &module.SimulationState{ 107 | AppParams: appParams, 108 | Cdc: cdc, 109 | Rand: r, 110 | GenState: genesisState, 111 | Accounts: accs, 112 | InitialStake: initialStake, 113 | NumBonded: numInitiallyBonded, 114 | GenTimestamp: genesisTimestamp, 115 | } 116 | 117 | simManager.GenerateGenesisStates(simState) 118 | 119 | appState, err := cdc.MarshalJSON(genesisState) 120 | if err != nil { 121 | panic(err) 122 | } 123 | 124 | return appState, accs 125 | } 126 | 127 | // AppStateFromGenesisFileFn util function to generate the genesis AppState 128 | // from a genesis.json file. 129 | func AppStateFromGenesisFileFn(r io.Reader, cdc *codec.Codec, genesisFile string) (tmtypes.GenesisDoc, []simulation.Account) { 130 | bytes, err := ioutil.ReadFile(genesisFile) 131 | if err != nil { 132 | panic(err) 133 | } 134 | 135 | var genesis tmtypes.GenesisDoc 136 | cdc.MustUnmarshalJSON(bytes, &genesis) 137 | 138 | var appState GenesisState 139 | cdc.MustUnmarshalJSON(genesis.AppState, &appState) 140 | 141 | var authGenesis auth.GenesisState 142 | if appState[auth.ModuleName] != nil { 143 | cdc.MustUnmarshalJSON(appState[auth.ModuleName], &authGenesis) 144 | } 145 | 146 | newAccs := make([]simulation.Account, len(authGenesis.Accounts)) 147 | for i, acc := range authGenesis.Accounts { 148 | // Pick a random private key, since we don't know the actual key 149 | // This should be fine as it's only used for mock Tendermint validators 150 | // and these keys are never actually used to sign by mock Tendermint. 151 | privkeySeed := make([]byte, 15) 152 | if _, err := r.Read(privkeySeed); err != nil { 153 | panic(err) 154 | } 155 | 156 | privKey := secp256k1.GenPrivKeySecp256k1(privkeySeed) 157 | 158 | // create simulator accounts 159 | simAcc := simulation.Account{PrivKey: privKey, PubKey: privKey.PubKey(), Address: acc.GetAddress()} 160 | newAccs[i] = simAcc 161 | } 162 | 163 | return genesis, newAccs 164 | } 165 | -------------------------------------------------------------------------------- /incubator/nft/app/test_helpers.go: -------------------------------------------------------------------------------- 1 | package simapp 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | abci "github.com/tendermint/tendermint/abci/types" 9 | "github.com/tendermint/tendermint/crypto" 10 | "github.com/tendermint/tendermint/crypto/ed25519" 11 | "github.com/tendermint/tendermint/libs/log" 12 | dbm "github.com/tendermint/tm-db" 13 | 14 | bam "github.com/cosmos/cosmos-sdk/baseapp" 15 | "github.com/cosmos/cosmos-sdk/codec" 16 | sdk "github.com/cosmos/cosmos-sdk/types" 17 | "github.com/cosmos/cosmos-sdk/x/auth" 18 | authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" 19 | "github.com/cosmos/cosmos-sdk/x/supply" 20 | ) 21 | 22 | // Setup initializes a new SimApp. A Nop logger is set in SimApp. 23 | func Setup(isCheckTx bool) *SimApp { 24 | db := dbm.NewMemDB() 25 | app := NewSimApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, 0) 26 | if !isCheckTx { 27 | // init chain must be called to stop deliverState from being nil 28 | genesisState := NewDefaultGenesisState() 29 | stateBytes, err := codec.MarshalJSONIndent(app.cdc, genesisState) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | // Initialize the chain 35 | app.InitChain( 36 | abci.RequestInitChain{ 37 | Validators: []abci.ValidatorUpdate{}, 38 | AppStateBytes: stateBytes, 39 | }, 40 | ) 41 | } 42 | 43 | return app 44 | } 45 | 46 | // SetupWithGenesisAccounts initializes a new SimApp with the passed in 47 | // genesis accounts. 48 | func SetupWithGenesisAccounts(genAccs []authexported.GenesisAccount) *SimApp { 49 | db := dbm.NewMemDB() 50 | app := NewSimApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, 0) 51 | 52 | // initialize the chain with the passed in genesis accounts 53 | genesisState := NewDefaultGenesisState() 54 | 55 | authGenesis := auth.NewGenesisState(auth.DefaultParams(), genAccs) 56 | genesisStateBz := app.cdc.MustMarshalJSON(authGenesis) 57 | genesisState[auth.ModuleName] = genesisStateBz 58 | 59 | stateBytes, err := codec.MarshalJSONIndent(app.cdc, genesisState) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | // Initialize the chain 65 | app.InitChain( 66 | abci.RequestInitChain{ 67 | Validators: []abci.ValidatorUpdate{}, 68 | AppStateBytes: stateBytes, 69 | }, 70 | ) 71 | 72 | app.Commit() 73 | app.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: app.LastBlockHeight() + 1}}) 74 | 75 | return app 76 | } 77 | 78 | // AddTestAddrs constructs and returns accNum amount of accounts with an 79 | // initial balance of accAmt 80 | func AddTestAddrs(app *SimApp, ctx sdk.Context, accNum int, accAmt sdk.Int) []sdk.AccAddress { 81 | testAddrs := make([]sdk.AccAddress, accNum) 82 | for i := 0; i < accNum; i++ { 83 | pk := ed25519.GenPrivKey().PubKey() 84 | testAddrs[i] = sdk.AccAddress(pk.Address()) 85 | } 86 | 87 | initCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), accAmt)) 88 | totalSupply := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), accAmt.MulRaw(int64(len(testAddrs))))) 89 | prevSupply := app.SupplyKeeper.GetSupply(ctx) 90 | app.SupplyKeeper.SetSupply(ctx, supply.NewSupply(prevSupply.GetTotal().Add(totalSupply...))) 91 | 92 | // fill all the addresses with some coins, set the loose pool tokens simultaneously 93 | for _, addr := range testAddrs { 94 | _, err := app.BankKeeper.AddCoins(ctx, addr, initCoins) 95 | if err != nil { 96 | panic(err) 97 | } 98 | } 99 | return testAddrs 100 | } 101 | 102 | // CheckBalance checks the balance of an account. 103 | func CheckBalance(t *testing.T, app *SimApp, addr sdk.AccAddress, exp sdk.Coins) { 104 | ctxCheck := app.BaseApp.NewContext(true, abci.Header{}) 105 | res := app.AccountKeeper.GetAccount(ctxCheck, addr) 106 | 107 | require.Equal(t, exp, res.GetCoins()) 108 | } 109 | 110 | // GenTx generates a signed mock transaction. 111 | func GenTx(msgs []sdk.Msg, accnums []uint64, seq []uint64, priv ...crypto.PrivKey) auth.StdTx { 112 | // Make the transaction free 113 | fee := auth.StdFee{ 114 | Amount: sdk.NewCoins(sdk.NewInt64Coin("foocoin", 0)), 115 | Gas: 100000, 116 | } 117 | 118 | sigs := make([]auth.StdSignature, len(priv)) 119 | memo := "testmemotestmemo" 120 | 121 | for i, p := range priv { 122 | // use a empty chainID for ease of testing 123 | sig, err := p.Sign(auth.StdSignBytes("", accnums[i], seq[i], fee, msgs, memo)) 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | sigs[i] = auth.StdSignature{ 129 | PubKey: p.PubKey(), 130 | Signature: sig, 131 | } 132 | } 133 | 134 | return auth.NewStdTx(msgs, fee, sigs, memo) 135 | } 136 | 137 | // SignCheckDeliver checks a generated signed transaction and simulates a 138 | // block commitment with the given transaction. A test assertion is made using 139 | // the parameter 'expPass' against the result. A corresponding result is 140 | // returned. 141 | func SignCheckDeliver( 142 | t *testing.T, cdc *codec.Codec, app *bam.BaseApp, header abci.Header, msgs []sdk.Msg, 143 | accNums, seq []uint64, expSimPass, expPass bool, priv ...crypto.PrivKey, 144 | ) (sdk.GasInfo, *sdk.Result, error) { 145 | tx := GenTx(msgs, accNums, seq, priv...) 146 | 147 | txBytes, err := cdc.MarshalBinaryLengthPrefixed(tx) 148 | require.Nil(t, err) 149 | 150 | // Must simulate now as CheckTx doesn't run Msgs anymore 151 | _, res, err := app.Simulate(txBytes, tx) 152 | 153 | if expSimPass { 154 | require.NoError(t, err) 155 | require.NotNil(t, res) 156 | } else { 157 | require.Error(t, err) 158 | require.Nil(t, res) 159 | } 160 | 161 | // Simulate a sending a transaction and committing a block 162 | app.BeginBlock(abci.RequestBeginBlock{Header: header}) 163 | gInfo, res, err := app.Deliver(tx) 164 | 165 | if expPass { 166 | require.NoError(t, err) 167 | require.NotNil(t, res) 168 | } else { 169 | require.Error(t, err) 170 | require.Nil(t, res) 171 | } 172 | 173 | app.EndBlock(abci.RequestEndBlock{}) 174 | app.Commit() 175 | 176 | return gInfo, res, err 177 | } 178 | 179 | // GenSequenceOfTxs generates a set of signed transactions of messages, such 180 | // that they differ only by having the sequence numbers incremented between 181 | // every transaction. 182 | func GenSequenceOfTxs(msgs []sdk.Msg, accnums []uint64, initSeqNums []uint64, numToGenerate int, priv ...crypto.PrivKey) []auth.StdTx { 183 | txs := make([]auth.StdTx, numToGenerate) 184 | for i := 0; i < numToGenerate; i++ { 185 | txs[i] = GenTx(msgs, accnums, initSeqNums, priv...) 186 | incrementAllSequenceNumbers(initSeqNums) 187 | } 188 | 189 | return txs 190 | } 191 | 192 | func incrementAllSequenceNumbers(initSeqNums []uint64) { 193 | for i := 0; i < len(initSeqNums); i++ { 194 | initSeqNums[i]++ 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /incubator/nft/client/cli/tx.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | 11 | "github.com/cosmos/cosmos-sdk/client" 12 | "github.com/cosmos/cosmos-sdk/client/context" 13 | "github.com/cosmos/cosmos-sdk/client/flags" 14 | "github.com/cosmos/cosmos-sdk/codec" 15 | sdk "github.com/cosmos/cosmos-sdk/types" 16 | "github.com/cosmos/cosmos-sdk/version" 17 | authtypes "github.com/cosmos/cosmos-sdk/x/auth" 18 | "github.com/cosmos/cosmos-sdk/x/auth/client/utils" 19 | "github.com/cosmos/modules/incubator/nft/types" 20 | ) 21 | 22 | // Edit metadata flags 23 | const ( 24 | flagTokenURI = "tokenURI" 25 | ) 26 | 27 | // GetTxCmd returns the transaction commands for this module 28 | func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { 29 | nftTxCmd := &cobra.Command{ 30 | Use: types.ModuleName, 31 | Short: "NFT transactions subcommands", 32 | RunE: client.ValidateCmd, 33 | } 34 | 35 | nftTxCmd.AddCommand(flags.PostCommands( 36 | GetCmdTransferNFT(cdc), 37 | GetCmdEditNFTMetadata(cdc), 38 | GetCmdMintNFT(cdc), 39 | GetCmdBurnNFT(cdc), 40 | )...) 41 | 42 | return nftTxCmd 43 | } 44 | 45 | // GetCmdTransferNFT is the CLI command for sending a TransferNFT transaction 46 | func GetCmdTransferNFT(cdc *codec.Codec) *cobra.Command { 47 | return &cobra.Command{ 48 | Use: "transfer [sender] [recipient] [denom] [tokenID]", 49 | Short: "transfer a NFT to a recipient", 50 | Long: strings.TrimSpace( 51 | fmt.Sprintf(`Transfer a NFT from a given collection that has a 52 | specific id (SHA-256 hex hash) to a specific recipient. 53 | 54 | Example: 55 | $ %s tx %s transfer 56 | cosmos1gghjut3ccd8ay0zduzj64hwre2fxs9ld75ru9p cosmos1l2rsakp388kuv9k8qzq6lrm9taddae7fpx59wm \ 57 | crypto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa \ 58 | --from mykey 59 | `, 60 | version.ClientName, types.ModuleName, 61 | ), 62 | ), 63 | Args: cobra.ExactArgs(4), 64 | RunE: func(cmd *cobra.Command, args []string) error { 65 | inBuf := bufio.NewReader(cmd.InOrStdin()) 66 | txBldr := authtypes.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) 67 | cliCtx := context.NewCLIContext().WithCodec(cdc) 68 | 69 | sender, err := sdk.AccAddressFromBech32(args[0]) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | recipient, err := sdk.AccAddressFromBech32(args[1]) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | denom := args[2] 80 | tokenID := args[3] 81 | 82 | msg := types.NewMsgTransferNFT(sender, recipient, denom, tokenID) 83 | return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) 84 | }, 85 | } 86 | } 87 | 88 | // GetCmdEditNFTMetadata is the CLI command for sending an EditMetadata transaction 89 | func GetCmdEditNFTMetadata(cdc *codec.Codec) *cobra.Command { 90 | cmd := &cobra.Command{ 91 | Use: "edit-metadata [denom] [tokenID]", 92 | Short: "edit the metadata of an NFT", 93 | Long: strings.TrimSpace( 94 | fmt.Sprintf(`Edit the metadata of an NFT from a given collection that has a 95 | specific id (SHA-256 hex hash). 96 | 97 | Example: 98 | $ %s tx %s edit-metadata crypto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa \ 99 | --tokenURI path_to_token_URI_JSON --from mykey 100 | `, 101 | version.ClientName, types.ModuleName, 102 | ), 103 | ), 104 | Args: cobra.ExactArgs(2), 105 | RunE: func(cmd *cobra.Command, args []string) error { 106 | inBuf := bufio.NewReader(cmd.InOrStdin()) 107 | cliCtx := context.NewCLIContext().WithCodec(cdc) 108 | txBldr := authtypes.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) 109 | 110 | denom := args[0] 111 | tokenID := args[1] 112 | tokenURI := viper.GetString(flagTokenURI) 113 | 114 | msg := types.NewMsgEditNFTMetadata(cliCtx.GetFromAddress(), tokenID, denom, tokenURI) 115 | return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) 116 | }, 117 | } 118 | 119 | cmd.Flags().String(flagTokenURI, "", "Extra properties available for querying") 120 | return cmd 121 | } 122 | 123 | // GetCmdMintNFT is the CLI command for a MintNFT transaction 124 | func GetCmdMintNFT(cdc *codec.Codec) *cobra.Command { 125 | cmd := &cobra.Command{ 126 | Use: "mint [denom] [tokenID] [recipient]", 127 | Short: "mint an NFT and set the owner to the recipient", 128 | Long: strings.TrimSpace( 129 | fmt.Sprintf(`Mint an NFT from a given collection that has a 130 | specific id (SHA-256 hex hash) and set the ownership to a specific address. 131 | 132 | Example: 133 | $ %s tx %s mint crypto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa \ 134 | cosmos1gghjut3ccd8ay0zduzj64hwre2fxs9ld75ru9p --from mykey 135 | `, 136 | version.ClientName, types.ModuleName, 137 | ), 138 | ), 139 | Args: cobra.ExactArgs(3), 140 | RunE: func(cmd *cobra.Command, args []string) error { 141 | inBuf := bufio.NewReader(cmd.InOrStdin()) 142 | cliCtx := context.NewCLIContext().WithCodec(cdc) 143 | txBldr := authtypes.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) 144 | 145 | denom := args[0] 146 | tokenID := args[1] 147 | 148 | recipient, err := sdk.AccAddressFromBech32(args[2]) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | tokenURI := viper.GetString(flagTokenURI) 154 | 155 | msg := types.NewMsgMintNFT(cliCtx.GetFromAddress(), recipient, tokenID, denom, tokenURI) 156 | return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) 157 | }, 158 | } 159 | 160 | cmd.Flags().String(flagTokenURI, "", "URI for supplemental off-chain metadata (should return a JSON object)") 161 | 162 | return cmd 163 | } 164 | 165 | // GetCmdBurnNFT is the CLI command for sending a BurnNFT transaction 166 | func GetCmdBurnNFT(cdc *codec.Codec) *cobra.Command { 167 | return &cobra.Command{ 168 | Use: "burn [denom] [tokenID]", 169 | Short: "burn an NFT", 170 | Long: strings.TrimSpace( 171 | fmt.Sprintf(`Burn (i.e permanently delete) an NFT from a given collection that has a 172 | specific id (SHA-256 hex hash). 173 | 174 | Example: 175 | $ %s tx %s burn crypto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa \ 176 | --from mykey 177 | `, 178 | version.ClientName, types.ModuleName, 179 | ), 180 | ), 181 | Args: cobra.ExactArgs(2), 182 | RunE: func(cmd *cobra.Command, args []string) error { 183 | inBuf := bufio.NewReader(cmd.InOrStdin()) 184 | cliCtx := context.NewCLIContext().WithCodec(cdc) 185 | txBldr := authtypes.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) 186 | 187 | denom := args[0] 188 | tokenID := args[1] 189 | 190 | msg := types.NewMsgBurnNFT(cliCtx.GetFromAddress(), tokenID, denom) 191 | return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) 192 | }, 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /incubator/nft/client/rest/query.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | 9 | "github.com/cosmos/cosmos-sdk/client/context" 10 | "github.com/cosmos/cosmos-sdk/codec" 11 | sdk "github.com/cosmos/cosmos-sdk/types" 12 | "github.com/cosmos/cosmos-sdk/types/rest" 13 | "github.com/cosmos/modules/incubator/nft/types" 14 | ) 15 | 16 | func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, queryRoute string) { 17 | // Get the total supply of a collection 18 | r.HandleFunc( 19 | "/nft/supply/{denom}", getSupply(cdc, cliCtx, queryRoute), 20 | ).Methods("GET") 21 | 22 | // Get the collections of NFTs owned by an address 23 | r.HandleFunc( 24 | "/nft/owner/{delegatorAddr}", getOwner(cdc, cliCtx, queryRoute), 25 | ).Methods("GET") 26 | 27 | // Get the NFTs owned by an address from a given collection 28 | r.HandleFunc( 29 | "/nft/owner/{delegatorAddr}/collection/{denom}", getOwnerByDenom(cdc, cliCtx, queryRoute), 30 | ).Methods("GET") 31 | 32 | // Get all the NFT from a given collection 33 | r.HandleFunc( 34 | "/nft/collection/{denom}", getCollection(cdc, cliCtx, queryRoute), 35 | ).Methods("GET") 36 | 37 | // Query all denoms 38 | r.HandleFunc( 39 | "/nft/denoms", getDenoms(cdc, cliCtx, queryRoute), 40 | ).Methods("GET") 41 | 42 | // Query a single NFT 43 | r.HandleFunc( 44 | "/nft/collection/{denom}/nft/{id}", getNFT(cdc, cliCtx, queryRoute), 45 | ).Methods("GET") 46 | } 47 | 48 | func getSupply(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { 49 | return func(w http.ResponseWriter, r *http.Request) { 50 | denom := mux.Vars(r)["denom"] 51 | 52 | params := types.NewQueryCollectionParams(denom) 53 | bz, err := cdc.MarshalJSON(params) 54 | if err != nil { 55 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 56 | return 57 | } 58 | 59 | res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/supply/%s", queryRoute, denom), bz) 60 | if err != nil { 61 | rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) 62 | return 63 | } 64 | 65 | rest.PostProcessResponse(w, cliCtx, res) 66 | } 67 | } 68 | 69 | func getOwner(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { 70 | return func(w http.ResponseWriter, r *http.Request) { 71 | address, err := sdk.AccAddressFromBech32(mux.Vars(r)["delegatorAddr"]) 72 | if err != nil { 73 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 74 | return 75 | } 76 | 77 | params := types.NewQueryBalanceParams(address, "") 78 | bz, err := cdc.MarshalJSON(params) 79 | if err != nil { 80 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 81 | return 82 | } 83 | 84 | res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/owner", queryRoute), bz) 85 | if err != nil { 86 | rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) 87 | return 88 | } 89 | 90 | rest.PostProcessResponse(w, cliCtx, res) 91 | } 92 | } 93 | 94 | func getOwnerByDenom(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { 95 | return func(w http.ResponseWriter, r *http.Request) { 96 | vars := mux.Vars(r) 97 | denom := vars["denom"] 98 | address, err := sdk.AccAddressFromBech32(vars["delegatorAddr"]) 99 | if err != nil { 100 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 101 | return 102 | } 103 | 104 | params := types.NewQueryBalanceParams(address, denom) 105 | bz, err := cdc.MarshalJSON(params) 106 | if err != nil { 107 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 108 | return 109 | } 110 | 111 | res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/ownerByDenom", queryRoute), bz) 112 | if err != nil { 113 | rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) 114 | return 115 | } 116 | 117 | rest.PostProcessResponse(w, cliCtx, res) 118 | } 119 | } 120 | 121 | func getCollection(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { 122 | return func(w http.ResponseWriter, r *http.Request) { 123 | denom := mux.Vars(r)["denom"] 124 | 125 | params := types.NewQueryCollectionParams(denom) 126 | bz, err := cdc.MarshalJSON(params) 127 | if err != nil { 128 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 129 | return 130 | } 131 | 132 | res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/collection", queryRoute), bz) 133 | if err != nil { 134 | rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) 135 | return 136 | } 137 | 138 | rest.PostProcessResponse(w, cliCtx, res) 139 | } 140 | } 141 | 142 | func getDenoms(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { 143 | return func(w http.ResponseWriter, r *http.Request) { 144 | res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/denoms", queryRoute), nil) 145 | if err != nil { 146 | rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) 147 | return 148 | } 149 | 150 | rest.PostProcessResponse(w, cliCtx, res) 151 | } 152 | } 153 | 154 | func getNFT(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { 155 | return func(w http.ResponseWriter, r *http.Request) { 156 | vars := mux.Vars(r) 157 | denom := vars["denom"] 158 | id := vars["id"] 159 | 160 | params := types.NewQueryNFTParams(denom, id) 161 | bz, err := cdc.MarshalJSON(params) 162 | if err != nil { 163 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 164 | return 165 | } 166 | 167 | res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/nft", queryRoute), bz) 168 | if err != nil { 169 | rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) 170 | return 171 | } 172 | 173 | rest.PostProcessResponse(w, cliCtx, res) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /incubator/nft/client/rest/rest.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | 6 | "github.com/cosmos/cosmos-sdk/client/context" 7 | "github.com/cosmos/cosmos-sdk/codec" 8 | ) 9 | 10 | // RegisterRoutes register distribution REST routes. 11 | func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, queryRoute string) { 12 | registerQueryRoutes(cliCtx, r, cdc, queryRoute) 13 | registerTxRoutes(cliCtx, r, cdc, queryRoute) 14 | } 15 | -------------------------------------------------------------------------------- /incubator/nft/client/rest/tx.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/cosmos/cosmos-sdk/client/context" 7 | "github.com/cosmos/cosmos-sdk/codec" 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | "github.com/cosmos/cosmos-sdk/types/rest" 10 | "github.com/cosmos/cosmos-sdk/x/auth/client/utils" 11 | "github.com/cosmos/modules/incubator/nft/types" 12 | 13 | "github.com/gorilla/mux" 14 | ) 15 | 16 | func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router, 17 | cdc *codec.Codec, queryRoute string) { 18 | // Transfer an NFT to an address 19 | r.HandleFunc( 20 | "/nfts/transfer", 21 | transferNFTHandler(cdc, cliCtx), 22 | ).Methods("POST") 23 | 24 | // Update an NFT metadata 25 | r.HandleFunc( 26 | "/nfts/collection/{denom}/nft/{id}/metadata", 27 | editNFTMetadataHandler(cdc, cliCtx), 28 | ).Methods("PUT") 29 | 30 | // Mint an NFT 31 | r.HandleFunc( 32 | "/nfts/mint", 33 | mintNFTHandler(cdc, cliCtx), 34 | ).Methods("POST") 35 | 36 | // Burn an NFT 37 | r.HandleFunc( 38 | "/nfts/collection/{denom}/nft/{id}/burn", 39 | burnNFTHandler(cdc, cliCtx), 40 | ).Methods("PUT") 41 | } 42 | 43 | type transferNFTReq struct { 44 | BaseReq rest.BaseReq `json:"base_req"` 45 | Denom string `json:"denom"` 46 | ID string `json:"id"` 47 | Recipient string `json:"recipient"` 48 | } 49 | 50 | func transferNFTHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { 51 | return func(w http.ResponseWriter, r *http.Request) { 52 | var req transferNFTReq 53 | if !rest.ReadRESTReq(w, r, cdc, &req) { 54 | rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request") 55 | return 56 | } 57 | baseReq := req.BaseReq.Sanitize() 58 | if !baseReq.ValidateBasic(w) { 59 | return 60 | } 61 | 62 | fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) 63 | if err != nil { 64 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 65 | return 66 | } 67 | 68 | recipient, err := sdk.AccAddressFromBech32(req.Recipient) 69 | if err != nil { 70 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 71 | return 72 | } 73 | 74 | // create the message 75 | msg := types.NewMsgTransferNFT(fromAddr, recipient, req.Denom, req.ID) 76 | 77 | utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg}) 78 | } 79 | } 80 | 81 | type editNFTMetadataReq struct { 82 | BaseReq rest.BaseReq `json:"base_req"` 83 | Denom string `json:"denom"` 84 | ID string `json:"id"` 85 | TokenURI string `json:"tokenURI"` 86 | } 87 | 88 | func editNFTMetadataHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { 89 | return func(w http.ResponseWriter, r *http.Request) { 90 | var req editNFTMetadataReq 91 | if !rest.ReadRESTReq(w, r, cdc, &req) { 92 | rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request") 93 | return 94 | } 95 | baseReq := req.BaseReq.Sanitize() 96 | if !baseReq.ValidateBasic(w) { 97 | return 98 | } 99 | 100 | fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) 101 | if err != nil { 102 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 103 | return 104 | } 105 | 106 | // create the message 107 | msg := types.NewMsgEditNFTMetadata(fromAddr, req.ID, req.Denom, req.TokenURI) 108 | 109 | utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg}) 110 | } 111 | } 112 | 113 | type mintNFTReq struct { 114 | BaseReq rest.BaseReq `json:"base_req"` 115 | Recipient sdk.AccAddress `json:"recipient"` 116 | Denom string `json:"denom"` 117 | ID string `json:"id"` 118 | TokenURI string `json:"tokenURI"` 119 | } 120 | 121 | func mintNFTHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { 122 | return func(w http.ResponseWriter, r *http.Request) { 123 | var req mintNFTReq 124 | if !rest.ReadRESTReq(w, r, cdc, &req) { 125 | rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request") 126 | return 127 | } 128 | baseReq := req.BaseReq.Sanitize() 129 | if !baseReq.ValidateBasic(w) { 130 | return 131 | } 132 | 133 | fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) 134 | if err != nil { 135 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 136 | return 137 | } 138 | 139 | // create the message 140 | msg := types.NewMsgMintNFT(fromAddr, req.Recipient, req.ID, req.Denom, req.TokenURI) 141 | 142 | utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg}) 143 | } 144 | } 145 | 146 | type burnNFTReq struct { 147 | BaseReq rest.BaseReq `json:"base_req"` 148 | Denom string `json:"denom"` 149 | ID string `json:"id"` 150 | } 151 | 152 | func burnNFTHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { 153 | return func(w http.ResponseWriter, r *http.Request) { 154 | var req burnNFTReq 155 | if !rest.ReadRESTReq(w, r, cdc, &req) { 156 | rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request") 157 | return 158 | } 159 | baseReq := req.BaseReq.Sanitize() 160 | if !baseReq.ValidateBasic(w) { 161 | return 162 | } 163 | 164 | fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) 165 | if err != nil { 166 | rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) 167 | return 168 | } 169 | 170 | // create the message 171 | msg := types.NewMsgBurnNFT(fromAddr, req.ID, req.Denom) 172 | utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg}) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /incubator/nft/docs/spec/01_concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | ## NFT 4 | 5 | The `NFT` Interface inherits the BaseNFT struct and includes getter functions for the asset data. It also includes a Stringer function in order to print the struct. The interface may change if metadata is moved to it’s own module as it might no longer be necessary for the flexibility of an interface. 6 | 7 | ```go 8 | // NFT non fungible token interface 9 | type NFT interface { 10 | GetID() string // unique identifier of the NFT 11 | GetOwner() sdk.AccAddress // gets owner account of the NFT 12 | SetOwner(address sdk.AccAddress) // gets owner account of the NFT 13 | GetTokenURI() string // metadata field: URI to retrieve the of chain metadata of the NFT 14 | EditMetadata(tokenURI string) // edit metadata of the NFT 15 | String() string // string representation of the NFT object 16 | } 17 | ``` 18 | 19 | ## Collections 20 | 21 | A Collection is used to organized sets of NFTs. It contains the denomination of the NFT instead of storing it within each NFT. This saves storage space by removing redundancy. 22 | 23 | ```go 24 | // Collection of non fungible tokens 25 | type Collection struct { 26 | Denom string `json:"denom,omitempty"` // name of the collection; not exported to clients 27 | NFTs []*NFT `json:"nfts"` // NFTs that belongs to a collection 28 | } 29 | ``` 30 | 31 | ## Owner 32 | 33 | An Owner is a struct that includes information about all NFTs owned by a single account. It would be possible to retrieve this information by looping through all Collections but that process could become computationally prohibitive so a more efficient retrieval system is to store redundant information limited to the token ID by owner. 34 | 35 | ```go 36 | // Owner of non fungible tokens 37 | type Owner struct { 38 | Address sdk.AccAddress `json:"address"` 39 | IDCollections IDCollections `json:"IDCollections"` 40 | } 41 | ``` 42 | 43 | An `IDCollection` is similar to a `Collection` except instead of containing NFTs it only contains an array of `NFT` IDs. This saves storage by avoiding redundancy. 44 | 45 | ```go 46 | // IDCollection of non fungible tokens 47 | type IDCollection struct { 48 | Denom string `json:"denom"` 49 | IDs []string `json:"IDs"` 50 | } 51 | -------------------------------------------------------------------------------- /incubator/nft/docs/spec/02_state.md: -------------------------------------------------------------------------------- 1 | # State 2 | 3 | ## Collections 4 | 5 | As all NFTs belong to a specific `Collection`, they are kept on store in an array 6 | within each `Collection`. Every time an NFT that belongs to a collection is updated, 7 | it needs to be updated on the corresponding NFT array on the corresponding `Collection`. 8 | `denomHash` is used as part of the key to limit the length of the `denomBytes` which is 9 | a hash of `denomBytes` made from the tendermint [tmhash library](https://github.com/tendermint/tendermint/tree/master/crypto/tmhash). 10 | 11 | - Collections: `0x00 | denomHash -> amino(Collection)` 12 | - denomHash: `tmhash(denomBytes)` 13 | 14 | ## Owners 15 | 16 | The ownership of an NFT is set initially when an NFT is minted and needs to be 17 | updated every time there's a transfer or when an NFT is burned. 18 | 19 | - Owners: `0x01 | addressBytes | denomHash -> amino(Owner)` 20 | - denomHash: `tmhash(denomBytes)` 21 | -------------------------------------------------------------------------------- /incubator/nft/docs/spec/03_messages.md: -------------------------------------------------------------------------------- 1 | # Messages 2 | 3 | ## MsgTransferNFT 4 | 5 | This is the most commonly expected MsgType to be supported across chains. While each application specific blockchain will have very different adoption of the `MsgMintNFT`, `MsgBurnNFT` and `MsgEditNFTMetadata` it should be expected that most chains support the ability to transfer ownership of the NFT asset. The exception to this would be non-transferable NFTs that might be attached to reputation or some asset which should not be transferable. It still makes sense for this to be represented as an NFT because there are common queriers which will remain relevant to the NFT type even if non-transferable. This Message will fail if the NFT does not exist. By default it will not fail if the transfer is executed by someone beside the owner. **It is highly recommended that a custom handler is made to restrict use of this Message type to prevent unintended use.** 6 | 7 | | **Field** | **Type** | **Description** | 8 | |:----------|:-----------------|:--------------------------------------------------------------------------------------------------------------| 9 | | Sender | `sdk.AccAddress` | The account address of the user sending the NFT. By default it is __not__ required that the sender is also the owner of the NFT. | 10 | | Recipient | `sdk.AccAddress` | The account address who will receive the NFT as a result of the transfer transaction. | 11 | | Denom | `string` | The denomination of the NFT, necessary as multiple denominations are able to be represented on each chain. | 12 | | ID | `string` | The unique ID of the NFT being transferred | 13 | 14 | ```go 15 | // MsgTransferNFT defines a TransferNFT message 16 | type MsgTransferNFT struct { 17 | Sender sdk.AccAddress 18 | Recipient sdk.AccAddress 19 | Denom string 20 | ID string 21 | } 22 | ``` 23 | 24 | ## MsgEditNFTMetadata 25 | 26 | This message type allows the `TokenURI` to be updated. By default anyone can execute this Message type. **It is highly recommended that a custom handler is made to restrict use of this Message type to prevent unintended use.** 27 | 28 | | **Field** | **Type** | **Description** | 29 | |:------------|:-----------------|:-----------------------------------------------------------------------------------------------------------| 30 | | Sender | `sdk.AccAddress` | The creator of the message | 31 | | ID | `string` | The unique ID of the NFT being edited | 32 | | Denom | `string` | The denomination of the NFT, necessary as multiple denominations are able to be represented on each chain. | 33 | | TokenURI | `string` | The URI pointing to a JSON object that contains subsequent metadata information off-chain | 34 | 35 | ```go 36 | // MsgEditNFTMetadata edits an NFT's metadata 37 | type MsgEditNFTMetadata struct { 38 | Sender sdk.AccAddress 39 | ID string 40 | Denom string 41 | TokenURI string 42 | } 43 | ``` 44 | 45 | ## MsgMintNFT 46 | 47 | This message type is used for minting new tokens. If a new `NFT` is minted under a new `Denom`, a new `Collection` will also be created, otherwise the `NFT` is added to the existing `Collection`. If a new `NFT` is minted by a new account, a new `Owner` is created, otherwise the `NFT` `ID` is added to the existing `Owner`'s `IDCollection`. By default anyone can execute this Message type. **It is highly recommended that a custom handler is made to restrict use of this Message type to prevent unintended use.** 48 | 49 | | **Field** | **Type** | **Description** | 50 | |:------------|:-----------------|:-----------------------------------------------------------------------------------------| 51 | | Sender | `sdk.AccAddress` | The sender of the Message | 52 | | Recipient | `sdk.AccAddress` | The recipiet of the new NFT | 53 | | ID | `string` | The unique ID of the NFT being minted | 54 | | Denom | `string` | The denomination of the NFT. | 55 | | TokenURI | `string` | The URI pointing to a JSON object that contains subsequent metadata information off-chain | 56 | 57 | ```go 58 | // MsgMintNFT defines a MintNFT message 59 | type MsgMintNFT struct { 60 | Sender sdk.AccAddress 61 | Recipient sdk.AccAddress 62 | ID string 63 | Denom string 64 | TokenURI string 65 | } 66 | ``` 67 | 68 | ### MsgBurnNFT 69 | 70 | This message type is used for burning tokens which destroys and deletes them. By default anyone can execute this Message type. **It is highly recommended that a custom handler is made to restrict use of this Message type to prevent unintended use.** 71 | 72 | 73 | | **Field** | **Type** | **Description** | 74 | |:----------|:-----------------|:---------------------------------------------------| 75 | | Sender | `sdk.AccAddress` | The account address of the user burning the token. | 76 | | ID | `string` | The ID of the Token. | 77 | | Denom | `string` | The Denom of the Token. | 78 | 79 | ```go 80 | // MsgBurnNFT defines a BurnNFT message 81 | type MsgBurnNFT struct { 82 | Sender sdk.AccAddress 83 | ID string 84 | Denom string 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /incubator/nft/docs/spec/04_events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | The nft module emits the following events: 4 | 5 | ## Handlers 6 | 7 | ### MsgTransferNFT 8 | 9 | | Type | Attribute Key | Attribute Value | 10 | |--------------|---------------|--------------------| 11 | | transfer_nft | denom | {nftDenom} | 12 | | transfer_nft | nft-id | {nftID} | 13 | | transfer_nft | recipient | {recipientAddress} | 14 | | message | module | nft | 15 | | message | action | transfer_nft | 16 | | message | sender | {senderAddress} | 17 | 18 | ### MsgEditNFTMetadata 19 | 20 | | Type | Attribute Key | Attribute Value | 21 | |-------------------|---------------|-------------------| 22 | | edit_nft_metadata | denom | {nftDenom} | 23 | | edit_nft_metadata | nft-id | {nftID} | 24 | | message | module | nft | 25 | | message | action | edit_nft_metadata | 26 | | message | sender | {senderAddress} | 27 | | message | token-uri | {tokenURI} | 28 | 29 | ### MsgMintNFT 30 | 31 | | Type | Attribute Key | Attribute Value | 32 | |----------|---------------|-----------------| 33 | | mint_nft | denom | {nftDenom} | 34 | | mint_nft | nft-id | {nftID} | 35 | | message | module | nft | 36 | | message | action | mint_nft | 37 | | message | sender | {senderAddress} | 38 | | message | token-uri | {tokenURI} | 39 | 40 | ### MsgBurnNFTs 41 | 42 | | Type | Attribute Key | Attribute Value | 43 | |----------|---------------|-----------------| 44 | | burn_nft | denom | {nftDenom} | 45 | | burn_nft | nft-id | {nftID} | 46 | | message | module | nft | 47 | | message | action | burn_nft | 48 | | message | sender | {senderAddress} | 49 | -------------------------------------------------------------------------------- /incubator/nft/docs/spec/05_future_improvements.md: -------------------------------------------------------------------------------- 1 | # Future Improvements 2 | 3 | There's interesting work that could be done about moving metadata into its own module. This could act as one of the `tokenURI` endpoints if a chain chooses to offer storage as a solution. Furthermore on-chain metadata can be trusted to a higher degree and might be used in secondary actions like price evaluation. Moving metadata to it's own module could be useful for the Bank Module as well. It would be able to describe attributes like decimal places and information regarding vesting schedules. It would be needed to have a level of introspection to describe the content without actually delivering the content for client libraries to interact with it. Using schema.org as a common location to settle metadata schema structure would be a good and impartial place to do so. 4 | 5 | Inter-Blockchain Communication will need to develop its own Message types that allow NFTs to be transferred across chains. Making sure that spec is able to support the NFTs created by this module should be easy. What might be more complicated is a transfer that includes optional metadata so that a receiving chain has the option of parsing and storing it instead of making IBC queries when that data needs to be accessed (assuming that information stays up to date). 6 | -------------------------------------------------------------------------------- /incubator/nft/docs/spec/06_appendix.md: -------------------------------------------------------------------------------- 1 | # Appendix 2 | 3 | * Cosmos SDK: [PR #4209](https://github.com/cosmos/cosmos-sdk/pull/4209) 4 | * Cosmos SDK: [Issue #4046](https://github.com/cosmos/cosmos-sdk/issues/4046) 5 | * Interchain Standards: [ICS #17](https://github.com/cosmos/ics/issues/30) 6 | * Binance: [BEP #7](https://github.com/binance-chain/BEPs/pull/7) 7 | * Ethereum: [EIP #721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) 8 | -------------------------------------------------------------------------------- /incubator/nft/exported/nft.go: -------------------------------------------------------------------------------- 1 | package exported 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | ) 6 | 7 | // NFT non fungible token interface 8 | type NFT interface { 9 | GetID() string 10 | GetOwner() sdk.AccAddress 11 | SetOwner(address sdk.AccAddress) 12 | GetTokenURI() string 13 | EditMetadata(tokenURI string) 14 | String() string 15 | } 16 | -------------------------------------------------------------------------------- /incubator/nft/genesis.go: -------------------------------------------------------------------------------- 1 | package nft 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | ) 6 | 7 | // InitGenesis sets nft information for genesis. 8 | func InitGenesis(ctx sdk.Context, k Keeper, data GenesisState) { 9 | k.SetOwners(ctx, data.Owners) 10 | 11 | for _, c := range data.Collections { 12 | sortedCollection := NewCollection(c.Denom, c.NFTs.Sort()) 13 | k.SetCollection(ctx, c.Denom, sortedCollection) 14 | } 15 | } 16 | 17 | // ExportGenesis returns a GenesisState for a given context and keeper. 18 | func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { 19 | return NewGenesisState(k.GetOwners(ctx), k.GetCollections(ctx)) 20 | } 21 | -------------------------------------------------------------------------------- /incubator/nft/genesis_test.go: -------------------------------------------------------------------------------- 1 | package nft_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/cosmos/modules/incubator/nft" 9 | ) 10 | 11 | func TestInitGenesis(t *testing.T) { 12 | app, ctx := createTestApp(false) 13 | genesisState := nft.DefaultGenesisState() 14 | require.Equal(t, 0, len(genesisState.Owners)) 15 | require.Equal(t, 0, len(genesisState.Collections)) 16 | 17 | ids := []string{id, id2, id3} 18 | idCollection := nft.NewIDCollection(denom, ids) 19 | idCollection2 := nft.NewIDCollection(denom2, ids) 20 | owner := nft.NewOwner(address, idCollection) 21 | 22 | owner2 := nft.NewOwner(address2, idCollection2) 23 | 24 | owners := []nft.Owner{owner, owner2} 25 | 26 | nft1 := nft.NewBaseNFT(id, address, tokenURI1) 27 | nft2 := nft.NewBaseNFT(id2, address, tokenURI1) 28 | nft3 := nft.NewBaseNFT(id3, address, tokenURI1) 29 | nfts := nft.NewNFTs(&nft1, &nft2, &nft3) 30 | collection := nft.NewCollection(denom, nfts) 31 | 32 | nftx := nft.NewBaseNFT(id, address2, tokenURI1) 33 | nft2x := nft.NewBaseNFT(id2, address2, tokenURI1) 34 | nft3x := nft.NewBaseNFT(id3, address2, tokenURI1) 35 | nftsx := nft.NewNFTs(&nftx, &nft2x, &nft3x) 36 | collection2 := nft.NewCollection(denom2, nftsx) 37 | 38 | collections := nft.NewCollections(collection, collection2) 39 | 40 | genesisState = nft.NewGenesisState(owners, collections) 41 | 42 | nft.InitGenesis(ctx, app.NFTKeeper, genesisState) 43 | 44 | returnedOwners := app.NFTKeeper.GetOwners(ctx) 45 | require.Equal(t, 2, len(owners)) 46 | require.Equal(t, returnedOwners[0].String(), owners[0].String()) 47 | require.Equal(t, returnedOwners[1].String(), owners[1].String()) 48 | 49 | returnedCollections := app.NFTKeeper.GetCollections(ctx) 50 | require.Equal(t, 2, len(returnedCollections)) 51 | require.Equal(t, returnedCollections[0].String(), collections[0].String()) 52 | require.Equal(t, returnedCollections[1].String(), collections[1].String()) 53 | 54 | exportedGenesisState := nft.ExportGenesis(ctx, app.NFTKeeper) 55 | require.Equal(t, len(genesisState.Owners), len(exportedGenesisState.Owners)) 56 | require.Equal(t, genesisState.Owners[0].String(), exportedGenesisState.Owners[0].String()) 57 | require.Equal(t, genesisState.Owners[1].String(), exportedGenesisState.Owners[1].String()) 58 | 59 | require.Equal(t, len(genesisState.Collections), len(exportedGenesisState.Collections)) 60 | require.Equal(t, genesisState.Collections[0].String(), exportedGenesisState.Collections[0].String()) 61 | require.Equal(t, genesisState.Collections[1].String(), exportedGenesisState.Collections[1].String()) 62 | } 63 | -------------------------------------------------------------------------------- /incubator/nft/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cosmos/modules/incubator/nft 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/cosmos/cosmos-sdk v0.38.4 7 | github.com/gorilla/mux v1.7.3 8 | github.com/spf13/cobra v0.0.6 9 | github.com/spf13/viper v1.6.2 10 | github.com/stretchr/testify v1.5.1 11 | github.com/tendermint/tendermint v0.33.3 12 | github.com/tendermint/tm-db v0.5.0 13 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /incubator/nft/handler.go: -------------------------------------------------------------------------------- 1 | package nft 2 | 3 | import ( 4 | "fmt" 5 | 6 | abci "github.com/tendermint/tendermint/abci/types" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 10 | "github.com/cosmos/modules/incubator/nft/keeper" 11 | "github.com/cosmos/modules/incubator/nft/types" 12 | ) 13 | 14 | // GenericHandler routes the messages to the handlers 15 | func GenericHandler(k keeper.Keeper) sdk.Handler { 16 | return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { 17 | switch msg := msg.(type) { 18 | case types.MsgTransferNFT: 19 | return HandleMsgTransferNFT(ctx, msg, k) 20 | case types.MsgEditNFTMetadata: 21 | return HandleMsgEditNFTMetadata(ctx, msg, k) 22 | case types.MsgMintNFT: 23 | return HandleMsgMintNFT(ctx, msg, k) 24 | case types.MsgBurnNFT: 25 | return HandleMsgBurnNFT(ctx, msg, k) 26 | default: 27 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, fmt.Sprintf("unrecognized nft message type: %T", msg)) 28 | } 29 | } 30 | } 31 | 32 | // HandleMsgTransferNFT handler for MsgTransferNFT 33 | func HandleMsgTransferNFT(ctx sdk.Context, msg types.MsgTransferNFT, k keeper.Keeper, 34 | ) (*sdk.Result, error) { 35 | nft, err := k.GetNFT(ctx, msg.Denom, msg.ID) 36 | if err != nil { 37 | return nil, err 38 | } 39 | // update NFT owner 40 | nft.SetOwner(msg.Recipient) 41 | // update the NFT (owners are updated within the keeper) 42 | err = k.UpdateNFT(ctx, msg.Denom, nft) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | ctx.EventManager().EmitEvents(sdk.Events{ 48 | sdk.NewEvent( 49 | types.EventTypeTransfer, 50 | sdk.NewAttribute(types.AttributeKeyRecipient, msg.Recipient.String()), 51 | sdk.NewAttribute(types.AttributeKeyDenom, msg.Denom), 52 | sdk.NewAttribute(types.AttributeKeyNFTID, msg.ID), 53 | ), 54 | sdk.NewEvent( 55 | sdk.EventTypeMessage, 56 | sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), 57 | sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), 58 | ), 59 | }) 60 | return &sdk.Result{Events: ctx.EventManager().Events()}, nil 61 | } 62 | 63 | // HandleMsgEditNFTMetadata handler for MsgEditNFTMetadata 64 | func HandleMsgEditNFTMetadata(ctx sdk.Context, msg types.MsgEditNFTMetadata, k keeper.Keeper, 65 | ) (*sdk.Result, error) { 66 | nft, err := k.GetNFT(ctx, msg.Denom, msg.ID) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | // update NFT 72 | nft.EditMetadata(msg.TokenURI) 73 | err = k.UpdateNFT(ctx, msg.Denom, nft) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | ctx.EventManager().EmitEvents(sdk.Events{ 79 | sdk.NewEvent( 80 | types.EventTypeEditNFTMetadata, 81 | sdk.NewAttribute(types.AttributeKeyDenom, msg.Denom), 82 | sdk.NewAttribute(types.AttributeKeyNFTID, msg.ID), 83 | sdk.NewAttribute(types.AttributeKeyNFTTokenURI, msg.TokenURI), 84 | ), 85 | sdk.NewEvent( 86 | sdk.EventTypeMessage, 87 | sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), 88 | sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), 89 | ), 90 | }) 91 | return &sdk.Result{Events: ctx.EventManager().Events()}, nil 92 | } 93 | 94 | // HandleMsgMintNFT handles MsgMintNFT 95 | func HandleMsgMintNFT(ctx sdk.Context, msg types.MsgMintNFT, k keeper.Keeper, 96 | ) (*sdk.Result, error) { 97 | nft := types.NewBaseNFT(msg.ID, msg.Recipient, msg.TokenURI) 98 | err := k.MintNFT(ctx, msg.Denom, &nft) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | ctx.EventManager().EmitEvents(sdk.Events{ 104 | sdk.NewEvent( 105 | types.EventTypeMintNFT, 106 | sdk.NewAttribute(types.AttributeKeyRecipient, msg.Recipient.String()), 107 | sdk.NewAttribute(types.AttributeKeyDenom, msg.Denom), 108 | sdk.NewAttribute(types.AttributeKeyNFTID, msg.ID), 109 | sdk.NewAttribute(types.AttributeKeyNFTTokenURI, msg.TokenURI), 110 | ), 111 | sdk.NewEvent( 112 | sdk.EventTypeMessage, 113 | sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), 114 | sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), 115 | ), 116 | }) 117 | return &sdk.Result{Events: ctx.EventManager().Events()}, nil 118 | } 119 | 120 | // HandleMsgBurnNFT handles MsgBurnNFT 121 | func HandleMsgBurnNFT(ctx sdk.Context, msg types.MsgBurnNFT, k keeper.Keeper, 122 | ) (*sdk.Result, error) { 123 | _, err := k.GetNFT(ctx, msg.Denom, msg.ID) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | // remove NFT 129 | err = k.DeleteNFT(ctx, msg.Denom, msg.ID) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | ctx.EventManager().EmitEvents(sdk.Events{ 135 | sdk.NewEvent( 136 | types.EventTypeBurnNFT, 137 | sdk.NewAttribute(types.AttributeKeyDenom, msg.Denom), 138 | sdk.NewAttribute(types.AttributeKeyNFTID, msg.ID), 139 | ), 140 | sdk.NewEvent( 141 | sdk.EventTypeMessage, 142 | sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), 143 | sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), 144 | ), 145 | }) 146 | return &sdk.Result{Events: ctx.EventManager().Events()}, nil 147 | } 148 | 149 | // EndBlocker is run at the end of the block 150 | func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate { 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /incubator/nft/integration_test.go: -------------------------------------------------------------------------------- 1 | package nft_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | abci "github.com/tendermint/tendermint/abci/types" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | "github.com/cosmos/modules/incubator/nft" 10 | simapp "github.com/cosmos/modules/incubator/nft/app" 11 | "github.com/cosmos/modules/incubator/nft/types" 12 | ) 13 | 14 | // nolint: deadcode unused 15 | var ( 16 | denom1 = "test-denom" 17 | denom2 = "test-denom2" 18 | denom3 = "test-denom3" 19 | id = "1" 20 | id2 = "2" 21 | id3 = "3" 22 | address = types.CreateTestAddrs(1)[0] 23 | address2 = types.CreateTestAddrs(2)[1] 24 | address3 = types.CreateTestAddrs(3)[2] 25 | tokenURI1 = "https://google.com/token-1.json" 26 | tokenURI2 = "https://google.com/token-2.json" 27 | ) 28 | 29 | func createTestApp(isCheckTx bool) (*simapp.SimApp, sdk.Context) { 30 | app := simapp.Setup(isCheckTx) 31 | ctx := app.BaseApp.NewContext(isCheckTx, abci.Header{}) 32 | 33 | return app, ctx 34 | } 35 | 36 | // CheckInvariants checks the invariants 37 | func CheckInvariants(k nft.Keeper, ctx sdk.Context) bool { 38 | collectionsSupply := make(map[string]int) 39 | ownersCollectionsSupply := make(map[string]int) 40 | 41 | k.IterateCollections(ctx, func(collection types.Collection) bool { 42 | collectionsSupply[collection.Denom] = collection.Supply() 43 | return false 44 | }) 45 | 46 | owners := k.GetOwners(ctx) 47 | for _, owner := range owners { 48 | for _, idCollection := range owner.IDCollections { 49 | ownersCollectionsSupply[idCollection.Denom] += idCollection.Supply() 50 | } 51 | } 52 | 53 | for denom, supply := range collectionsSupply { 54 | if supply != ownersCollectionsSupply[denom] { 55 | fmt.Printf("denom is %s, supply is %d, ownerSupply is %d", denom, supply, ownersCollectionsSupply[denom]) 56 | return false 57 | } 58 | } 59 | return true 60 | } 61 | -------------------------------------------------------------------------------- /incubator/nft/keeper/collection.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | "github.com/cosmos/modules/incubator/nft/types" 6 | ) 7 | 8 | // IterateCollections iterates over collections and performs a function 9 | func (k Keeper) IterateCollections(ctx sdk.Context, handler func(collection types.Collection) (stop bool)) { 10 | store := ctx.KVStore(k.storeKey) 11 | iterator := sdk.KVStorePrefixIterator(store, types.CollectionsKeyPrefix) 12 | defer iterator.Close() 13 | for ; iterator.Valid(); iterator.Next() { 14 | var collection types.Collection 15 | k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &collection) 16 | if handler(collection) { 17 | break 18 | } 19 | } 20 | } 21 | 22 | // SetCollection sets the entire collection of a single denom 23 | func (k Keeper) SetCollection(ctx sdk.Context, denom string, collection types.Collection) { 24 | store := ctx.KVStore(k.storeKey) 25 | collectionKey := types.GetCollectionKey(denom) 26 | bz := k.cdc.MustMarshalBinaryLengthPrefixed(collection) 27 | store.Set(collectionKey, bz) 28 | } 29 | 30 | // GetCollection returns a collection of NFTs 31 | func (k Keeper) GetCollection(ctx sdk.Context, denom string) (collection types.Collection, found bool) { 32 | store := ctx.KVStore(k.storeKey) 33 | collectionKey := types.GetCollectionKey(denom) 34 | bz := store.Get(collectionKey) 35 | if bz == nil { 36 | return 37 | } 38 | k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &collection) 39 | return collection, true 40 | } 41 | 42 | // GetCollections returns all the NFTs collections 43 | func (k Keeper) GetCollections(ctx sdk.Context) (collections []types.Collection) { 44 | k.IterateCollections(ctx, 45 | func(collection types.Collection) (stop bool) { 46 | collections = append(collections, collection) 47 | return false 48 | }, 49 | ) 50 | return 51 | } 52 | 53 | // GetDenoms returns all the NFT denoms 54 | func (k Keeper) GetDenoms(ctx sdk.Context) (denoms []string) { 55 | k.IterateCollections(ctx, 56 | func(collection types.Collection) (stop bool) { 57 | denoms = append(denoms, collection.Denom) 58 | return false 59 | }, 60 | ) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /incubator/nft/keeper/collection_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/cosmos/modules/incubator/nft/keeper" 9 | "github.com/cosmos/modules/incubator/nft/types" 10 | ) 11 | 12 | func TestSetCollection(t *testing.T) { 13 | app, ctx := createTestApp(false) 14 | 15 | // create a new nft with id = "id" and owner = "address" 16 | // MintNFT shouldn't fail when collection does not exist 17 | nft := types.NewBaseNFT(id, address, tokenURI) 18 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 19 | require.NoError(t, err) 20 | 21 | // collection should exist 22 | collection, exists := app.NFTKeeper.GetCollection(ctx, denom) 23 | require.True(t, exists) 24 | 25 | // create a new NFT and add it to the collection created with the NFT mint 26 | nft2 := types.NewBaseNFT(id2, address, tokenURI) 27 | collection2, err2 := collection.AddNFT(&nft2) 28 | require.NoError(t, err2) 29 | app.NFTKeeper.SetCollection(ctx, denom, collection2) 30 | 31 | collection2, exists = app.NFTKeeper.GetCollection(ctx, denom) 32 | require.True(t, exists) 33 | require.Len(t, collection2.NFTs, 2) 34 | 35 | // reset collection for invariant sanity 36 | app.NFTKeeper.SetCollection(ctx, denom, collection) 37 | 38 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 39 | require.False(t, fail, msg) 40 | } 41 | func TestGetCollection(t *testing.T) { 42 | app, ctx := createTestApp(false) 43 | 44 | // collection shouldn't exist 45 | collection, exists := app.NFTKeeper.GetCollection(ctx, denom) 46 | require.Empty(t, collection) 47 | require.False(t, exists) 48 | 49 | // MintNFT shouldn't fail when collection does not exist 50 | nft := types.NewBaseNFT(id, address, tokenURI) 51 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 52 | require.NoError(t, err) 53 | 54 | // collection should exist 55 | collection, exists = app.NFTKeeper.GetCollection(ctx, denom) 56 | require.True(t, exists) 57 | require.NotEmpty(t, collection) 58 | 59 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 60 | require.False(t, fail, msg) 61 | } 62 | func TestGetCollections(t *testing.T) { 63 | app, ctx := createTestApp(false) 64 | 65 | // collections should be empty 66 | collections := app.NFTKeeper.GetCollections(ctx) 67 | require.Empty(t, collections) 68 | 69 | // MintNFT shouldn't fail when collection does not exist 70 | nft := types.NewBaseNFT(id, address, tokenURI) 71 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 72 | require.NoError(t, err) 73 | 74 | // collections should equal 1 75 | collections = app.NFTKeeper.GetCollections(ctx) 76 | require.NotEmpty(t, collections) 77 | require.Equal(t, len(collections), 1) 78 | 79 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 80 | require.False(t, fail, msg) 81 | } 82 | -------------------------------------------------------------------------------- /incubator/nft/keeper/integration_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | abci "github.com/tendermint/tendermint/abci/types" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | simapp "github.com/cosmos/modules/incubator/nft/app" 8 | "github.com/cosmos/modules/incubator/nft/types" 9 | ) 10 | 11 | // nolint: deadcode unused 12 | var ( 13 | denom = "test-denom" 14 | denom2 = "test-denom2" 15 | denom3 = "test-denom3" 16 | id = "1" 17 | id2 = "2" 18 | id3 = "3" 19 | address = types.CreateTestAddrs(1)[0] 20 | address2 = types.CreateTestAddrs(2)[1] 21 | address3 = types.CreateTestAddrs(3)[2] 22 | tokenURI = "https://google.com/token-1.json" 23 | tokenURI2 = "https://google.com/token-2.json" 24 | ) 25 | 26 | func createTestApp(isCheckTx bool) (*simapp.SimApp, sdk.Context) { 27 | app := simapp.Setup(isCheckTx) 28 | 29 | ctx := app.BaseApp.NewContext(isCheckTx, abci.Header{}) 30 | 31 | return app, ctx 32 | } 33 | -------------------------------------------------------------------------------- /incubator/nft/keeper/invariants.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | // DONTCOVER 4 | 5 | import ( 6 | "fmt" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | "github.com/cosmos/modules/incubator/nft/types" 10 | ) 11 | 12 | // RegisterInvariants registers all supply invariants 13 | func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { 14 | ir.RegisterRoute( 15 | types.ModuleName, "supply", 16 | SupplyInvariant(k), 17 | ) 18 | } 19 | 20 | // AllInvariants runs all invariants of the nfts module. 21 | func AllInvariants(k Keeper) sdk.Invariant { 22 | return func(ctx sdk.Context) (string, bool) { 23 | return SupplyInvariant(k)(ctx) 24 | } 25 | } 26 | 27 | // SupplyInvariant checks that the total amount of nfts on collections matches the total amount owned by addresses 28 | func SupplyInvariant(k Keeper) sdk.Invariant { 29 | return func(ctx sdk.Context) (string, bool) { 30 | collectionsSupply := make(map[string]int) 31 | ownersCollectionsSupply := make(map[string]int) 32 | var msg string 33 | count := 0 34 | 35 | k.IterateCollections(ctx, func(collection types.Collection) bool { 36 | collectionsSupply[collection.Denom] = collection.Supply() 37 | return false 38 | }) 39 | 40 | for _, owner := range k.GetOwners(ctx) { 41 | for _, idCollection := range owner.IDCollections { 42 | ownersCollectionsSupply[idCollection.Denom] += idCollection.Supply() 43 | } 44 | } 45 | 46 | for denom, supply := range collectionsSupply { 47 | if supply != ownersCollectionsSupply[denom] { 48 | count++ 49 | msg += fmt.Sprintf("total %s NFTs supply invariance:\n"+ 50 | "\ttotal %s NFTs supply: %d\n"+ 51 | "\tsum of %s NFTs by owner: %d\n", denom, denom, supply, denom, ownersCollectionsSupply[denom]) 52 | } 53 | } 54 | broken := count != 0 55 | 56 | return sdk.FormatInvariant(types.ModuleName, "supply", fmt.Sprintf( 57 | "%d NFT supply invariants found\n%s", count, msg)), broken 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /incubator/nft/keeper/keeper.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tendermint/tendermint/libs/log" 7 | 8 | "github.com/cosmos/cosmos-sdk/codec" 9 | 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | "github.com/cosmos/modules/incubator/nft/types" 12 | ) 13 | 14 | // Keeper maintains the link to data storage and exposes getter/setter methods for the various parts of the state machine 15 | type Keeper struct { 16 | storeKey sdk.StoreKey // Unexposed key to access store from sdk.Context 17 | 18 | cdc *codec.Codec // The amino codec for binary encoding/decoding. 19 | } 20 | 21 | // NewKeeper creates new instances of the nft Keeper 22 | func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey) Keeper { 23 | return Keeper{ 24 | storeKey: storeKey, 25 | cdc: cdc, 26 | } 27 | } 28 | 29 | // Logger returns a module-specific logger. 30 | func (k Keeper) Logger(ctx sdk.Context) log.Logger { 31 | return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) 32 | } 33 | -------------------------------------------------------------------------------- /incubator/nft/keeper/nft.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 8 | "github.com/cosmos/modules/incubator/nft/exported" 9 | "github.com/cosmos/modules/incubator/nft/types" 10 | ) 11 | 12 | // IsNFT returns whether an NFT exists 13 | func (k Keeper) IsNFT(ctx sdk.Context, denom, id string) (exists bool) { 14 | _, err := k.GetNFT(ctx, denom, id) 15 | return err == nil 16 | } 17 | 18 | // GetNFT gets the entire NFT metadata struct for a uint64 19 | func (k Keeper) GetNFT(ctx sdk.Context, denom, id string) (nft exported.NFT, err error) { 20 | collection, found := k.GetCollection(ctx, denom) 21 | if !found { 22 | return nil, sdkerrors.Wrap(types.ErrUnknownCollection, fmt.Sprintf("collection of %s doesn't exist", denom)) 23 | } 24 | nft, err = collection.GetNFT(id) 25 | 26 | if err != nil { 27 | return nil, err 28 | } 29 | return nft, err 30 | } 31 | 32 | // UpdateNFT updates an already existing NFTs 33 | func (k Keeper) UpdateNFT(ctx sdk.Context, denom string, nft exported.NFT) (err error) { 34 | collection, found := k.GetCollection(ctx, denom) 35 | if !found { 36 | return sdkerrors.Wrap(types.ErrUnknownCollection, fmt.Sprintf("collection #%s doesn't exist", denom)) 37 | } 38 | oldNFT, err := collection.GetNFT(nft.GetID()) 39 | if err != nil { 40 | return err 41 | } 42 | // if the owner changed then update the owners KVStore too 43 | if !oldNFT.GetOwner().Equals(nft.GetOwner()) { 44 | err = k.SwapOwners(ctx, denom, nft.GetID(), oldNFT.GetOwner(), nft.GetOwner()) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | collection, err = collection.UpdateNFT(nft) 50 | 51 | if err != nil { 52 | return err 53 | } 54 | k.SetCollection(ctx, denom, collection) 55 | return nil 56 | } 57 | 58 | // MintNFT mints an NFT and manages that NFTs existence within Collections and Owners 59 | func (k Keeper) MintNFT(ctx sdk.Context, denom string, nft exported.NFT) (err error) { 60 | collection, found := k.GetCollection(ctx, denom) 61 | if found { 62 | collection, err = collection.AddNFT(nft) 63 | if err != nil { 64 | return err 65 | } 66 | } else { 67 | collection = types.NewCollection(denom, types.NewNFTs(nft)) 68 | } 69 | k.SetCollection(ctx, denom, collection) 70 | 71 | ownerIDCollection, _ := k.GetOwnerByDenom(ctx, nft.GetOwner(), denom) 72 | ownerIDCollection = ownerIDCollection.AddID(nft.GetID()) 73 | k.SetOwnerByDenom(ctx, nft.GetOwner(), denom, ownerIDCollection.IDs) 74 | return 75 | } 76 | 77 | // DeleteNFT deletes an existing NFT from store 78 | func (k Keeper) DeleteNFT(ctx sdk.Context, denom, id string) (err error) { 79 | collection, found := k.GetCollection(ctx, denom) 80 | if !found { 81 | return sdkerrors.Wrap(types.ErrUnknownCollection, fmt.Sprintf("collection of %s doesn't exist", denom)) 82 | } 83 | nft, err := collection.GetNFT(id) 84 | if err != nil { 85 | return err 86 | } 87 | ownerIDCollection, found := k.GetOwnerByDenom(ctx, nft.GetOwner(), denom) 88 | if !found { 89 | return sdkerrors.Wrap(types.ErrUnknownCollection, 90 | fmt.Sprintf("id collection #%s doesn't exist for owner %s", denom, nft.GetOwner()), 91 | ) 92 | } 93 | ownerIDCollection, err = ownerIDCollection.DeleteID(nft.GetID()) 94 | if err != nil { 95 | return err 96 | } 97 | k.SetOwnerByDenom(ctx, nft.GetOwner(), denom, ownerIDCollection.IDs) 98 | 99 | collection, err = collection.DeleteNFT(nft) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | k.SetCollection(ctx, denom, collection) 105 | 106 | return 107 | } 108 | -------------------------------------------------------------------------------- /incubator/nft/keeper/nft_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/cosmos/modules/incubator/nft/keeper" 9 | "github.com/cosmos/modules/incubator/nft/types" 10 | ) 11 | 12 | func TestMintNFT(t *testing.T) { 13 | app, ctx := createTestApp(false) 14 | 15 | // MintNFT shouldn't fail when collection does not exist 16 | nft := types.NewBaseNFT(id, address, tokenURI) 17 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 18 | require.NoError(t, err) 19 | 20 | // MintNFT shouldn't fail when collection exists 21 | nft2 := types.NewBaseNFT(id2, address, tokenURI) 22 | err = app.NFTKeeper.MintNFT(ctx, denom, &nft2) 23 | require.NoError(t, err) 24 | } 25 | 26 | func TestGetNFT(t *testing.T) { 27 | app, ctx := createTestApp(false) 28 | 29 | // MintNFT shouldn't fail when collection does not exist 30 | nft := types.NewBaseNFT(id, address, tokenURI) 31 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 32 | require.NoError(t, err) 33 | 34 | // GetNFT should get the NFT 35 | receivedNFT, err := app.NFTKeeper.GetNFT(ctx, denom, id) 36 | require.NoError(t, err) 37 | require.Equal(t, receivedNFT.GetID(), id) 38 | require.True(t, receivedNFT.GetOwner().Equals(address)) 39 | require.Equal(t, receivedNFT.GetTokenURI(), tokenURI) 40 | 41 | // MintNFT shouldn't fail when collection exists 42 | nft2 := types.NewBaseNFT(id2, address, tokenURI) 43 | err = app.NFTKeeper.MintNFT(ctx, denom, &nft2) 44 | require.NoError(t, err) 45 | 46 | // GetNFT should get the NFT when collection exists 47 | receivedNFT2, err := app.NFTKeeper.GetNFT(ctx, denom, id2) 48 | require.NoError(t, err) 49 | require.Equal(t, receivedNFT2.GetID(), id2) 50 | require.True(t, receivedNFT2.GetOwner().Equals(address)) 51 | require.Equal(t, receivedNFT2.GetTokenURI(), tokenURI) 52 | 53 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 54 | require.False(t, fail, msg) 55 | } 56 | 57 | func TestUpdateNFT(t *testing.T) { 58 | app, ctx := createTestApp(false) 59 | 60 | nft := types.NewBaseNFT(id, address, tokenURI) 61 | 62 | // UpdateNFT should fail when NFT doesn't exists 63 | err := app.NFTKeeper.UpdateNFT(ctx, denom, &nft) 64 | require.Error(t, err) 65 | 66 | // MintNFT shouldn't fail when collection does not exist 67 | err = app.NFTKeeper.MintNFT(ctx, denom, &nft) 68 | require.NoError(t, err) 69 | 70 | nonnft := types.NewBaseNFT(id2, address, tokenURI) 71 | // UpdateNFT should fail when NFT doesn't exists 72 | err = app.NFTKeeper.UpdateNFT(ctx, denom, &nonnft) 73 | require.Error(t, err) 74 | 75 | // UpdateNFT shouldn't fail when NFT exists 76 | nft2 := types.NewBaseNFT(id, address, tokenURI2) 77 | err = app.NFTKeeper.UpdateNFT(ctx, denom, &nft2) 78 | require.NoError(t, err) 79 | 80 | // UpdateNFT shouldn't fail when NFT exists 81 | nft2 = types.NewBaseNFT(id, address2, tokenURI2) 82 | err = app.NFTKeeper.UpdateNFT(ctx, denom, &nft2) 83 | require.NoError(t, err) 84 | 85 | // GetNFT should get the NFT with new tokenURI 86 | receivedNFT, err := app.NFTKeeper.GetNFT(ctx, denom, id) 87 | require.NoError(t, err) 88 | require.Equal(t, receivedNFT.GetTokenURI(), tokenURI2) 89 | } 90 | 91 | func TestDeleteNFT(t *testing.T) { 92 | app, ctx := createTestApp(false) 93 | 94 | // DeleteNFT should fail when NFT doesn't exist and collection doesn't exist 95 | err := app.NFTKeeper.DeleteNFT(ctx, denom, id) 96 | require.Error(t, err) 97 | 98 | // MintNFT should not fail when collection does not exist 99 | nft := types.NewBaseNFT(id, address, tokenURI) 100 | err = app.NFTKeeper.MintNFT(ctx, denom, &nft) 101 | require.NoError(t, err) 102 | 103 | // DeleteNFT should fail when NFT doesn't exist but collection does exist 104 | err = app.NFTKeeper.DeleteNFT(ctx, denom, id2) 105 | require.Error(t, err) 106 | 107 | // DeleteNFT should not fail when NFT and collection exist 108 | err = app.NFTKeeper.DeleteNFT(ctx, denom, id) 109 | require.NoError(t, err) 110 | 111 | // NFT should no longer exist 112 | isNFT := app.NFTKeeper.IsNFT(ctx, denom, id) 113 | require.False(t, isNFT) 114 | 115 | owner := app.NFTKeeper.GetOwner(ctx, address) 116 | require.Equal(t, 0, owner.Supply()) 117 | 118 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 119 | require.False(t, fail, msg) 120 | } 121 | 122 | func TestIsNFT(t *testing.T) { 123 | app, ctx := createTestApp(false) 124 | 125 | // IsNFT should return false 126 | isNFT := app.NFTKeeper.IsNFT(ctx, denom, id) 127 | require.False(t, isNFT) 128 | 129 | // MintNFT shouldn't fail when collection does not exist 130 | nft := types.NewBaseNFT(id, address, tokenURI) 131 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 132 | require.NoError(t, err) 133 | 134 | // IsNFT should return true 135 | isNFT = app.NFTKeeper.IsNFT(ctx, denom, id) 136 | require.True(t, isNFT) 137 | } 138 | -------------------------------------------------------------------------------- /incubator/nft/keeper/owners.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 8 | "github.com/cosmos/modules/incubator/nft/types" 9 | ) 10 | 11 | // GetOwners returns all the Owners ID Collections 12 | func (k Keeper) GetOwners(ctx sdk.Context) (owners []types.Owner) { 13 | var foundOwners = make(map[string]bool) 14 | k.IterateOwners(ctx, 15 | func(owner types.Owner) (stop bool) { 16 | if _, ok := foundOwners[owner.Address.String()]; !ok { 17 | foundOwners[owner.Address.String()] = true 18 | owners = append(owners, owner) 19 | } 20 | return false 21 | }, 22 | ) 23 | return 24 | } 25 | 26 | // GetOwner gets all the ID Collections owned by an address 27 | func (k Keeper) GetOwner(ctx sdk.Context, address sdk.AccAddress) (owner types.Owner) { 28 | var idCollections []types.IDCollection 29 | k.IterateIDCollections(ctx, types.GetOwnersKey(address), 30 | func(_ sdk.AccAddress, idCollection types.IDCollection) (stop bool) { 31 | idCollections = append(idCollections, idCollection) 32 | return false 33 | }, 34 | ) 35 | return types.NewOwner(address, idCollections...) 36 | } 37 | 38 | // GetOwnerByDenom gets the ID Collection owned by an address of a specific denom 39 | func (k Keeper) GetOwnerByDenom(ctx sdk.Context, owner sdk.AccAddress, denom string) (idCollection types.IDCollection, found bool) { 40 | store := ctx.KVStore(k.storeKey) 41 | b := store.Get(types.GetOwnerKey(owner, denom)) 42 | if b == nil { 43 | return types.NewIDCollection(denom, []string{}), false 44 | } 45 | k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &idCollection) 46 | return idCollection, true 47 | } 48 | 49 | // SetOwnerByDenom sets a collection of NFT IDs owned by an address 50 | func (k Keeper) SetOwnerByDenom(ctx sdk.Context, owner sdk.AccAddress, denom string, ids []string) { 51 | store := ctx.KVStore(k.storeKey) 52 | key := types.GetOwnerKey(owner, denom) 53 | 54 | var idCollection types.IDCollection 55 | idCollection.Denom = denom 56 | idCollection.IDs = ids 57 | 58 | store.Set(key, k.cdc.MustMarshalBinaryLengthPrefixed(idCollection)) 59 | } 60 | 61 | // SetOwner sets an entire Owner 62 | func (k Keeper) SetOwner(ctx sdk.Context, owner types.Owner) { 63 | for _, idCollection := range owner.IDCollections { 64 | k.SetOwnerByDenom(ctx, owner.Address, idCollection.Denom, idCollection.IDs) 65 | } 66 | } 67 | 68 | // SetOwners sets all Owners 69 | func (k Keeper) SetOwners(ctx sdk.Context, owners []types.Owner) { 70 | for _, owner := range owners { 71 | k.SetOwner(ctx, owner) 72 | } 73 | } 74 | 75 | // IterateIDCollections iterates over the IDCollections by Owner and performs a function 76 | func (k Keeper) IterateIDCollections(ctx sdk.Context, prefix []byte, 77 | handler func(owner sdk.AccAddress, idCollection types.IDCollection) (stop bool)) { 78 | store := ctx.KVStore(k.storeKey) 79 | iterator := sdk.KVStorePrefixIterator(store, prefix) 80 | defer iterator.Close() 81 | for ; iterator.Valid(); iterator.Next() { 82 | var idCollection types.IDCollection 83 | k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &idCollection) 84 | 85 | owner, _ := types.SplitOwnerKey(iterator.Key()) 86 | if handler(owner, idCollection) { 87 | break 88 | } 89 | } 90 | } 91 | 92 | // IterateOwners iterates over all Owners and performs a function 93 | func (k Keeper) IterateOwners(ctx sdk.Context, handler func(owner types.Owner) (stop bool)) { 94 | store := ctx.KVStore(k.storeKey) 95 | iterator := sdk.KVStorePrefixIterator(store, types.OwnersKeyPrefix) 96 | defer iterator.Close() 97 | for ; iterator.Valid(); iterator.Next() { 98 | var owner types.Owner 99 | 100 | address, _ := types.SplitOwnerKey(iterator.Key()) 101 | owner = k.GetOwner(ctx, address) 102 | 103 | if handler(owner) { 104 | break 105 | } 106 | } 107 | } 108 | 109 | // SwapOwners swaps the owners of a NFT ID 110 | func (k Keeper) SwapOwners(ctx sdk.Context, denom string, id string, oldAddress sdk.AccAddress, newAddress sdk.AccAddress) (err error) { 111 | oldOwnerIDCollection, found := k.GetOwnerByDenom(ctx, oldAddress, denom) 112 | if !found { 113 | return sdkerrors.Wrap(types.ErrUnknownCollection, 114 | fmt.Sprintf("id collection %s doesn't exist for owner %s", denom, oldAddress), 115 | ) 116 | } 117 | oldOwnerIDCollection, err = oldOwnerIDCollection.DeleteID(id) 118 | if err != nil { 119 | return err 120 | } 121 | k.SetOwnerByDenom(ctx, oldAddress, denom, oldOwnerIDCollection.IDs) 122 | 123 | newOwnerIDCollection, found := k.GetOwnerByDenom(ctx, newAddress, denom) 124 | if !found { 125 | newOwnerIDCollection = types.NewIDCollection(denom, []string{}) 126 | } 127 | newOwnerIDCollection = newOwnerIDCollection.AddID(id) 128 | k.SetOwnerByDenom(ctx, newAddress, denom, newOwnerIDCollection.IDs) 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /incubator/nft/keeper/owners_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/cosmos/modules/incubator/nft/keeper" 9 | "github.com/cosmos/modules/incubator/nft/types" 10 | ) 11 | 12 | func TestGetOwners(t *testing.T) { 13 | app, ctx := createTestApp(false) 14 | 15 | nft := types.NewBaseNFT(id, address, tokenURI) 16 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 17 | require.NoError(t, err) 18 | 19 | nft2 := types.NewBaseNFT(id2, address2, tokenURI) 20 | err = app.NFTKeeper.MintNFT(ctx, denom, &nft2) 21 | require.NoError(t, err) 22 | 23 | nft3 := types.NewBaseNFT(id3, address3, tokenURI) 24 | err = app.NFTKeeper.MintNFT(ctx, denom, &nft3) 25 | require.NoError(t, err) 26 | 27 | owners := app.NFTKeeper.GetOwners(ctx) 28 | require.Equal(t, 3, len(owners)) 29 | 30 | nft = types.NewBaseNFT(id, address, tokenURI) 31 | err = app.NFTKeeper.MintNFT(ctx, denom2, &nft) 32 | require.NoError(t, err) 33 | 34 | nft2 = types.NewBaseNFT(id2, address2, tokenURI) 35 | err = app.NFTKeeper.MintNFT(ctx, denom2, &nft2) 36 | require.NoError(t, err) 37 | 38 | nft3 = types.NewBaseNFT(id3, address3, tokenURI) 39 | err = app.NFTKeeper.MintNFT(ctx, denom2, &nft3) 40 | require.NoError(t, err) 41 | 42 | owners = app.NFTKeeper.GetOwners(ctx) 43 | require.Equal(t, 3, len(owners)) 44 | 45 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 46 | require.False(t, fail, msg) 47 | } 48 | 49 | func TestSetOwner(t *testing.T) { 50 | app, ctx := createTestApp(false) 51 | 52 | nft := types.NewBaseNFT(id, address, tokenURI) 53 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 54 | require.NoError(t, err) 55 | 56 | idCollection := types.NewIDCollection(denom, []string{id, id2, id3}) 57 | owner := types.NewOwner(address, idCollection) 58 | 59 | oldOwner := app.NFTKeeper.GetOwner(ctx, address) 60 | 61 | app.NFTKeeper.SetOwner(ctx, owner) 62 | 63 | newOwner := app.NFTKeeper.GetOwner(ctx, address) 64 | require.NotEqual(t, oldOwner.String(), newOwner.String()) 65 | require.Equal(t, owner.String(), newOwner.String()) 66 | 67 | // for invariant sanity 68 | app.NFTKeeper.SetOwner(ctx, oldOwner) 69 | 70 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 71 | require.False(t, fail, msg) 72 | } 73 | 74 | func TestSetOwners(t *testing.T) { 75 | app, ctx := createTestApp(false) 76 | 77 | // create NFT where id = "id" with owner = "address" 78 | nft := types.NewBaseNFT(id, address, tokenURI) 79 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 80 | require.NoError(t, err) 81 | 82 | // create NFT where id = "id2" with owner = "address2" 83 | nft = types.NewBaseNFT(id2, address2, tokenURI) 84 | err = app.NFTKeeper.MintNFT(ctx, denom, &nft) 85 | require.NoError(t, err) 86 | 87 | // create two owners (address and address2) with the same id collections of "id", "id2" & "id3" 88 | idCollection := types.NewIDCollection(denom, []string{id, id2, id3}) 89 | owner := types.NewOwner(address, idCollection) 90 | owner2 := types.NewOwner(address2, idCollection) 91 | 92 | // get both owners that were created during the NFT mint process 93 | oldOwner := app.NFTKeeper.GetOwner(ctx, address) 94 | oldOwner2 := app.NFTKeeper.GetOwner(ctx, address2) 95 | 96 | // replace previous old owners with updated versions (that have multiple ids) 97 | app.NFTKeeper.SetOwners(ctx, []types.Owner{owner, owner2}) 98 | 99 | newOwner := app.NFTKeeper.GetOwner(ctx, address) 100 | require.NotEqual(t, oldOwner.String(), newOwner.String()) 101 | require.Equal(t, owner.String(), newOwner.String()) 102 | 103 | newOwner2 := app.NFTKeeper.GetOwner(ctx, address2) 104 | require.NotEqual(t, oldOwner2.String(), newOwner2.String()) 105 | require.Equal(t, owner2.String(), newOwner2.String()) 106 | 107 | // replace old owners for invariance sanity 108 | app.NFTKeeper.SetOwners(ctx, []types.Owner{oldOwner, oldOwner2}) 109 | 110 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 111 | require.False(t, fail, msg) 112 | } 113 | 114 | func TestSwapOwners(t *testing.T) { 115 | app, ctx := createTestApp(false) 116 | 117 | nft := types.NewBaseNFT(id, address, tokenURI) 118 | err := app.NFTKeeper.MintNFT(ctx, denom, &nft) 119 | require.NoError(t, err) 120 | 121 | err = app.NFTKeeper.SwapOwners(ctx, denom, id, address, address2) 122 | require.NoError(t, err) 123 | 124 | err = app.NFTKeeper.SwapOwners(ctx, denom, id, address, address2) 125 | require.Error(t, err) 126 | 127 | err = app.NFTKeeper.SwapOwners(ctx, denom2, id, address, address2) 128 | require.Error(t, err) 129 | 130 | msg, fail := keeper.SupplyInvariant(app.NFTKeeper)(ctx) 131 | require.False(t, fail, msg) 132 | } 133 | -------------------------------------------------------------------------------- /incubator/nft/keeper/querier.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 9 | "github.com/cosmos/modules/incubator/nft/types" 10 | 11 | abci "github.com/tendermint/tendermint/abci/types" 12 | ) 13 | 14 | // query endpoints supported by the NFT Querier 15 | const ( 16 | QuerySupply = "supply" 17 | QueryOwner = "owner" 18 | QueryOwnerByDenom = "ownerByDenom" 19 | QueryCollection = "collection" 20 | QueryDenoms = "denoms" 21 | QueryNFT = "nft" 22 | ) 23 | 24 | // NewQuerier is the module level router for state queries 25 | func NewQuerier(k Keeper) sdk.Querier { 26 | return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) { 27 | switch path[0] { 28 | case QuerySupply: 29 | return querySupply(ctx, path[1:], req, k) 30 | case QueryOwner: 31 | return queryOwner(ctx, path[1:], req, k) 32 | case QueryOwnerByDenom: 33 | return queryOwnerByDenom(ctx, path[1:], req, k) 34 | case QueryCollection: 35 | return queryCollection(ctx, path[1:], req, k) 36 | case QueryDenoms: 37 | return queryDenoms(ctx, path[1:], req, k) 38 | case QueryNFT: 39 | return queryNFT(ctx, path[1:], req, k) 40 | default: 41 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown nft query endpoint") 42 | } 43 | } 44 | } 45 | 46 | func querySupply(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, error) { 47 | var params types.QueryCollectionParams 48 | 49 | err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) 50 | if err != nil { 51 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, fmt.Sprintf("incorrectly formatted request data %v", err.Error())) 52 | } 53 | 54 | collection, found := k.GetCollection(ctx, params.Denom) 55 | if !found { 56 | return nil, sdkerrors.Wrap(types.ErrUnknownCollection, fmt.Sprintf("unknown denom %s", params.Denom)) 57 | } 58 | 59 | bz := []byte(strconv.Itoa(collection.Supply())) 60 | return bz, nil 61 | } 62 | 63 | func queryOwner(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, error) { 64 | var params types.QueryBalanceParams 65 | 66 | err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) 67 | if err != nil { 68 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, err.Error()) 69 | } 70 | 71 | owner := k.GetOwner(ctx, params.Owner) 72 | bz, err := types.ModuleCdc.MarshalJSON(owner) 73 | if err != nil { 74 | return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) 75 | } 76 | 77 | return bz, nil 78 | } 79 | 80 | func queryOwnerByDenom(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, error) { 81 | var params types.QueryBalanceParams 82 | 83 | err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) 84 | if err != nil { 85 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, err.Error()) 86 | } 87 | 88 | var owner types.Owner 89 | 90 | idCollection, _ := k.GetOwnerByDenom(ctx, params.Owner, params.Denom) 91 | owner.Address = params.Owner 92 | owner.IDCollections = append(owner.IDCollections, idCollection).Sort() 93 | 94 | bz, err := types.ModuleCdc.MarshalJSON(owner) 95 | if err != nil { 96 | return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) 97 | } 98 | 99 | return bz, nil 100 | } 101 | 102 | func queryCollection(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, error) { 103 | var params types.QueryCollectionParams 104 | 105 | err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) 106 | if err != nil { 107 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, err.Error()) 108 | } 109 | 110 | collection, found := k.GetCollection(ctx, params.Denom) 111 | if !found { 112 | return nil, sdkerrors.Wrap(types.ErrUnknownCollection, fmt.Sprintf("unknown denom %s", params.Denom)) 113 | } 114 | 115 | // use Collections custom JSON to make the denom the key of the object 116 | collections := types.NewCollections(collection) 117 | bz, err := types.ModuleCdc.MarshalJSON(collections) 118 | if err != nil { 119 | return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) 120 | } 121 | 122 | return bz, nil 123 | } 124 | 125 | func queryDenoms(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, error) { 126 | denoms := k.GetDenoms(ctx) 127 | 128 | bz, err := types.ModuleCdc.MarshalJSON(denoms) 129 | if err != nil { 130 | return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) 131 | } 132 | 133 | return bz, nil 134 | } 135 | 136 | func queryNFT(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, error) { 137 | var params types.QueryNFTParams 138 | 139 | err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms) 140 | if err != nil { 141 | return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, err.Error()) 142 | } 143 | 144 | nft, err := k.GetNFT(ctx, params.Denom, params.TokenID) 145 | if err != nil { 146 | return nil, sdkerrors.Wrap(types.ErrUnknownNFT, fmt.Sprintf("invalid NFT #%s from collection %s", params.TokenID, params.Denom)) 147 | } 148 | 149 | bz, err := types.ModuleCdc.MarshalJSON(nft) 150 | if err != nil { 151 | return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) 152 | } 153 | 154 | return bz, nil 155 | } 156 | -------------------------------------------------------------------------------- /incubator/nft/module.go: -------------------------------------------------------------------------------- 1 | package nft 2 | 3 | // DONTCOVER 4 | 5 | import ( 6 | "encoding/json" 7 | "math/rand" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/spf13/cobra" 11 | 12 | abci "github.com/tendermint/tendermint/abci/types" 13 | 14 | "github.com/cosmos/cosmos-sdk/client/context" 15 | "github.com/cosmos/cosmos-sdk/codec" 16 | sdk "github.com/cosmos/cosmos-sdk/types" 17 | "github.com/cosmos/cosmos-sdk/types/module" 18 | sim "github.com/cosmos/cosmos-sdk/x/simulation" 19 | "github.com/cosmos/modules/incubator/nft/client/cli" 20 | "github.com/cosmos/modules/incubator/nft/client/rest" 21 | "github.com/cosmos/modules/incubator/nft/simulation" 22 | "github.com/cosmos/modules/incubator/nft/types" 23 | ) 24 | 25 | var ( 26 | _ module.AppModule = AppModule{} 27 | _ module.AppModuleBasic = AppModuleBasic{} 28 | _ module.AppModuleSimulation = AppModule{} 29 | ) 30 | 31 | // AppModuleBasic app module basics object 32 | type AppModuleBasic struct{} 33 | 34 | var _ module.AppModuleBasic = AppModuleBasic{} 35 | 36 | // Name defines module name 37 | func (AppModuleBasic) Name() string { 38 | return ModuleName 39 | } 40 | 41 | // RegisterCodec registers module codec 42 | func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { 43 | RegisterCodec(cdc) 44 | } 45 | 46 | // DefaultGenesis default genesis state 47 | func (AppModuleBasic) DefaultGenesis() json.RawMessage { 48 | return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) 49 | } 50 | 51 | // ValidateGenesis module validate genesis 52 | func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { 53 | var data GenesisState 54 | err := ModuleCdc.UnmarshalJSON(bz, &data) 55 | if err != nil { 56 | return err 57 | } 58 | return ValidateGenesis(data) 59 | } 60 | 61 | // RegisterRESTRoutes registers rest routes 62 | func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { 63 | rest.RegisterRoutes(ctx, rtr, ModuleCdc, RouterKey) 64 | } 65 | 66 | // GetTxCmd gets the root tx command of this module 67 | func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { 68 | return cli.GetTxCmd(StoreKey, cdc) 69 | } 70 | 71 | // GetQueryCmd gets the root query command of this module 72 | func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { 73 | return cli.GetQueryCmd(StoreKey, cdc) 74 | } 75 | 76 | //____________________________________________________________________________ 77 | 78 | // AppModule supply app module 79 | type AppModule struct { 80 | AppModuleBasic 81 | 82 | keeper Keeper 83 | 84 | // Account keeper is used for testing purposes only 85 | accountKeeper types.AccountKeeper 86 | } 87 | 88 | // NewAppModule creates a new AppModule object 89 | func NewAppModule(keeper Keeper, accountKeeper types.AccountKeeper) AppModule { 90 | return AppModule{ 91 | AppModuleBasic: AppModuleBasic{}, 92 | 93 | keeper: keeper, 94 | accountKeeper: accountKeeper, 95 | } 96 | } 97 | 98 | // Name defines module name 99 | func (AppModule) Name() string { 100 | return ModuleName 101 | } 102 | 103 | // RegisterInvariants registers the nft module invariants 104 | func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { 105 | RegisterInvariants(ir, am.keeper) 106 | } 107 | 108 | // Route module message route name 109 | func (AppModule) Route() string { 110 | return RouterKey 111 | } 112 | 113 | // NewHandler module handler 114 | func (am AppModule) NewHandler() sdk.Handler { 115 | return GenericHandler(am.keeper) 116 | } 117 | 118 | // QuerierRoute module querier route name 119 | func (AppModule) QuerierRoute() string { 120 | return QuerierRoute 121 | } 122 | 123 | // NewQuerierHandler module querier 124 | func (am AppModule) NewQuerierHandler() sdk.Querier { 125 | return NewQuerier(am.keeper) 126 | } 127 | 128 | // InitGenesis module init-genesis 129 | func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { 130 | var genesisState GenesisState 131 | ModuleCdc.MustUnmarshalJSON(data, &genesisState) 132 | InitGenesis(ctx, am.keeper, genesisState) 133 | return []abci.ValidatorUpdate{} 134 | } 135 | 136 | // ExportGenesis module export genesis 137 | func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { 138 | gs := ExportGenesis(ctx, am.keeper) 139 | return ModuleCdc.MustMarshalJSON(gs) 140 | } 141 | 142 | // BeginBlock module begin-block 143 | func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} 144 | 145 | // EndBlock module end-block 146 | func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { 147 | return EndBlocker(ctx, am.keeper) 148 | } 149 | 150 | // RegisterStoreDecoder registers a decoder for nft module's types 151 | func (AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { 152 | sdr[StoreKey] = simulation.DecodeStore 153 | } 154 | 155 | // ProposalContents doesn't return any content functions for governance proposals. 156 | func (AppModule) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent { return nil } 157 | 158 | // GenerateGenesisState creates a randomized GenState of the nft module. 159 | func (AppModule) GenerateGenesisState(simState *module.SimulationState) { 160 | simulation.RandomizedGenState(simState) 161 | } 162 | 163 | // RandomizedParams doesn't create randomized nft param changes for the simulator. 164 | func (AppModule) RandomizedParams(_ *rand.Rand) []sim.ParamChange { return nil } 165 | 166 | // WeightedOperations doesn't return any operation for the nft module. 167 | func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation { 168 | return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper) 169 | } 170 | -------------------------------------------------------------------------------- /incubator/nft/simulation/decoder.go: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | kv "github.com/tendermint/tendermint/libs/kv" 8 | 9 | "github.com/cosmos/cosmos-sdk/codec" 10 | "github.com/cosmos/modules/incubator/nft/types" 11 | ) 12 | 13 | // DecodeStore unmarshals the KVPair's Value to the corresponding gov type 14 | func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string { 15 | switch { 16 | case bytes.Equal(kvA.Key[:1], types.CollectionsKeyPrefix): 17 | var collectionA, collectionB types.Collection 18 | cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &collectionA) 19 | cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &collectionB) 20 | return fmt.Sprintf("%v\n%v", collectionA, collectionB) 21 | 22 | case bytes.Equal(kvA.Key[:1], types.OwnersKeyPrefix): 23 | var idCollectionA, idCollectionB types.IDCollection 24 | cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &idCollectionA) 25 | cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &idCollectionB) 26 | return fmt.Sprintf("%v\n%v", idCollectionA, idCollectionB) 27 | 28 | default: 29 | panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1])) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /incubator/nft/simulation/decoder_test.go: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/tendermint/tendermint/crypto/ed25519" 10 | kv "github.com/tendermint/tendermint/libs/kv" 11 | 12 | "github.com/cosmos/cosmos-sdk/codec" 13 | sdk "github.com/cosmos/cosmos-sdk/types" 14 | "github.com/cosmos/modules/incubator/nft/types" 15 | ) 16 | 17 | var ( 18 | delPk1 = ed25519.GenPrivKey().PubKey() 19 | addr = sdk.AccAddress(delPk1.Address()) 20 | ) 21 | 22 | func makeTestCodec() (cdc *codec.Codec) { 23 | cdc = codec.New() 24 | sdk.RegisterCodec(cdc) 25 | types.RegisterCodec(cdc) 26 | return 27 | } 28 | 29 | func TestDecodeStore(t *testing.T) { 30 | cdc := makeTestCodec() 31 | nft := types.NewBaseNFT("1", addr, "token URI") 32 | collection := types.NewCollection("kitties", types.NFTs{&nft}) 33 | idCollection := types.NewIDCollection("kitties", []string{"1", "2", "3"}) 34 | 35 | kvPairs := kv.Pairs{ 36 | kv.Pair{Key: types.GetCollectionKey("kitties"), Value: cdc.MustMarshalBinaryLengthPrefixed(collection)}, 37 | kv.Pair{Key: types.GetOwnerKey(addr, "kitties"), Value: cdc.MustMarshalBinaryLengthPrefixed(idCollection)}, 38 | kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}}, 39 | } 40 | 41 | tests := []struct { 42 | name string 43 | expectedLog string 44 | }{ 45 | {"collections", fmt.Sprintf("%v\n%v", collection, collection)}, 46 | {"owners", fmt.Sprintf("%v\n%v", idCollection, idCollection)}, 47 | {"other", ""}, 48 | } 49 | 50 | for i, tt := range tests { 51 | tt, i := tt, i 52 | t.Run(tt.name, func(t *testing.T) { 53 | switch i { 54 | case len(tests) - 1: 55 | require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name) 56 | default: 57 | require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /incubator/nft/simulation/genesis.go: -------------------------------------------------------------------------------- 1 | package simulation 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cosmos/cosmos-sdk/codec" 7 | "github.com/cosmos/cosmos-sdk/types/module" 8 | "github.com/cosmos/cosmos-sdk/x/simulation" 9 | "github.com/cosmos/modules/incubator/nft/types" 10 | ) 11 | 12 | const ( 13 | kitties = "crypto-kitties" 14 | doggos = "crypto-doggos" 15 | ) 16 | 17 | // RandomizedGenState generates a random GenesisState for nft 18 | func RandomizedGenState(simState *module.SimulationState) { 19 | collections := types.NewCollections(types.NewCollection(doggos, types.NFTs{}), types.NewCollection(kitties, types.NFTs{})) 20 | var ownerships []types.Owner 21 | for _, acc := range simState.Accounts { 22 | // 10% of accounts own an NFT 23 | if simState.Rand.Intn(100) < 10 { 24 | baseNFT := types.NewBaseNFT( 25 | simulation.RandStringOfLength(simState.Rand, 10), // id 26 | acc.Address, 27 | simulation.RandStringOfLength(simState.Rand, 45), // tokenURI 28 | ) 29 | 30 | var ( 31 | idCollection types.IDCollection 32 | err error 33 | ) 34 | 35 | // 50% doggos and 50% kitties 36 | if simState.Rand.Intn(100) < 50 { 37 | collections[0], err = collections[0].AddNFT(&baseNFT) 38 | if err != nil { 39 | panic(err) 40 | } 41 | idCollection = types.NewIDCollection(doggos, []string{baseNFT.ID}) 42 | } else { 43 | collections[1], err = collections[1].AddNFT(&baseNFT) 44 | if err != nil { 45 | panic(err) 46 | } 47 | idCollection = types.NewIDCollection(kitties, []string{baseNFT.ID}) 48 | } 49 | 50 | ownership := types.NewOwner(acc.Address, idCollection) 51 | ownerships = append(ownerships, ownership) 52 | } 53 | } 54 | 55 | nftGenesis := types.NewGenesisState(ownerships, collections) 56 | 57 | fmt.Printf("Selected randomly generated NFT genesis state:\n%s\n", codec.MustMarshalJSONIndent(simState.Cdc, nftGenesis)) 58 | simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(nftGenesis) 59 | } 60 | -------------------------------------------------------------------------------- /incubator/nft/spec/01_concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | ## NFT 4 | 5 | The `NFT` Interface inherits the BaseNFT struct and includes getter functions for the asset data. It also includes a Stringer function in order to print the struct. The interface may change if metadata is moved to it’s own module as it might no longer be necessary for the flexibility of an interface. 6 | 7 | ```go 8 | // NFT non fungible token interface 9 | type NFT interface { 10 | GetID() string // unique identifier of the NFT 11 | GetOwner() sdk.AccAddress // gets owner account of the NFT 12 | SetOwner(address sdk.AccAddress) // gets owner account of the NFT 13 | GetTokenURI() string // metadata field: URI to retrieve the of chain metadata of the NFT 14 | EditMetadata(tokenURI string) // edit metadata of the NFT 15 | String() string // string representation of the NFT object 16 | } 17 | ``` 18 | 19 | ## Collections 20 | 21 | A Collection is used to organized sets of NFTs. It contains the denomination of the NFT instead of storing it within each NFT. This saves storage space by removing redundancy. 22 | 23 | ```go 24 | // Collection of non fungible tokens 25 | type Collection struct { 26 | Denom string `json:"denom,omitempty"` // name of the collection; not exported to clients 27 | NFTs []*NFT `json:"nfts"` // NFTs that belongs to a collection 28 | } 29 | ``` 30 | 31 | ## Owner 32 | 33 | An Owner is a struct that includes information about all NFTs owned by a single account. It would be possible to retrieve this information by looping through all Collections but that process could become computationally prohibitive so a more efficient retrieval system is to store redundant information limited to the token ID by owner. 34 | 35 | ```go 36 | // Owner of non fungible tokens 37 | type Owner struct { 38 | Address sdk.AccAddress `json:"address"` 39 | IDCollections IDCollections `json:"IDCollections"` 40 | } 41 | ``` 42 | 43 | An `IDCollection` is similar to a `Collection` except instead of containing NFTs it only contains an array of `NFT` IDs. This saves storage by avoiding redundancy. 44 | 45 | ```go 46 | // IDCollection of non fungible tokens 47 | type IDCollection struct { 48 | Denom string `json:"denom"` 49 | IDs []string `json:"IDs"` 50 | } 51 | -------------------------------------------------------------------------------- /incubator/nft/spec/02_state.md: -------------------------------------------------------------------------------- 1 | # State 2 | 3 | ## Collections 4 | 5 | As all NFTs belong to a specific `Collection`, they are kept on store in an array 6 | within each `Collection`. Every time an NFT that belongs to a collection is updated, 7 | it needs to be updated on the corresponding NFT array on the corresponding `Collection`. 8 | `denomHash` is used as part of the key to limit the length of the `denomBytes` which is 9 | a hash of `denomBytes` made from the tendermint [tmhash library](https://github.com/tendermint/tendermint/tree/master/crypto/tmhash). 10 | 11 | - Collections: `0x00 | denomHash -> amino(Collection)` 12 | - denomHash: `tmhash(denomBytes)` 13 | 14 | ## Owners 15 | 16 | The ownership of an NFT is set initially when an NFT is minted and needs to be 17 | updated every time there's a transfer or when an NFT is burned. 18 | 19 | - Owners: `0x01 | addressBytes | denomHash -> amino(Owner)` 20 | - denomHash: `tmhash(denomBytes)` 21 | -------------------------------------------------------------------------------- /incubator/nft/spec/03_messages.md: -------------------------------------------------------------------------------- 1 | # Messages 2 | 3 | ## MsgTransferNFT 4 | 5 | This is the most commonly expected MsgType to be supported across chains. While each application specific blockchain will have very different adoption of the `MsgMintNFT`, `MsgBurnNFT` and `MsgEditNFTMetadata` it should be expected that most chains support the ability to transfer ownership of the NFT asset. The exception to this would be non-transferable NFTs that might be attached to reputation or some asset which should not be transferable. It still makes sense for this to be represented as an NFT because there are common queriers which will remain relevant to the NFT type even if non-transferable. This Message will fail if the NFT does not exist. By default it will not fail if the transfer is executed by someone beside the owner. **It is highly recommended that a custom handler is made to restrict use of this Message type to prevent unintended use.** 6 | 7 | | **Field** | **Type** | **Description** | 8 | |:----------|:-----------------|:--------------------------------------------------------------------------------------------------------------| 9 | | Sender | `sdk.AccAddress` | The account address of the user sending the NFT. By default it is __not__ required that the sender is also the owner of the NFT. | 10 | | Recipient | `sdk.AccAddress` | The account address who will receive the NFT as a result of the transfer transaction. | 11 | | Denom | `string` | The denomination of the NFT, necessary as multiple denominations are able to be represented on each chain. | 12 | | ID | `string` | The unique ID of the NFT being transferred | 13 | 14 | ```go 15 | // MsgTransferNFT defines a TransferNFT message 16 | type MsgTransferNFT struct { 17 | Sender sdk.AccAddress 18 | Recipient sdk.AccAddress 19 | Denom string 20 | ID string 21 | } 22 | ``` 23 | 24 | ## MsgEditNFTMetadata 25 | 26 | This message type allows the `TokenURI` to be updated. By default anyone can execute this Message type. **It is highly recommended that a custom handler is made to restrict use of this Message type to prevent unintended use.** 27 | 28 | | **Field** | **Type** | **Description** | 29 | |:------------|:-----------------|:-----------------------------------------------------------------------------------------------------------| 30 | | Sender | `sdk.AccAddress` | The creator of the message | 31 | | ID | `string` | The unique ID of the NFT being edited | 32 | | Denom | `string` | The denomination of the NFT, necessary as multiple denominations are able to be represented on each chain. | 33 | | TokenURI | `string` | The URI pointing to a JSON object that contains subsequent metadata information off-chain | 34 | 35 | ```go 36 | // MsgEditNFTMetadata edits an NFT's metadata 37 | type MsgEditNFTMetadata struct { 38 | Sender sdk.AccAddress 39 | ID string 40 | Denom string 41 | TokenURI string 42 | } 43 | ``` 44 | 45 | ## MsgMintNFT 46 | 47 | This message type is used for minting new tokens. If a new `NFT` is minted under a new `Denom`, a new `Collection` will also be created, otherwise the `NFT` is added to the existing `Collection`. If a new `NFT` is minted by a new account, a new `Owner` is created, otherwise the `NFT` `ID` is added to the existing `Owner`'s `IDCollection`. By default anyone can execute this Message type. **It is highly recommended that a custom handler is made to restrict use of this Message type to prevent unintended use.** 48 | 49 | | **Field** | **Type** | **Description** | 50 | |:------------|:-----------------|:-----------------------------------------------------------------------------------------| 51 | | Sender | `sdk.AccAddress` | The sender of the Message | 52 | | Recipient | `sdk.AccAddress` | The recipiet of the new NFT | 53 | | ID | `string` | The unique ID of the NFT being minted | 54 | | Denom | `string` | The denomination of the NFT. | 55 | | TokenURI | `string` | The URI pointing to a JSON object that contains subsequent metadata information off-chain | 56 | 57 | ```go 58 | // MsgMintNFT defines a MintNFT message 59 | type MsgMintNFT struct { 60 | Sender sdk.AccAddress 61 | Recipient sdk.AccAddress 62 | ID string 63 | Denom string 64 | TokenURI string 65 | } 66 | ``` 67 | 68 | ### MsgBurnNFT 69 | 70 | This message type is used for burning tokens which destroys and deletes them. By default anyone can execute this Message type. **It is highly recommended that a custom handler is made to restrict use of this Message type to prevent unintended use.** 71 | 72 | 73 | | **Field** | **Type** | **Description** | 74 | |:----------|:-----------------|:---------------------------------------------------| 75 | | Sender | `sdk.AccAddress` | The account address of the user burning the token. | 76 | | ID | `string` | The ID of the Token. | 77 | | Denom | `string` | The Denom of the Token. | 78 | 79 | ```go 80 | // MsgBurnNFT defines a BurnNFT message 81 | type MsgBurnNFT struct { 82 | Sender sdk.AccAddress 83 | ID string 84 | Denom string 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /incubator/nft/spec/04_events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | The nft module emits the following events: 4 | 5 | ## Handlers 6 | 7 | ### MsgTransferNFT 8 | 9 | | Type | Attribute Key | Attribute Value | 10 | |--------------|---------------|--------------------| 11 | | transfer_nft | denom | {nftDenom} | 12 | | transfer_nft | nft-id | {nftID} | 13 | | transfer_nft | recipient | {recipientAddress} | 14 | | message | module | nft | 15 | | message | action | transfer_nft | 16 | | message | sender | {senderAddress} | 17 | 18 | ### MsgEditNFTMetadata 19 | 20 | | Type | Attribute Key | Attribute Value | 21 | |-------------------|---------------|-------------------| 22 | | edit_nft_metadata | denom | {nftDenom} | 23 | | edit_nft_metadata | nft-id | {nftID} | 24 | | message | module | nft | 25 | | message | action | edit_nft_metadata | 26 | | message | sender | {senderAddress} | 27 | | message | token-uri | {tokenURI} | 28 | 29 | ### MsgMintNFT 30 | 31 | | Type | Attribute Key | Attribute Value | 32 | |----------|---------------|-----------------| 33 | | mint_nft | denom | {nftDenom} | 34 | | mint_nft | nft-id | {nftID} | 35 | | message | module | nft | 36 | | message | action | mint_nft | 37 | | message | sender | {senderAddress} | 38 | | message | token-uri | {tokenURI} | 39 | 40 | ### MsgBurnNFTs 41 | 42 | | Type | Attribute Key | Attribute Value | 43 | |----------|---------------|-----------------| 44 | | burn_nft | denom | {nftDenom} | 45 | | burn_nft | nft-id | {nftID} | 46 | | message | module | nft | 47 | | message | action | burn_nft | 48 | | message | sender | {senderAddress} | 49 | -------------------------------------------------------------------------------- /incubator/nft/spec/05_future_improvements.md: -------------------------------------------------------------------------------- 1 | # Future Improvements 2 | 3 | There's interesting work that could be done about moving metadata into its own module. This could act as one of the `tokenURI` endpoints if a chain chooses to offer storage as a solution. Furthermore on-chain metadata can be trusted to a higher degree and might be used in secondary actions like price evaluation. Moving metadata to it's own module could be useful for the Bank Module as well. It would be able to describe attributes like decimal places and information regarding vesting schedules. It would be needed to have a level of introspection to describe the content without actually delivering the content for client libraries to interact with it. Using schema.org as a common location to settle metadata schema structure would be a good and impartial place to do so. 4 | 5 | Inter-Blockchain Communication will need to develop its own Message types that allow NFTs to be transferred across chains. Making sure that spec is able to support the NFTs created by this module should be easy. What might be more complicated is a transfer that includes optional metadata so that a receiving chain has the option of parsing and storing it instead of making IBC queries when that data needs to be accessed (assuming that information stays up to date). 6 | -------------------------------------------------------------------------------- /incubator/nft/spec/06_appendix.md: -------------------------------------------------------------------------------- 1 | # Appendix 2 | 3 | * Cosmos SDK: [PR #4209](https://github.com/cosmos/cosmos-sdk/pull/4209) 4 | * Cosmos SDK: [Issue #4046](https://github.com/cosmos/cosmos-sdk/issues/4046) 5 | * Interchain Standards: [ICS #17](https://github.com/cosmos/ics/issues/30) 6 | * Binance: [BEP #7](https://github.com/binance-chain/BEPs/pull/7) 7 | * Ethereum: [EIP #721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) 8 | -------------------------------------------------------------------------------- /incubator/nft/types/codec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // DONTCOVER 4 | 5 | import ( 6 | "github.com/cosmos/cosmos-sdk/codec" 7 | "github.com/cosmos/modules/incubator/nft/exported" 8 | ) 9 | 10 | // RegisterCodec concrete types on codec 11 | func RegisterCodec(cdc *codec.Codec) { 12 | cdc.RegisterInterface((*exported.NFT)(nil), nil) 13 | cdc.RegisterConcrete(&BaseNFT{}, "cosmos-sdk/BaseNFT", nil) 14 | cdc.RegisterConcrete(&IDCollection{}, "cosmos-sdk/IDCollection", nil) 15 | cdc.RegisterConcrete(&Collection{}, "cosmos-sdk/Collection", nil) 16 | cdc.RegisterConcrete(&Owner{}, "cosmos-sdk/Owner", nil) 17 | cdc.RegisterConcrete(MsgTransferNFT{}, "cosmos-sdk/MsgTransferNFT", nil) 18 | cdc.RegisterConcrete(MsgEditNFTMetadata{}, "cosmos-sdk/MsgEditNFTMetadata", nil) 19 | cdc.RegisterConcrete(MsgMintNFT{}, "cosmos-sdk/MsgMintNFT", nil) 20 | cdc.RegisterConcrete(MsgBurnNFT{}, "cosmos-sdk/MsgBurnNFT", nil) 21 | } 22 | 23 | // ModuleCdc generic sealed codec to be used throughout this module 24 | var ModuleCdc *codec.Codec 25 | 26 | func init() { 27 | ModuleCdc = codec.New() 28 | codec.RegisterCrypto(ModuleCdc) 29 | RegisterCodec(ModuleCdc) 30 | ModuleCdc.Seal() 31 | } 32 | -------------------------------------------------------------------------------- /incubator/nft/types/collection.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 10 | "github.com/cosmos/modules/incubator/nft/exported" 11 | ) 12 | 13 | // Collection of non fungible tokens 14 | type Collection struct { 15 | Denom string `json:"denom,omitempty" yaml:"denom"` // name of the collection; not exported to clients 16 | NFTs NFTs `json:"nfts" yaml:"nfts"` // NFTs that belong to a collection 17 | } 18 | 19 | // NewCollection creates a new NFT Collection 20 | func NewCollection(denom string, nfts NFTs) Collection { 21 | return Collection{ 22 | Denom: strings.TrimSpace(denom), 23 | NFTs: nfts, 24 | } 25 | } 26 | 27 | // EmptyCollection returns an empty collection 28 | func EmptyCollection() Collection { 29 | return NewCollection("", NewNFTs()) 30 | } 31 | 32 | // GetNFT gets a NFT from the collection 33 | func (collection Collection) GetNFT(id string) (nft exported.NFT, err error) { 34 | nft, found := collection.NFTs.Find(id) 35 | if found { 36 | return nft, nil 37 | } 38 | return nil, sdkerrors.Wrap(ErrUnknownNFT, 39 | fmt.Sprintf("NFT #%s doesn't exist in collection %s", id, collection.Denom), 40 | ) 41 | } 42 | 43 | // ContainsNFT returns whether or not a Collection contains an NFT 44 | func (collection Collection) ContainsNFT(id string) bool { 45 | _, err := collection.GetNFT(id) 46 | return err == nil 47 | } 48 | 49 | // AddNFT adds an NFT to the collection 50 | func (collection Collection) AddNFT(nft exported.NFT) (Collection, error) { 51 | id := nft.GetID() 52 | exists := collection.ContainsNFT(id) 53 | if exists { 54 | return collection, sdkerrors.Wrap(ErrNFTAlreadyExists, 55 | fmt.Sprintf("NFT #%s already exists in collection %s", id, collection.Denom), 56 | ) 57 | } 58 | collection.NFTs = collection.NFTs.Append(nft) 59 | return collection, nil 60 | } 61 | 62 | // UpdateNFT updates an NFT from a collection 63 | func (collection Collection) UpdateNFT(nft exported.NFT) (Collection, error) { 64 | nfts, ok := collection.NFTs.Update(nft.GetID(), nft) 65 | 66 | if !ok { 67 | return collection, sdkerrors.Wrap(ErrUnknownNFT, 68 | fmt.Sprintf("NFT #%s doesn't exist on collection %s", nft.GetID(), collection.Denom), 69 | ) 70 | } 71 | collection.NFTs = nfts 72 | return collection, nil 73 | } 74 | 75 | // DeleteNFT deletes an NFT from a collection 76 | func (collection Collection) DeleteNFT(nft exported.NFT) (Collection, error) { 77 | nfts, ok := collection.NFTs.Remove(nft.GetID()) 78 | if !ok { 79 | return collection, sdkerrors.Wrap(ErrUnknownNFT, 80 | fmt.Sprintf("NFT #%s doesn't exist on collection %s", nft.GetID(), collection.Denom), 81 | ) 82 | } 83 | collection.NFTs = nfts 84 | return collection, nil 85 | } 86 | 87 | // Supply gets the total supply of NFTs of a collection 88 | func (collection Collection) Supply() int { 89 | return len(collection.NFTs) 90 | } 91 | 92 | // String follows stringer interface 93 | func (collection Collection) String() string { 94 | return fmt.Sprintf(`Denom: %s 95 | NFTs: 96 | 97 | %s`, 98 | collection.Denom, 99 | collection.NFTs.String(), 100 | ) 101 | } 102 | 103 | // ---------------------------------------------------------------------------- 104 | // Collections 105 | 106 | // Collections define an array of Collection 107 | type Collections []Collection 108 | 109 | // NewCollections creates a new set of NFTs 110 | func NewCollections(collections ...Collection) Collections { 111 | if len(collections) == 0 { 112 | return Collections{} 113 | } 114 | return Collections(collections).Sort() 115 | } 116 | 117 | // Append appends two sets of Collections 118 | func (collections Collections) Append(collectionsB ...Collection) Collections { 119 | return append(collections, collectionsB...).Sort() 120 | } 121 | 122 | // Find returns the searched collection from the set 123 | func (collections Collections) Find(denom string) (Collection, bool) { 124 | index := collections.find(denom) 125 | if index == -1 { 126 | return Collection{}, false 127 | } 128 | return collections[index], true 129 | } 130 | 131 | // Remove removes a collection from the set of collections 132 | func (collections Collections) Remove(denom string) (Collections, bool) { 133 | index := collections.find(denom) 134 | if index == -1 { 135 | return collections, false 136 | } 137 | collections[len(collections)-1], collections[index] = collections[index], collections[len(collections)-1] 138 | return collections[:len(collections)-1], true 139 | } 140 | 141 | // String follows stringer interface 142 | func (collections Collections) String() string { 143 | if len(collections) == 0 { 144 | return "" 145 | } 146 | 147 | out := "" 148 | for _, collection := range collections { 149 | out += fmt.Sprintf("%v\n", collection.String()) 150 | } 151 | return out[:len(out)-1] 152 | } 153 | 154 | // Empty returns true if there are no collections and false otherwise. 155 | func (collections Collections) Empty() bool { 156 | return len(collections) == 0 157 | } 158 | 159 | func (collections Collections) find(denom string) (idx int) { 160 | return FindUtil(collections, denom) 161 | } 162 | 163 | // ---------------------------------------------------------------------------- 164 | // Encoding 165 | 166 | // CollectionJSON is the exported Collection format for clients 167 | type CollectionJSON map[string]Collection 168 | 169 | // MarshalJSON for Collections 170 | func (collections Collections) MarshalJSON() ([]byte, error) { 171 | collectionJSON := make(CollectionJSON) 172 | 173 | for _, collection := range collections { 174 | denom := collection.Denom 175 | collection.Denom = "" 176 | collectionJSON[denom] = collection 177 | } 178 | 179 | return json.Marshal(collectionJSON) 180 | } 181 | 182 | // UnmarshalJSON for Collections 183 | func (collections *Collections) UnmarshalJSON(b []byte) error { 184 | collectionJSON := make(CollectionJSON) 185 | 186 | if err := json.Unmarshal(b, &collectionJSON); err != nil { 187 | return err 188 | } 189 | 190 | for denom, collection := range collectionJSON { 191 | *collections = append(*collections, NewCollection(denom, collection.NFTs)) 192 | } 193 | 194 | return nil 195 | } 196 | 197 | //----------------------------------------------------------------------------- 198 | // Sort & Findable interfaces 199 | 200 | func (collections Collections) ElAtIndex(index int) string { return collections[index].Denom } 201 | func (collections Collections) Len() int { return len(collections) } 202 | func (collections Collections) Less(i, j int) bool { 203 | return strings.Compare(collections[i].Denom, collections[j].Denom) == -1 204 | } 205 | func (collections Collections) Swap(i, j int) { 206 | collections[i], collections[j] = collections[j], collections[i] 207 | } 208 | 209 | var _ sort.Interface = Collections{} 210 | 211 | // Sort is a helper function to sort the set of coins inplace 212 | func (collections Collections) Sort() Collections { 213 | sort.Sort(collections) 214 | return collections 215 | } 216 | -------------------------------------------------------------------------------- /incubator/nft/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 5 | ) 6 | 7 | var ( 8 | ErrInvalidCollection = sdkerrors.Register(ModuleName, 1, "invalid NFT collection") 9 | ErrUnknownCollection = sdkerrors.Register(ModuleName, 2, "unknown NFT collection") 10 | ErrInvalidNFT = sdkerrors.Register(ModuleName, 3, "invalid NFT") 11 | ErrUnknownNFT = sdkerrors.Register(ModuleName, 4, "unknown NFT") 12 | ErrNFTAlreadyExists = sdkerrors.Register(ModuleName, 5, "NFT already exists") 13 | ErrEmptyMetadata = sdkerrors.Register(ModuleName, 6, "NFT metadata can't be empty") 14 | ) 15 | -------------------------------------------------------------------------------- /incubator/nft/types/events.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // NFT module event types 4 | var ( 5 | EventTypeTransfer = "transfer_nft" 6 | EventTypeEditNFTMetadata = "edit_nft_metadata" 7 | EventTypeMintNFT = "mint_nft" 8 | EventTypeBurnNFT = "burn_nft" 9 | 10 | AttributeValueCategory = ModuleName 11 | 12 | AttributeKeySender = "sender" 13 | AttributeKeyRecipient = "recipient" 14 | AttributeKeyOwner = "owner" 15 | AttributeKeyNFTID = "nft-id" 16 | AttributeKeyNFTTokenURI = "token-uri" 17 | AttributeKeyDenom = "denom" 18 | ) 19 | -------------------------------------------------------------------------------- /incubator/nft/types/expected_keepers.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" 6 | ) 7 | 8 | // AccountKeeper defines the expected account keeper (noalias) 9 | type AccountKeeper interface { 10 | GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account 11 | } 12 | -------------------------------------------------------------------------------- /incubator/nft/types/genesis.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 5 | ) 6 | 7 | // GenesisState is the state that must be provided at genesis. 8 | type GenesisState struct { 9 | Owners []Owner `json:"owners"` 10 | Collections Collections `json:"collections"` 11 | } 12 | 13 | // NewGenesisState creates a new genesis state. 14 | func NewGenesisState(owners []Owner, collections Collections) GenesisState { 15 | return GenesisState{ 16 | Owners: owners, 17 | Collections: collections, 18 | } 19 | } 20 | 21 | // DefaultGenesisState returns a default genesis state 22 | func DefaultGenesisState() GenesisState { 23 | return NewGenesisState([]Owner{}, NewCollections()) 24 | } 25 | 26 | // ValidateGenesis performs basic validation of nfts genesis data returning an 27 | // error for any failed validation criteria. 28 | func ValidateGenesis(data GenesisState) error { 29 | for _, Owner := range data.Owners { 30 | if Owner.Address.Empty() { 31 | return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "address cannot be empty") 32 | } 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /incubator/nft/types/keys.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tendermint/tendermint/crypto/tmhash" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | ) 10 | 11 | const ( 12 | // ModuleName is the name of the module 13 | ModuleName = "nft" 14 | 15 | // StoreKey is the default store key for NFT 16 | StoreKey = ModuleName 17 | 18 | // QuerierRoute is the querier route for the NFT store. 19 | QuerierRoute = ModuleName 20 | 21 | // RouterKey is the message route for the NFT module 22 | RouterKey = ModuleName 23 | ) 24 | 25 | // NFTs are stored as follow: 26 | // 27 | // - Colections: 0x00 : 28 | // 29 | // - Owners: 0x01: 30 | var ( 31 | CollectionsKeyPrefix = []byte{0x00} // key for NFT collections 32 | OwnersKeyPrefix = []byte{0x01} // key for balance of NFTs held by an address 33 | ) 34 | 35 | // GetCollectionKey gets the key of a collection 36 | func GetCollectionKey(denom string) []byte { 37 | h := tmhash.New() 38 | _, err := h.Write([]byte(denom)) 39 | if err != nil { 40 | panic(err) 41 | } 42 | bs := h.Sum(nil) 43 | 44 | return append(CollectionsKeyPrefix, bs...) 45 | } 46 | 47 | // SplitOwnerKey gets an address and denom from an owner key 48 | func SplitOwnerKey(key []byte) (sdk.AccAddress, []byte) { 49 | if len(key) != 53 { 50 | panic(fmt.Sprintf("unexpected key length %d", len(key))) 51 | } 52 | address := key[1 : sdk.AddrLen+1] 53 | denomHashBz := key[sdk.AddrLen+1:] 54 | return sdk.AccAddress(address), denomHashBz 55 | } 56 | 57 | // GetOwnersKey gets the key prefix for all the collections owned by an account address 58 | func GetOwnersKey(address sdk.AccAddress) []byte { 59 | return append(OwnersKeyPrefix, address.Bytes()...) 60 | } 61 | 62 | // GetOwnerKey gets the key of a collection owned by an account address 63 | func GetOwnerKey(address sdk.AccAddress, denom string) []byte { 64 | h := tmhash.New() 65 | _, err := h.Write([]byte(denom)) 66 | if err != nil { 67 | panic(err) 68 | } 69 | bs := h.Sum(nil) 70 | 71 | return append(GetOwnersKey(address), bs...) 72 | } 73 | -------------------------------------------------------------------------------- /incubator/nft/types/nft.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | "github.com/cosmos/modules/incubator/nft/exported" 11 | ) 12 | 13 | var _ exported.NFT = (*BaseNFT)(nil) 14 | 15 | // BaseNFT non fungible token definition 16 | type BaseNFT struct { 17 | ID string `json:"id,omitempty" yaml:"id"` // id of the token; not exported to clients 18 | Owner sdk.AccAddress `json:"owner" yaml:"owner"` // account address that owns the NFT 19 | TokenURI string `json:"token_uri" yaml:"token_uri"` // optional extra properties available for querying 20 | } 21 | 22 | // NewBaseNFT creates a new NFT instance 23 | func NewBaseNFT(id string, owner sdk.AccAddress, tokenURI string) BaseNFT { 24 | return BaseNFT{ 25 | ID: id, 26 | Owner: owner, 27 | TokenURI: strings.TrimSpace(tokenURI), 28 | } 29 | } 30 | 31 | // GetID returns the ID of the token 32 | func (bnft BaseNFT) GetID() string { return bnft.ID } 33 | 34 | // GetOwner returns the account address that owns the NFT 35 | func (bnft BaseNFT) GetOwner() sdk.AccAddress { return bnft.Owner } 36 | 37 | // SetOwner updates the owner address of the NFT 38 | func (bnft *BaseNFT) SetOwner(address sdk.AccAddress) { 39 | bnft.Owner = address 40 | } 41 | 42 | // GetTokenURI returns the path to optional extra properties 43 | func (bnft BaseNFT) GetTokenURI() string { return bnft.TokenURI } 44 | 45 | // EditMetadata edits metadata of an nft 46 | func (bnft *BaseNFT) EditMetadata(tokenURI string) { 47 | bnft.TokenURI = tokenURI 48 | } 49 | 50 | func (bnft BaseNFT) String() string { 51 | return fmt.Sprintf(`ID: %s 52 | Owner: %s 53 | TokenURI: %s`, 54 | bnft.ID, 55 | bnft.Owner, 56 | bnft.TokenURI, 57 | ) 58 | } 59 | 60 | // ---------------------------------------------------------------------------- 61 | // NFT 62 | 63 | // NFTs define a list of NFT 64 | type NFTs []exported.NFT 65 | 66 | // NewNFTs creates a new set of NFTs 67 | func NewNFTs(nfts ...exported.NFT) NFTs { 68 | if len(nfts) == 0 { 69 | return NFTs{} 70 | } 71 | return NFTs(nfts).Sort() 72 | } 73 | 74 | // Append appends two sets of NFTs 75 | func (nfts NFTs) Append(nftsB ...exported.NFT) NFTs { 76 | return append(nfts, nftsB...).Sort() 77 | } 78 | 79 | // Find returns the searched collection from the set 80 | func (nfts NFTs) Find(id string) (nft exported.NFT, found bool) { 81 | index := nfts.find(id) 82 | if index == -1 { 83 | return nft, false 84 | } 85 | return nfts[index], true 86 | } 87 | 88 | // Update removes and replaces an NFT from the set 89 | func (nfts NFTs) Update(id string, nft exported.NFT) (NFTs, bool) { 90 | index := nfts.find(id) 91 | if index == -1 { 92 | return nfts, false 93 | } 94 | 95 | return append(append(nfts[:index], nft), nfts[index+1:]...), true 96 | } 97 | 98 | // Remove removes an NFT from the set of NFTs 99 | func (nfts NFTs) Remove(id string) (NFTs, bool) { 100 | index := nfts.find(id) 101 | if index == -1 { 102 | return nfts, false 103 | } 104 | 105 | return append(nfts[:index], nfts[index+1:]...), true 106 | } 107 | 108 | // String follows stringer interface 109 | func (nfts NFTs) String() string { 110 | if len(nfts) == 0 { 111 | return "" 112 | } 113 | 114 | out := "" 115 | for _, nft := range nfts { 116 | out += fmt.Sprintf("%v\n", nft.String()) 117 | } 118 | return out[:len(out)-1] 119 | } 120 | 121 | // Empty returns true if there are no NFTs and false otherwise. 122 | func (nfts NFTs) Empty() bool { 123 | return len(nfts) == 0 124 | } 125 | 126 | func (nfts NFTs) find(id string) int { 127 | return FindUtil(nfts, id) 128 | } 129 | 130 | // ---------------------------------------------------------------------------- 131 | // Encoding 132 | 133 | // NFTJSON is the exported NFT format for clients 134 | type NFTJSON map[string]BaseNFT 135 | 136 | // MarshalJSON for NFTs 137 | func (nfts NFTs) MarshalJSON() ([]byte, error) { 138 | nftJSON := make(NFTJSON) 139 | for _, nft := range nfts { 140 | id := nft.GetID() 141 | bnft := NewBaseNFT(id, nft.GetOwner(), nft.GetTokenURI()) 142 | nftJSON[id] = bnft 143 | } 144 | return json.Marshal(nftJSON) 145 | } 146 | 147 | // UnmarshalJSON for NFTs 148 | func (nfts *NFTs) UnmarshalJSON(b []byte) error { 149 | nftJSON := make(NFTJSON) 150 | if err := json.Unmarshal(b, &nftJSON); err != nil { 151 | return err 152 | } 153 | 154 | for id, nft := range nftJSON { 155 | bnft := NewBaseNFT(id, nft.GetOwner(), nft.GetTokenURI()) 156 | *nfts = append(*nfts, &bnft) 157 | } 158 | return nil 159 | } 160 | 161 | // Findable and Sort interfaces 162 | func (nfts NFTs) ElAtIndex(index int) string { return nfts[index].GetID() } 163 | func (nfts NFTs) Len() int { return len(nfts) } 164 | func (nfts NFTs) Less(i, j int) bool { return strings.Compare(nfts[i].GetID(), nfts[j].GetID()) == -1 } 165 | func (nfts NFTs) Swap(i, j int) { nfts[i], nfts[j] = nfts[j], nfts[i] } 166 | 167 | var _ sort.Interface = NFTs{} 168 | 169 | // Sort is a helper function to sort the set of coins in place 170 | func (nfts NFTs) Sort() NFTs { 171 | sort.Sort(nfts) 172 | return nfts 173 | } 174 | -------------------------------------------------------------------------------- /incubator/nft/types/nft_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // ---------------------------------------- BaseNFT --------------------------------------------------- 11 | 12 | func TestBaseNFTGetMethods(t *testing.T) { 13 | testNFT := NewBaseNFT(id, address, tokenURI) 14 | 15 | require.Equal(t, id, testNFT.GetID()) 16 | require.Equal(t, address, testNFT.GetOwner()) 17 | require.Equal(t, tokenURI, testNFT.GetTokenURI()) 18 | } 19 | 20 | func TestBaseNFTSetMethods(t *testing.T) { 21 | testNFT := NewBaseNFT(id, address, tokenURI) 22 | 23 | testNFT.SetOwner(address2) 24 | require.Equal(t, address2, testNFT.GetOwner()) 25 | 26 | testNFT.EditMetadata(tokenURI2) 27 | require.Equal(t, tokenURI2, testNFT.GetTokenURI()) 28 | } 29 | 30 | func TestBaseNFTStringFormat(t *testing.T) { 31 | testNFT := NewBaseNFT(id, address, tokenURI) 32 | expected := fmt.Sprintf(`ID: %s 33 | Owner: %s 34 | TokenURI: %s`, 35 | id, address, tokenURI) 36 | require.Equal(t, expected, testNFT.String()) 37 | } 38 | 39 | // ---------------------------------------- NFTs --------------------------------------------------- 40 | 41 | func TestNewNFTs(t *testing.T) { 42 | emptyNFTs := NewNFTs() 43 | require.Equal(t, len(emptyNFTs), 0) 44 | 45 | testNFT := NewBaseNFT(id, address, tokenURI) 46 | oneNFTs := NewNFTs(&testNFT) 47 | require.Equal(t, len(oneNFTs), 1) 48 | 49 | testNFT2 := NewBaseNFT(id2, address, tokenURI) 50 | twoNFTs := NewNFTs(&testNFT, &testNFT2) 51 | require.Equal(t, len(twoNFTs), 2) 52 | } 53 | 54 | func TestNFTsAppendMethod(t *testing.T) { 55 | testNFT := NewBaseNFT(id, address, tokenURI) 56 | nfts := NewNFTs(&testNFT) 57 | require.Equal(t, len(nfts), 1) 58 | 59 | testNFT2 := NewBaseNFT(id2, address, tokenURI) 60 | nfts2 := NewNFTs(&testNFT2) 61 | 62 | nfts = nfts.Append(nfts2...) 63 | require.Equal(t, len(nfts), 2) 64 | 65 | var id3 = string('3') 66 | var id4 = string('4') 67 | var id5 = string('5') 68 | testNFT3 := NewBaseNFT(id3, address, tokenURI) 69 | testNFT4 := NewBaseNFT(id4, address, tokenURI) 70 | testNFT5 := NewBaseNFT(id5, address, tokenURI) 71 | 72 | nfts3 := NewNFTs(&testNFT5, &testNFT3, &testNFT4) 73 | nfts = nfts.Append(nfts3...) 74 | require.Equal(t, len(nfts), 5) 75 | 76 | nft, found := nfts.Find(id2) 77 | require.True(t, found) 78 | require.Equal(t, nft.String(), testNFT2.String()) 79 | 80 | nft, found = nfts.Find(id5) 81 | require.True(t, found) 82 | require.Equal(t, nft.String(), testNFT5.String()) 83 | 84 | nft, found = nfts.Find(id3) 85 | require.True(t, found) 86 | require.Equal(t, nft.String(), testNFT3.String()) 87 | } 88 | 89 | func TestNFTsFindMethod(t *testing.T) { 90 | testNFT := NewBaseNFT(id, address, tokenURI) 91 | testNFT2 := NewBaseNFT(id2, address, tokenURI) 92 | 93 | var id3 = string('3') 94 | var id4 = string('4') 95 | var id5 = string('5') 96 | testNFT3 := NewBaseNFT(id3, address, tokenURI) 97 | testNFT4 := NewBaseNFT(id4, address, tokenURI) 98 | testNFT5 := NewBaseNFT(id5, address, tokenURI) 99 | 100 | nfts := NewNFTs(&testNFT, &testNFT3, &testNFT4, &testNFT5, &testNFT2) 101 | nft, found := nfts.Find(id) 102 | require.True(t, found) 103 | require.Equal(t, nft.String(), testNFT.String()) 104 | 105 | nft, found = nfts.Find(id2) 106 | require.True(t, found) 107 | require.Equal(t, nft.String(), testNFT2.String()) 108 | 109 | nft, found = nfts.Find(id3) 110 | require.True(t, found) 111 | require.Equal(t, nft.String(), testNFT3.String()) 112 | 113 | nft, found = nfts.Find(id4) 114 | require.True(t, found) 115 | require.Equal(t, nft.String(), testNFT4.String()) 116 | 117 | nft, found = nfts.Find(id5) 118 | require.True(t, found) 119 | require.Equal(t, nft.String(), testNFT5.String()) 120 | 121 | var id6 = string('6') 122 | nft, found = nfts.Find(id6) 123 | require.False(t, found) 124 | require.Nil(t, nft) 125 | } 126 | 127 | func TestNFTsUpdateMethod(t *testing.T) { 128 | testNFT := NewBaseNFT(id, address, tokenURI) 129 | testNFT2 := NewBaseNFT(id2, address, tokenURI) 130 | nfts := NewNFTs(&testNFT) 131 | var success bool 132 | nfts, success = nfts.Update(id, &testNFT2) 133 | require.True(t, success) 134 | 135 | nft, found := nfts.Find(id2) 136 | require.True(t, found) 137 | require.Equal(t, nft.String(), testNFT2.String()) 138 | 139 | nft, found = nfts.Find(id) 140 | require.False(t, found) 141 | require.Nil(t, nft) 142 | 143 | var returnedNFTs NFTs 144 | returnedNFTs, success = nfts.Update(id, &testNFT2) 145 | require.False(t, success) 146 | require.Equal(t, returnedNFTs.String(), nfts.String()) 147 | } 148 | 149 | func TestNFTsRemoveMethod(t *testing.T) { 150 | testNFT := NewBaseNFT(id, address, tokenURI) 151 | testNFT2 := NewBaseNFT(id2, address, tokenURI) 152 | nfts := NewNFTs(&testNFT, &testNFT2) 153 | 154 | var success bool 155 | nfts, success = nfts.Remove(id) 156 | require.True(t, success) 157 | require.Equal(t, len(nfts), 1) 158 | 159 | nfts, success = nfts.Remove(id2) 160 | require.True(t, success) 161 | require.Equal(t, len(nfts), 0) 162 | 163 | var returnedNFTs NFTs 164 | returnedNFTs, success = nfts.Remove(id2) 165 | require.False(t, success) 166 | require.Equal(t, nfts.String(), returnedNFTs.String()) 167 | } 168 | 169 | func TestNFTsStringMethod(t *testing.T) { 170 | testNFT := NewBaseNFT(id, address, tokenURI) 171 | nfts := NewNFTs(&testNFT) 172 | require.Equal(t, nfts.String(), fmt.Sprintf(`ID: %s 173 | Owner: %s 174 | TokenURI: %s`, id, address, tokenURI)) 175 | } 176 | 177 | func TestNFTsEmptyMethod(t *testing.T) { 178 | nfts := NewNFTs() 179 | require.True(t, nfts.Empty()) 180 | testNFT := NewBaseNFT(id, address, tokenURI) 181 | nfts = NewNFTs(&testNFT) 182 | require.False(t, nfts.Empty()) 183 | } 184 | 185 | func TestNFTsMarshalUnmarshalJSON(t *testing.T) { 186 | testNFT := NewBaseNFT(id, address, tokenURI) 187 | nfts := NewNFTs(&testNFT) 188 | bz, err := nfts.MarshalJSON() 189 | require.NoError(t, err) 190 | require.Equal(t, string(bz), 191 | fmt.Sprintf(`{"%s":{"id":"%s","owner":"%s","token_uri":"%s"}}`, 192 | id, id, address.String(), tokenURI)) 193 | 194 | var unmarshaledNFTs NFTs 195 | err = unmarshaledNFTs.UnmarshalJSON(bz) 196 | require.NoError(t, err) 197 | require.Equal(t, unmarshaledNFTs.String(), nfts.String()) 198 | 199 | bz = []byte{} 200 | err = unmarshaledNFTs.UnmarshalJSON(bz) 201 | require.Error(t, err) 202 | } 203 | 204 | func TestNFTsSortInterface(t *testing.T) { 205 | testNFT := NewBaseNFT(id, address, tokenURI) 206 | testNFT2 := NewBaseNFT(id2, address, tokenURI) 207 | 208 | nfts := NewNFTs(&testNFT) 209 | require.Equal(t, nfts.Len(), 1) 210 | 211 | nfts = NewNFTs(&testNFT, &testNFT2) 212 | require.Equal(t, nfts.Len(), 2) 213 | 214 | require.True(t, nfts.Less(0, 1)) 215 | require.False(t, nfts.Less(1, 0)) 216 | 217 | nfts.Swap(0, 1) 218 | require.False(t, nfts.Less(0, 1)) 219 | require.True(t, nfts.Less(1, 0)) 220 | 221 | nfts.Sort() 222 | require.True(t, nfts.Less(0, 1)) 223 | require.False(t, nfts.Less(1, 0)) 224 | } 225 | -------------------------------------------------------------------------------- /incubator/nft/types/owners_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // ---------------------------------------- IDCollection --------------------------------------------------- 11 | 12 | func TestNewIDCollection(t *testing.T) { 13 | ids := []string{id, id2, id3} 14 | idCollection := NewIDCollection(denom, ids) 15 | require.Equal(t, idCollection.Denom, denom) 16 | require.Equal(t, len(idCollection.IDs), 3) 17 | } 18 | 19 | func TestIDCollectionExistsMethod(t *testing.T) { 20 | ids := []string{id2, id} 21 | idCollection := NewIDCollection(denom, ids) 22 | require.True(t, idCollection.Exists(id)) 23 | require.True(t, idCollection.Exists(id2)) 24 | require.False(t, idCollection.Exists(id3)) 25 | } 26 | 27 | func TestIDCollectionAddIDMethod(t *testing.T) { 28 | ids := []string{id, id2} 29 | idCollection := NewIDCollection(denom, ids) 30 | idCollection = idCollection.AddID(id3) 31 | require.Equal(t, len(idCollection.IDs), 3) 32 | } 33 | 34 | func TestIDCollectionDeleteIDMethod(t *testing.T) { 35 | ids := []string{id, id2} 36 | idCollection := NewIDCollection(denom, ids) 37 | newIDCollection, err := idCollection.DeleteID(id3) 38 | require.Error(t, err) 39 | require.Equal(t, idCollection.String(), newIDCollection.String()) 40 | 41 | idCollection, err = idCollection.DeleteID(id2) 42 | require.NoError(t, err) 43 | require.Equal(t, len(idCollection.IDs), 1) 44 | } 45 | 46 | func TestIDCollectionSupplyMethod(t *testing.T) { 47 | idCollectionEmpty := IDCollection{} 48 | require.Equal(t, 0, idCollectionEmpty.Supply()) 49 | 50 | ids := []string{id, id2} 51 | idCollection := NewIDCollection(denom, ids) 52 | require.Equal(t, 2, idCollection.Supply()) 53 | 54 | idCollection, err := idCollection.DeleteID(id) 55 | require.Nil(t, err) 56 | require.Equal(t, idCollection.Supply(), 1) 57 | 58 | idCollection, err = idCollection.DeleteID(id2) 59 | require.Nil(t, err) 60 | require.Equal(t, idCollection.Supply(), 0) 61 | 62 | idCollection = idCollection.AddID(id) 63 | require.Nil(t, err) 64 | require.Equal(t, idCollection.Supply(), 1) 65 | } 66 | 67 | func TestIDCollectionStringMethod(t *testing.T) { 68 | ids := []string{id, id2} 69 | idCollection := NewIDCollection(denom, ids) 70 | require.Equal(t, idCollection.String(), fmt.Sprintf(`Denom: %s 71 | IDs: %s,%s`, denom, id, id2)) 72 | } 73 | 74 | // ---------------------------------------- IDCollections --------------------------------------------------- 75 | 76 | func TestIDCollectionsString(t *testing.T) { 77 | emptyCollections := IDCollections([]IDCollection{}) 78 | require.Equal(t, emptyCollections.String(), "") 79 | 80 | ids := []string{id, id2} 81 | idCollection := NewIDCollection(denom, ids) 82 | idCollection2 := NewIDCollection(denom2, ids) 83 | 84 | idCollections := IDCollections([]IDCollection{idCollection, idCollection2}) 85 | require.Equal(t, idCollections.String(), fmt.Sprintf(`Denom: %s 86 | IDs: %s,%s 87 | Denom: %s 88 | IDs: %s,%s`, denom, id, id2, denom2, id, id2)) 89 | } 90 | 91 | // ---------------------------------------- Owner --------------------------------------------------- 92 | 93 | func TestNewOwner(t *testing.T) { 94 | ids := []string{id, id2} 95 | idCollection := NewIDCollection(denom, ids) 96 | idCollection2 := NewIDCollection(denom2, ids) 97 | 98 | owner := NewOwner(address, idCollection, idCollection2) 99 | require.Equal(t, owner.Address.String(), address.String()) 100 | require.Equal(t, len(owner.IDCollections), 2) 101 | } 102 | 103 | func TestOwnerSupplyMethod(t *testing.T) { 104 | owner := NewOwner(address) 105 | require.Equal(t, owner.Supply(), 0) 106 | 107 | ids := []string{id, id2} 108 | idCollection := NewIDCollection(denom, ids) 109 | owner = NewOwner(address, idCollection) 110 | require.Equal(t, owner.Supply(), 2) 111 | 112 | idCollection2 := NewIDCollection(denom2, ids) 113 | owner = NewOwner(address, idCollection, idCollection2) 114 | require.Equal(t, owner.Supply(), 4) 115 | } 116 | 117 | func TestOwnerGetIDCollectionMethod(t *testing.T) { 118 | ids := []string{id, id2} 119 | idCollection := NewIDCollection(denom, ids) 120 | owner := NewOwner(address, idCollection) 121 | 122 | gotCollection, found := owner.GetIDCollection(denom2) 123 | require.False(t, found) 124 | require.Equal(t, gotCollection.Denom, "") 125 | require.Equal(t, len(gotCollection.IDs), 0) 126 | require.Equal(t, gotCollection.String(), IDCollection{}.String()) 127 | 128 | gotCollection, found = owner.GetIDCollection(denom) 129 | require.True(t, found) 130 | require.Equal(t, gotCollection.String(), idCollection.String()) 131 | 132 | idCollection2 := NewIDCollection(denom2, ids) 133 | owner = NewOwner(address, idCollection, idCollection2) 134 | 135 | gotCollection, found = owner.GetIDCollection(denom) 136 | require.True(t, found) 137 | require.Equal(t, gotCollection.String(), idCollection.String()) 138 | 139 | gotCollection, found = owner.GetIDCollection(denom2) 140 | require.True(t, found) 141 | require.Equal(t, gotCollection.String(), idCollection2.String()) 142 | } 143 | 144 | func TestOwnerUpdateIDCollectionMethod(t *testing.T) { 145 | ids := []string{id} 146 | idCollection := NewIDCollection(denom, ids) 147 | owner := NewOwner(address, idCollection) 148 | require.Equal(t, owner.Supply(), 1) 149 | 150 | ids2 := []string{id, id2} 151 | idCollection2 := NewIDCollection(denom2, ids2) 152 | 153 | // UpdateIDCollection should fail if denom doesn't exist 154 | returnedOwner, err := owner.UpdateIDCollection(idCollection2) 155 | require.Error(t, err) 156 | 157 | idCollection3 := NewIDCollection(denom, ids2) 158 | returnedOwner, err = owner.UpdateIDCollection(idCollection3) 159 | require.NoError(t, err) 160 | require.Equal(t, returnedOwner.Supply(), 2) 161 | 162 | owner = returnedOwner 163 | 164 | returnedCollection, _ := owner.GetIDCollection(denom) 165 | require.Equal(t, len(returnedCollection.IDs), 2) 166 | 167 | owner = NewOwner(address, idCollection, idCollection2) 168 | require.Equal(t, owner.Supply(), 3) 169 | 170 | returnedOwner, err = owner.UpdateIDCollection(idCollection3) 171 | require.NoError(t, err) 172 | require.Equal(t, returnedOwner.Supply(), 4) 173 | } 174 | 175 | func TestOwnerDeleteIDMethod(t *testing.T) { 176 | ids := []string{id, id2} 177 | idCollection := NewIDCollection(denom, ids) 178 | owner := NewOwner(address, idCollection) 179 | 180 | returnedOwner, err := owner.DeleteID(denom2, id) 181 | require.Error(t, err) 182 | require.Equal(t, owner.String(), returnedOwner.String()) 183 | 184 | returnedOwner, err = owner.DeleteID(denom, id3) 185 | require.Error(t, err) 186 | require.Equal(t, owner.String(), returnedOwner.String()) 187 | 188 | owner, err = owner.DeleteID(denom, id) 189 | require.NoError(t, err) 190 | 191 | returnedCollection, _ := owner.GetIDCollection(denom) 192 | require.Equal(t, len(returnedCollection.IDs), 1) 193 | } 194 | -------------------------------------------------------------------------------- /incubator/nft/types/querier.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // DONTCOVER 4 | 5 | import ( 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | ) 8 | 9 | // QueryCollectionParams defines the params for queries: 10 | // - 'custom/nft/supply' 11 | // - 'custom/nft/collection' 12 | type QueryCollectionParams struct { 13 | Denom string 14 | } 15 | 16 | // NewQueryCollectionParams creates a new instance of QuerySupplyParams 17 | func NewQueryCollectionParams(denom string) QueryCollectionParams { 18 | return QueryCollectionParams{Denom: denom} 19 | } 20 | 21 | // Bytes exports the Denom as bytes 22 | func (q QueryCollectionParams) Bytes() []byte { 23 | return []byte(q.Denom) 24 | } 25 | 26 | // QueryBalanceParams params for query 'custom/nfts/balance' 27 | type QueryBalanceParams struct { 28 | Owner sdk.AccAddress 29 | Denom string // optional 30 | } 31 | 32 | // NewQueryBalanceParams creates a new instance of QuerySupplyParams 33 | func NewQueryBalanceParams(owner sdk.AccAddress, denom ...string) QueryBalanceParams { 34 | if len(denom) > 0 { 35 | return QueryBalanceParams{ 36 | Owner: owner, 37 | Denom: denom[0], 38 | } 39 | } 40 | return QueryBalanceParams{Owner: owner} 41 | } 42 | 43 | // QueryNFTParams params for query 'custom/nfts/nft' 44 | type QueryNFTParams struct { 45 | Denom string 46 | TokenID string 47 | } 48 | 49 | // NewQueryNFTParams creates a new instance of QueryNFTParams 50 | func NewQueryNFTParams(denom, id string) QueryNFTParams { 51 | return QueryNFTParams{ 52 | Denom: denom, 53 | TokenID: id, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /incubator/nft/types/test_common.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | ) 9 | 10 | // nolint: deadcode unused 11 | var ( 12 | denom = "denom" 13 | denom2 = "test-denom2" 14 | denom3 = "test-denom3" 15 | id = "1" 16 | id2 = "2" 17 | id3 = "3" 18 | address = CreateTestAddrs(1)[0] 19 | address2 = CreateTestAddrs(2)[1] 20 | address3 = CreateTestAddrs(3)[2] 21 | tokenURI = "https://google.com/token-1.json" 22 | tokenURI2 = "https://google.com/token-2.json" 23 | ) 24 | 25 | // CreateTestAddrs creates test addresses 26 | func CreateTestAddrs(numAddrs int) []sdk.AccAddress { 27 | var addresses []sdk.AccAddress 28 | var buffer bytes.Buffer 29 | 30 | // start at 100 so we can make up to 999 test addresses with valid test addresses 31 | for i := 100; i < (numAddrs + 100); i++ { 32 | numString := strconv.Itoa(i) 33 | buffer.WriteString("A58856F0FD53BF058B4909A21AEC019107BA6") //base address string 34 | 35 | buffer.WriteString(numString) //adding on final two digits to make addresses unique 36 | res, _ := sdk.AccAddressFromHex(buffer.String()) 37 | bech := res.String() 38 | addresses = append(addresses, testAddr(buffer.String(), bech)) 39 | buffer.Reset() 40 | } 41 | return addresses 42 | } 43 | 44 | // for incode address generation 45 | func testAddr(addr string, bech string) sdk.AccAddress { 46 | res, err := sdk.AccAddressFromHex(addr) 47 | if err != nil { 48 | panic(err) 49 | } 50 | bechexpected := res.String() 51 | if bech != bechexpected { 52 | panic("Bech encoding doesn't match reference") 53 | } 54 | 55 | bechres, err := sdk.AccAddressFromBech32(bech) 56 | if err != nil { 57 | panic(err) 58 | } 59 | if !bytes.Equal(bechres, res) { 60 | panic("Bech decode and hex decode don't match") 61 | } 62 | 63 | return res 64 | } 65 | -------------------------------------------------------------------------------- /incubator/nft/types/utils.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "strings" 4 | 5 | // Findable is an interface for iterable types that allows the FindUtil function to work 6 | type Findable interface { 7 | ElAtIndex(index int) string 8 | Len() int 9 | } 10 | 11 | // FindUtil is a binary search funcion for types that support the Findable interface (elements must be sorted) 12 | func FindUtil(group Findable, el string) int { 13 | if group.Len() == 0 { 14 | return -1 15 | } 16 | low := 0 17 | high := group.Len() - 1 18 | median := 0 19 | for low <= high { 20 | median = (low + high) / 2 21 | switch compare := strings.Compare(group.ElAtIndex(median), el); { 22 | case compare == 0: 23 | // if group[median].element == el 24 | return median 25 | case compare == -1: 26 | // if group[median].element < el 27 | low = median + 1 28 | default: 29 | // if group[median].element > el 30 | high = median - 1 31 | } 32 | } 33 | return -1 34 | } 35 | -------------------------------------------------------------------------------- /incubator/poa/docs/spec/01_state.md: -------------------------------------------------------------------------------- 1 | # State 2 | 3 | ## LastTotalPower 4 | 5 | LastTotalPower tracks the total amount of weight recorded during the previous end block. 6 | 7 | - LastTotalPower: `0x12 -> amino(sdk.Int)` 8 | 9 | ## Params 10 | 11 | Params is a module-wide configuration structure that stores system parameters 12 | and defines overall functioning of the POA module. 13 | 14 | - Params: `Paramsspace("poa") -> amino(params)` 15 | 16 | ```go 17 | type Params struct { 18 | UnbondingTime time.Duration // time duration of unbonding 19 | MaxValidators uint16 // maximum number of validators 20 | // TODO: MaxEntries uint16 // max entries for either unbonding delegation or redelegation (per pair/trio) 21 | AcceptAllValidators bool // Sets the value if a network wants to accept all applicants to be validators 22 | IncreaseWeight bool // Disallow validators to increase there power 23 | } 24 | ``` 25 | 26 | ## Validator 27 | 28 | Validators objects should be primarily stored and accessed by the 29 | `OperatorAddr`, an SDK validator address for the operator of the validator. Two 30 | additional indices are maintained per validator object in order to fulfill 31 | required lookups for slashing and validator-set updates. A third special index 32 | (`LastValidatorPower`) is also maintained which however remains constant 33 | throughout each block, unlike the first two indices which mirror the validator 34 | records within a block. 35 | 36 | - Validators: `0x21 | OperatorAddr -> amino(validator)` 37 | - ValidatorsByConsAddr: `0x22 | ConsAddr -> OperatorAddr` 38 | - ValidatorsByPower: `0x23 | BigEndian(ConsensusPower) | OperatorAddr -> OperatorAddr` 39 | - LastValidatorsPower: `0x11 OperatorAddr -> amino(ConsensusPower)` 40 | 41 | `Validators` is the primary index - it ensures that each operator can have only one 42 | associated validator, where the public key of that validator can change in the 43 | future. Delegators can refer to the immutable operator of the validator, without 44 | concern for the changing public key. 45 | 46 | `ValidatorByConsAddr` is an additional index that enables lookups for slashing. 47 | When Tendermint reports evidence, it provides the validator address, so this 48 | map is needed to find the operator. Note that the `ConsAddr` corresponds to the 49 | address which can be derived from the validator's `ConsPubKey`. 50 | 51 | `ValidatorsByPower` is an additional index that provides a sorted list o 52 | potential validators to quickly determine the current active set. Here 53 | ConsensusPower is validator.Tokens/10^6. Note that all validators where 54 | `Jailed` is true are not stored within this index. 55 | 56 | `LastValidatorsPower` is a special index that provides a historical list of the 57 | last-block's bonded validators. This index remains constant during a block but 58 | is updated during the validator set update process which takes place in [`EndBlock`](./04_end_block.md). 59 | 60 | Each validator's state is stored in a `Validator` struct: 61 | 62 | ```go 63 | type Validator struct { 64 | OperatorAddress sdk.ValAddress // address of the validator's operator; bech encoded in JSON 65 | ConsPubKey crypto.PubKey // the consensus public key of the validator; bech encoded in JSON 66 | Jailed bool // has the validator been jailed from bonded status? 67 | Status sdk.BondStatus // validator status (bonded/unbonding/unbonded) 68 | Weight sdk.Int // weight (repuatation) associated with each validator 69 | Description Description // description terms for the validator 70 | UnbondingHeight int64 // if unbonding, height at which this validator has begun unbonding 71 | UnbondingCompletionTime time.Time // if unbonding, min time for the validator to complete unbonding 72 | } 73 | 74 | type Description struct { 75 | Moniker string // name 76 | Identity string // optional identity signature (ex. UPort or Keybase) 77 | Website string // optional website link 78 | SecurityContact string // optional email for security contact 79 | Details string // optional details 80 | } 81 | ``` 82 | 83 | ## Queues 84 | 85 | All queues objects are sorted by timestamp. The time used within any queue is 86 | first rounded to the nearest nanosecond then sorted. The sortable time format 87 | used is a slight modification of the RFC3339Nano and uses the the format string 88 | `"2006-01-02T15:04:05.000000000"`. Notably this format: 89 | 90 | - right pads all zeros 91 | - drops the time zone info (uses UTC) 92 | 93 | In all cases, the stored timestamp represents the maturation time of the queue 94 | element. 95 | 96 | ### ValidatorQueue 97 | 98 | For the purpose of tracking progress of unbonding validators the validator 99 | queue is kept. 100 | 101 | - ValidatorQueueTime: `0x43 | format(time) -> []sdk.ValAddress` 102 | 103 | The stored object as each key is an array of validator operator addresses from 104 | which the validator object can be accessed. Typically it is expected that only 105 | a single validator record will be associated with a given timestamp however it is possible 106 | that multiple validators exist in the queue at the same location. 107 | -------------------------------------------------------------------------------- /incubator/poa/docs/spec/02_state_transitions.md: -------------------------------------------------------------------------------- 1 | # State Transitions 2 | 3 | This document describes the state transition operations pertaining to: 4 | 5 | 1. [Validators](./02_state_transitions.md#validators) 6 | 2. [Slashing](./02_state_transitions.md#slashing) 7 | 8 | ## Validators 9 | 10 | State transitions in validators are performed on every [`EndBlock`](./04_end_block.md#validator-set-changes) in order to check for changes in the active `ValidatorSet`. 11 | 12 | ### Non-Bonded to Bonded 13 | 14 | When a validator is bonded from any other state the following operations occur: 15 | 16 | - set `validator.Status` to `Bonded` 17 | - delete the existing record from `ValidatorByPowerIndex` 18 | - add a new updated record to the `ValidatorByPowerIndex` 19 | - update the `Validator` object for this validator 20 | - if it exists, delete any `ValidatorQueue` record for this validator 21 | 22 | ### Bonded to Unbonding 23 | 24 | When a validator begins the unbonding process the following operations occur: 25 | 26 | - set `validator.Status` to `Unbonding` 27 | - delete the existing record from `ValidatorByPowerIndex` 28 | - add a new updated record to the `ValidatorByPowerIndex` 29 | - update the `Validator` object for this validator 30 | - insert a new record into the `ValidatorQueue` for this validator 31 | 32 | ### Unbonding to Unbonded 33 | 34 | A validator moves from unbonding to unbonded when the `ValidatorQueue` object 35 | moves from bonded to unbonded 36 | 37 | - update the `Validator` object for this validator 38 | - set `validator.Status` to `Unbonded` 39 | 40 | ### Jail/Unjail 41 | 42 | when a validator is jailed it is effectively removed from the Tendermint set. 43 | this process may be also be reversed. the following operations occur: 44 | 45 | - set `Validator.Jailed` and update object 46 | - if jailed delete record from `ValidatorByPowerIndex` 47 | - if unjailed add record to `ValidatorByPowerIndex` 48 | 49 | ### Begin Unbonding 50 | 51 | When a validator wants to remove himself from the validator. 52 | The following operations occur: 53 | 54 | - subtract the total weight from the validator 55 | - remove the validator if it is unbonded and there is no more weight associated with it. 56 | 57 | ## Slashing 58 | 59 | ### Slash Validator 60 | 61 | ### Slash Unbonding Delegation 62 | 63 | ### Slash Redelegation 64 | -------------------------------------------------------------------------------- /incubator/poa/docs/spec/03_messages.md: -------------------------------------------------------------------------------- 1 | # Messages 2 | 3 | In this section we describe the processing of the staking messages and the corresponding updates to the state. All created/modified state objects specified by each message are defined within the [state](./02_state.md) section. 4 | 5 | ## MsgCreateValidator 6 | 7 | A validator is created using the `MsgCreateValidator` message, if `AcceptAllValidators` is set to `true`. 8 | 9 | ```go 10 | type MsgCreateValidator struct { 11 | Description Description 12 | ValidatorAddr sdk.ValAddress 13 | PubKey crypto.PubKey 14 | } 15 | ``` 16 | 17 | This message is expected to fail if: 18 | 19 | - another validator with this operator address is already registered 20 | - another validator with this pubkey is already registered 21 | - the description fields are too large 22 | - `AcceptAllValidators` is set to `false` 23 | 24 | This message creates and stores the `Validator` object at appropriate indexes. 25 | The validator always starts as unbonded but may be bonded in the first end-block. 26 | 27 | ## MsgEditValidator 28 | 29 | The `Description` of a validator can be updated using the `MsgEditCandidacy`. 30 | 31 | ```go 32 | type MsgEditCandidacy struct { 33 | Description Description 34 | ValidatorAddr sdk.ValAddress 35 | } 36 | ``` 37 | 38 | This message is expected to fail if: 39 | 40 | - the description fields are too large 41 | 42 | This message stores the updated `Validator` object. 43 | 44 | ## MsgProposeCreateValidator 45 | 46 | A validator is created using the `MsgProposeCreateValidator` message, if `AcceptAllValidators` is set to `false`. 47 | 48 | ```go 49 | type MsgProposeCreateValidator struct { 50 | Title string // title of the validator 51 | Description string // description of validator 52 | Validator NewValidatorCreatation // validator details 53 | } 54 | 55 | type NewValidatorCreatation struct { 56 | Description stakingtypes.Description // description of validator 57 | ValidatorAddress sdk.ValAddress // validator address 58 | PubKey crypto.PubKey // public key of the validator 59 | } 60 | 61 | ``` 62 | 63 | This message is expected to fail if: 64 | 65 | - another validator with this operator address is already registered 66 | - another validator with this pubkey is already registered 67 | - the description fields are too large 68 | - if `AcceptAllValidators` is set to `true` 69 | 70 | This message creates and stores the `Validator` object at appropriate indexes. 71 | The validator always starts as unbonded but may be bonded in the first end-block. 72 | 73 | ## MsgProposeNewWeight 74 | 75 | ```go 76 | type MsgProposeNewWeight struct { 77 | Title string // title of the validator 78 | Description string // description of validator 79 | Validator ValidatorNewWeight // validator of which the increase is proposed for 80 | } 81 | 82 | type ValidatorNewWeight struct { 83 | ValidatorAddress sdk.ValAddress // validator address 84 | PubKey crypto.PubKey // public key of the validator 85 | NewWeight sdk.Int // new weight 86 | } 87 | ``` 88 | 89 | This message is expected to fail if: 90 | 91 | - `IncreaseWeight` is set to false, if the proposed weight is greater than the current weight. 92 | 93 | This message stores the updated `Validator` object. 94 | 95 | ## MsgBeginUnbonding 96 | 97 | The begin unbonding message allows validator to remove themselves from the validator set. 98 | 99 | ```go 100 | type MsgBeginUnbonding struct { 101 | ValidatorAddr sdk.ValAddress 102 | } 103 | ``` 104 | 105 | This message is expected to fail if: 106 | 107 | - the validator doesn't exist 108 | - existing `UnbondingDelegation` has maximum entries as defined by `params.MaxEntries` 109 | 110 | When this message is processed the following actions occur: 111 | 112 | - The validator will be removed from the validator set after the predefined `UnbondingTime` has passed 113 | -------------------------------------------------------------------------------- /incubator/poa/docs/spec/04_end_block.md: -------------------------------------------------------------------------------- 1 | # End-Block 2 | 3 | Each abci end block call, the operations to update queues and validator set 4 | changes are specified to execute. 5 | 6 | ## Validator Set Changes 7 | 8 | The POA validator set is updated during this process by state transitions 9 | that run at the end of every block. As a part of this process any updated 10 | validators are also returned back to Tendermint for inclusion in the Tendermint 11 | validator set which is responsible for validating Tendermint messages at the 12 | consensus layer. Operations are as following: 13 | 14 | - the new validator set is taken as the top `params.MaxValidators` number of 15 | validators retrieved from the ValidatorsByPower index 16 | - the previous validator set is compared with the new validator set: 17 | - missing validators begin unbonding and their `Tokens` are transferred from the 18 | `BondedPool` to the `NotBondedPool` `ModuleAccount` 19 | - new validators are instantly bonded and their `Tokens` are transferred from the 20 | `NotBondedPool` to the `BondedPool` `ModuleAccount` 21 | 22 | In all cases, any validators leaving or entering the bonded validator set or 23 | changing balances and staying within the bonded validator set incur an update 24 | message which is passed back to Tendermint. 25 | 26 | ## Queues 27 | 28 | Within the POA module, certain state-transitions are not instantaneous but take place 29 | over a duration of time (typically the unbonding period). When these 30 | transitions are mature certain operations must take place in order to complete 31 | the state operation. This is achieved through the use of queues which are 32 | checked/processed at the end of each block. 33 | 34 | ### Unbonding Validators 35 | 36 | When a validator is kicked out of the bonded validator set (either through 37 | being jailed, or not having sufficient bonded tokens) it begins the unbonding 38 | process. At this point the validator is said to be an unbonding validator, 39 | whereby it will mature to become an "unbonded validator" after the unbonding 40 | period has passed. 41 | 42 | Each block the validator queue is to be checked for mature unbonding validators 43 | (namely with a completion time <= current time). 44 | -------------------------------------------------------------------------------- /incubator/poa/docs/spec/05_hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | Other modules may register operations to execute when a certain event has 4 | occurred within the POA module. These events can be registered to execute either 5 | right `Before` or `After` the POA event (as per the hook name). The 6 | following hooks can registered with the POA module: 7 | 8 | - `AfterValidatorCreated(Context, ValAddress)` 9 | - called when a validator is created 10 | - `BeforeValidatorModified(Context, ValAddress)` 11 | - called when a validator's state is changed 12 | - `AfterValidatorRemoved(Context, ConsAddress, ValAddress)` 13 | - called when a validator is deleted 14 | - `AfterValidatorBonded(Context, ConsAddress, ValAddress)` 15 | - called when a validator is bonded 16 | - `AfterValidatorBeginUnbonding(Context, ConsAddress, ValAddress)` 17 | - called when a validator begins unbonding 18 | -------------------------------------------------------------------------------- /incubator/poa/docs/spec/06_events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | The POA module emits the following events: 4 | 5 | ## EndBlocker 6 | 7 | | Type | Attribute Key | Attribute Value | 8 | | ------------------ | ------------- | ------------------ | 9 | | complete_unbonding | validator | {validatorAddress} | 10 | 11 | ## Handlers 12 | 13 | ### MsgCreateValidator 14 | 15 | | Type | Attribute Key | Attribute Value | 16 | | ---------------- | ------------- | ------------------ | 17 | | create_validator | validator | {validatorAddress} | 18 | | create_validator | weight | 10 | 19 | | message | module | poa | 20 | | message | action | create_validator | 21 | | message | sender | {senderAddress} | 22 | 23 | ### MsgProposeCreateValidator 24 | 25 | | Type | Attribute Key | Attribute Value | 26 | | ---------------- | ------------- | ------------------ | 27 | | create_validator | validator | {validatorAddress} | 28 | | create_validator | weight | 10 | 29 | | message | module | poa | 30 | | message | action | create_validator | 31 | | message | sender | {senderAddress} | 32 | 33 | ### MsgProposeNewWeight 34 | 35 | | Type | Attribute Key | Attribute Value | 36 | | ---------------- | ------------- | ------------------ | 37 | | create_validator | validator | {validatorAddress} | 38 | | create_validator | newWeight | {newWeight} | 39 | | message | module | poa | 40 | | message | action | new_weight | 41 | | message | sender | {senderAddress} | 42 | 43 | ### MsgEditValidator 44 | 45 | | Type | Attribute Key | Attribute Value | 46 | | ------- | ------------- | --------------- | 47 | | message | module | poa | 48 | | message | action | edit_validator | 49 | | message | sender | {senderAddress} | 50 | -------------------------------------------------------------------------------- /incubator/poa/docs/spec/07_params.md: -------------------------------------------------------------------------------- 1 | # Parameters 2 | 3 | The POA module contains the following parameters: 4 | 5 | | Key | Type | Example | 6 | | ------------------- | ---------------- | ----------------- | 7 | | UnbondingTime | string (time ns) | "259200000000000" | 8 | | MaxValidators | uint16 | 100 | 9 | | KeyMaxEntries | uint16 | 7 | 10 | | AcceptAllValidators | bool | false | 11 | | IncreaseWeight | bool | false | 12 | | DefaultWeight | uint16 | 10 | 13 | -------------------------------------------------------------------------------- /incubator/poa/docs/spec/README.md: -------------------------------------------------------------------------------- 1 | # POA module specification 2 | 3 | ## Overview 4 | 5 | The POA module described here is meant to be used as an alternative and less complex module then the original staking located [here](../staking). In this system full nodes can become validators with out having a token at stake. The security model of a public blockchain using the POA should be based on the governance curated registry that defines validator set and assigns corresponding voting power to each individual validator. 6 | 7 | ## Validator Set Curation 8 | 9 | The validator set currently has two options on how its curated, currently this module is built with reliance on the [governance module](../governance/README.md). By default `AcceptAllValidators` is set to false, meaning a validator needs to make a governance proposal to request to be added to the validator set. If `AcceptAllValidators` is set to true then a validator request will not go through governance and the validator will become one instantly. All validators are defaulted to a weight of 10 on creation. 10 | 11 | If a validator would like to increase his weight then `IncreaseWeight` must be set to true and the proposal to increase the weight will always have to go through governance, but if a validator would like to decrease their weight then they can do this without a governance proposal. 12 | 13 | ## Contents 14 | 15 | 1. **[State](01_state.md)** 16 | - [Pool](01_state.md#pool) 17 | - [LastTotalPower](01_state.md#lasttotalpower) 18 | - [Params](01_state.md#params) 19 | - [Validator](01_state.md#validator) 20 | - [Queues](01_state.md#queues) 21 | 2. **[State Transitions](02_state_transitions.md)** 22 | - [Validators](02_state_transitions.md#validators) 23 | - [Slashing](02_state_transitions.md#slashing) 24 | 3. **[Messages](03_messages.md)** 25 | - [MsgCreateValidator](03_messages.md#msgcreatevalidator) 26 | - [MsgEditValidator](03_messages.md#msgeditvalidator) 27 | - [MsgBeginUnbonding](03_messages.md#msgbeginunbonding) 28 | 4. **[End-Block ](04_end_block.md)** 29 | - [Validator Set Changes](04_end_block.md#validator-set-changes) 30 | - [Queues ](04_end_block.md#queues-) 31 | 5. **[Hooks](05_hooks.md)** 32 | 6. **[Events](06_events.md)** 33 | - [EndBlocker](06_events.md#endblocker) 34 | - [Handlers](06_events.md#handlers) 35 | 7. **[Parameters](07_params.md)** 36 | --------------------------------------------------------------------------------