Merge #4209: NFT Module

* in sync with @okwme/cosmos-nft

* remove tmp tx

* structuring and minor changes

* supply and client files

* adding cli client

* complete cli/tx and rest.go

* cleanup and restructuring

* restructure rest folder

* minor updates on clients

* update querier

* encoding for clients and other changes

* genesis, invariants, and keeper updates

* update types

* make golangcibot happy

* renamed and removed bank keeper

* remove handlers for editmetadata, mint, burn, buy

* nft interface

* minor cleanup

* sort collections and nfts

* balance and find

* nft query and tx

* touch ups

* uint in place of int

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* little fixes:
- fix error to err to avoid collision
- error handling

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* module generalization changes

* fixes

* query with data

* minor updates and TODOs

* fix CLI tx

* golang bot fixes

* handlers and txs done

* update module generalization

* Added very basic tests which for some reason do not work

* fix test

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* fixed test, now we should fix implementation, seems to fail

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* fix test, create new struct instead of changing the old one

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* fix handler with new logic

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* let's make it compile

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* single failing test example, need to be fixed and extended

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* single failing test example, need to be fixed and extended

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* reverting work, still problems unmarshalling inside iterator from test

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* Setter in nft.go should return NFT instead of BaseNFT

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* remove TODOS

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* comment out broken tests, we want at least a green mark here

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* little fixes

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* hopefully no conflict

* minor changes for tests

* change nft id to string, refactors

* messy pause

* Changes Balances to OWners add all necessary functions, updated Keeper with UpdateNFT as as well as MintNFT and made sure they all update Owners

* pause dev to merge sdk master

* go.mod changes

* getting closer still need module.go

* builds!!!

* fix lint begin handler tests

* stableish

* re-order nft attributes, add back mint and burn msgs and handlers

* add errors to minting the same NFT and burning an NFT that doesnt exist

* first querier test

* add simulations for nft msgs

* handler tests check tags now (fixed a bug!)

* update simulation

* generic handler

* need to check if it compiles on another machine

* fix weird interface error

* add back cli

* wtfff

* codec error fixed, logs removed. still returning empty arrays of IDs

* Take empty input as yes answer

Closes: #4564

* Add pending log entry

* merged in master

* marshall errors

* build commands

* working!!!

* linting errors

* remove unused func

* pause

* fix burn error

* fix burn error

* tests for querier

* typo

* tests for NFT types

* module spec standard

* tests for Collection and Collections types

* merge w Fede

* tests for Owner Type

* added genesis tests and beefed up keeper, querier, handler & types tests

* linting errors deadcode

* DONT COVER test_common.go

* add msg type tests

* Update x/nft/internal/keeper/key.go

Co-Authored-By: Federico Kunze <31522760+fedekunze@users.noreply.github.com>

* Update x/nft/genesis.go

Co-Authored-By: Federico Kunze <31522760+fedekunze@users.noreply.github.com>

* Update x/nft/client/cli/query.go

Co-Authored-By: Federico Kunze <31522760+fedekunze@users.noreply.github.com>

* Apply suggestions from code review

* typo

* cleanup events

* split events

* more cleanup

* remove restrictions from default handlers

* not sure where these go mod changes came from

* sim generated changes

* make format

* add mint and burn sims

* move NFT interface to nft/exported

* make format

* NFT spec

* Updates

* more updates

* update specs readme

* fix sims

* rest additions

* rest additions

* fix invariant

* minimal nft without name, description or image

* sim

* fix sim

* fix sim

* fix Update methods

* nothing

* simplify update and remove

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* remove test on memory location

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* TEST to get logs, need to be removed

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* fix simulator editMetadata Msg type

* owner not found start with empty collection

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* artifacts on errors in case of failure, else, no artifacts

Signed-off-by: Karoly Albert Szabo <szabo.karoly.a@gmail.com>

* add more invariant checks to handler_tests

* never forget to overwrite

* merge and update spec

* colins feedback

* code coverage test

* code coverage test

* code coverage test

* spelling

* clean up client

* testing code coverage

* testing code coverage

* testing code coverage

* testing code coverage

* testing code coverage

* Update docs/spec/nft/README.md

Co-Authored-By: frog power 4000 <rigel.rozanski@gmail.com>

* Apply suggestions from code review

Co-Authored-By: frog power 4000 <rigel.rozanski@gmail.com>

* minor changes

* integration tests and fixes

* minor golangCI fixes

* Update simapp/app.go

Co-Authored-By: Bot from GolangCI <42910462+golangcibot@users.noreply.github.com>
This commit is contained in:
Federico Kunze 2019-08-26 18:54:45 +02:00 committed by frog power 4000
parent ee99b8e22a
commit eeb847c845
62 changed files with 5616 additions and 23 deletions

View File

@ -87,6 +87,13 @@ jobs:
- make:
target: test_sim_import_export
description: "Test application import/export simulation"
- run:
command: |
mkdir -p /tmp/errors
cp /tmp/**/app-simulation-seed* /tmp/errors/
when: on_fail
- store_artifacts:
path: /tmp/errors
test_sim_after_import:
executor: golang
@ -94,6 +101,13 @@ jobs:
- make:
target: test_sim_after_import
description: "Test simulation after import"
- run:
command: |
mkdir -p /tmp/errors
cp /tmp/**/app-simulation-seed* /tmp/errors/
when: on_fail
- store_artifacts:
path: /tmp/errors
test_sim_multi_seed_long:
executor: golang
@ -101,6 +115,13 @@ jobs:
- make:
target: test_sim_multi_seed_long
description: "Test multi-seed simulation (long)"
- run:
command: |
mkdir -p /tmp/errors
cp /tmp/**/app-simulation-seed* /tmp/errors/
when: on_fail
- store_artifacts:
path: /tmp/errors
test_sim_multi_seed_short:
executor: golang
@ -108,6 +129,14 @@ jobs:
- make:
target: test_sim_multi_seed_short
description: "Test multi-seed simulation (short)"
- run:
command: |
mkdir -p /tmp/errors
cp /tmp/**/app-simulation-seed* /tmp/errors/
when: on_fail
- store_artifacts:
path: /tmp/errors
test_cover:
executor: golang

View File

@ -24,3 +24,4 @@ ignore:
- "docs"
- "*.md"
- "*.rst"
- "x/**/test_common.go"

View File

@ -3,11 +3,12 @@ package keys
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/multisig"
"github.com/tendermint/tendermint/crypto/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func TestBech32KeysOutput(t *testing.T) {

View File

@ -23,10 +23,7 @@ block.
- [Mint](./mint) - Staking token provision creation.
- [Params](./params) - Globally available parameter store.
- [Supply](./supply) - Total supply of the chain.
## Interchain standards
- [ICS30](./_ics/ics-030-signed-messages.md) - Signed messages standard.
- [NFT](./nft) - Non-fungible tokens.
For details on the underlying blockchain and p2p protocols, see
the [Tendermint specification](https://github.com/tendermint/tendermint/tree/master/docs/spec).

View File

@ -1,7 +1,7 @@
# Specification of Specifications
This file intends to outline the common structure for specifications within
this directory.
this directory.
## Tense
@ -15,7 +15,7 @@ be considered preferable. In certain instances, due to the complex nature of
the functionality being described pseudo-code may the most suitable form of
specification. In these cases use of pseudo-code is permissible, but should be
presented in a concise manner, ideally restricted to only the complex
element as a part of a larger description.
element as a part of a larger description.
## Common Layout
@ -23,20 +23,20 @@ The following generalized file structure should be used to breakdown
specifications for modules. With the exception of README.md, `XX` at the
beginning of the file name should be replaced with a number to indicate
document flow (ex. read `01_state.md` before `02_state_transitions.md`). The
following list is nonbinding and all files are optional.
following list is nonbinding and all files are optional.
- `README.md` - overview of the module
- `XX_concepts.md` - describe specialized concepts and definitions used throughout the spec
- `XX_state.md` - specify and describe structures expected to marshalled into the store, and their keys
- `XX_state_transitions.md` - standard state transition operations triggered by hooks, messages, etc.
- `XX_messages.md` - specify message structure(s) and expected state machine behaviour(s)
- `XX_begin_block.md` - specify any begin-block operations
- `XX_end_block.md` - specify any end-block operations
- `XX_hooks.md` - describe available hooks to be called by/from this module
- `XX_tags.md` - list and describe event tags used
- `XX_params.md` - list all module parameters, their types (in JSON) and examples
- `XX_future_improvements.md` - describe future improvements of this module
- `XX_appendix.md` - supplementary details referenced elsewhere within the spec
- `README.md` - overview of the module
- `XX_concepts.md` - describe specialized concepts and definitions used throughout the spec
- `XX_state.md` - specify and describe structures expected to marshalled into the store, and their keys
- `XX_state_transitions.md` - standard state transition operations triggered by hooks, messages, etc.
- `XX_messages.md` - specify message structure(s) and expected state machine behaviour(s)
- `XX_begin_block.md` - specify any begin-block operations
- `XX_end_block.md` - specify any end-block operations
- `XX_hooks.md` - describe available hooks to be called by/from this module
- `XX_events.md` - list and describe event tags used
- `XX_params.md` - list all module parameters, their types (in JSON) and examples
- `XX_future_improvements.md` - describe future improvements of this module
- `XX_appendix.md` - supplementary details referenced elsewhere within the spec
### Notation for key-value mapping
@ -44,14 +44,14 @@ Within `state.md` the following notation `->` should be used to describe key to
value mapping:
```
key -> value
key -> value
```
to represent byte concatenation the `|` may be used. In addition, encoding
type may be specified, for example:
```
0x00 | addressBytes | address2Bytes -> amino(value_object)
0x00 | addressBytes | address2Bytes -> amino(value_object)
```
Additionally, index mappings may be specified by mapping to the `nil` value, for example:

View File

@ -0,0 +1,50 @@
# Concepts
## NFT
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 its own module as it might no longer be necessary for the flexibility of an interface.
```go
// NFT non fungible token interface
type NFT interface {
GetID() string // unique identifier of the NFT
GetOwner() sdk.AccAddress // gets owner account of the NFT
SetOwner(address sdk.AccAddress) // gets owner account of the NFT
GetTokenURI() string // metadata field: URI to retrieve the of chain metadata of the NFT
EditMetadata(tokenURI string) // edit metadata of the NFT
String() string // string representation of the NFT object
}
```
## Collections
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.
```go
// Collection of non fungible tokens
type Collection struct {
Denom string `json:"denom,omitempty"` // name of the collection; not exported to clients
NFTs []*NFT `json:"nfts"` // NFTs that belongs to a collection
}
```
## Owner
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.
```go
// Owner of non fungible tokens
type Owner struct {
Address sdk.AccAddress `json:"address"`
IDCollections IDCollections `json:"IDCollections"`
}
```
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.
```go
// IDCollection of non fungible tokens
type IDCollection struct {
Denom string `json:"denom"`
IDs []string `json:"IDs"`
}

20
docs/spec/nft/02_state.md Normal file
View File

@ -0,0 +1,20 @@
# State
## Collections
As all NFTs belong to a specific `Collection`, they are kept on store in an array
within each `Collection`. Every time an NFT that belongs to a collection is updated,
it needs to be updated on the corresponding NFT array on the corresponding `Collection`.
`denomHash` is used as part of the key to limit the length of the `denomBytes` which is
a hash of `denomBytes` made from the tendermint [tmhash library](https://github.com/tendermint/tendermint/tree/master/crypto/tmhash).
- Collections: `0x00 | denomHash -> amino(Collection)`
- denomHash: `tmhash(denomBytes)`
## Owners
The ownership of an NFT is set initially when an NFT is minted and needs to be
updated every time there's a transfer or when an NFT is burned.
- Owners: `0x01 | addressBytes | denomHash -> amino(Owner)`
- denomHash: `tmhash(denomBytes)`

View File

@ -0,0 +1,86 @@
# Messages
## MsgTransferNFT
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.**
| **Field** | **Type** | **Description** |
|:----------|:-----------------|:--------------------------------------------------------------------------------------------------------------|
| 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. |
| Recipient | `sdk.AccAddress` | The account address who will receive the NFT as a result of the transfer transaction. |
| Denom | `string` | The denomination of the NFT, necessary as multiple denominations are able to be represented on each chain. |
| ID | `string` | The unique ID of the NFT being transferred |
```go
// MsgTransferNFT defines a TransferNFT message
type MsgTransferNFT struct {
Sender sdk.AccAddress
Recipient sdk.AccAddress
Denom string
ID string
}
```
## MsgEditNFTMetadata
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.**
| **Field** | **Type** | **Description** |
|:------------|:-----------------|:-----------------------------------------------------------------------------------------------------------|
| Sender | `sdk.AccAddress` | The creator of the message |
| ID | `string` | The unique ID of the NFT being edited |
| Denom | `string` | The denomination of the NFT, necessary as multiple denominations are able to be represented on each chain. |
| TokenURI | `string` | The URI pointing to a JSON object that contains subsequent metadata information off-chain |
```go
// MsgEditNFTMetadata edits an NFT's metadata
type MsgEditNFTMetadata struct {
Sender sdk.AccAddress
ID string
Denom string
TokenURI string
}
```
## MsgMintNFT
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.**
| **Field** | **Type** | **Description** |
|:------------|:-----------------|:-----------------------------------------------------------------------------------------|
| Sender | `sdk.AccAddress` | The sender of the Message |
| Recipient | `sdk.AccAddress` | The recipiet of the new NFT |
| ID | `string` | The unique ID of the NFT being minted |
| Denom | `string` | The denomination of the NFT. |
| TokenURI | `string` | The URI pointing to a JSON object that contains subsequent metadata information off-chain |
```go
// MsgMintNFT defines a MintNFT message
type MsgMintNFT struct {
Sender sdk.AccAddress
Recipient sdk.AccAddress
ID string
Denom string
TokenURI string
}
```
### MsgBurnNFT
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.**
| **Field** | **Type** | **Description** |
|:----------|:-----------------|:---------------------------------------------------|
| Sender | `sdk.AccAddress` | The account address of the user burning the token. |
| ID | `string` | The ID of the Token. |
| Denom | `string` | The Denom of the Token. |
```go
// MsgBurnNFT defines a BurnNFT message
type MsgBurnNFT struct {
Sender sdk.AccAddress
ID string
Denom string
}
```

View File

@ -0,0 +1,48 @@
# Events
The nft module emits the following events:
## Handlers
### MsgTransferNFT
| Type | Attribute Key | Attribute Value |
|--------------|---------------|--------------------|
| transfer_nft | denom | {nftDenom} |
| transfer_nft | nft-id | {nftID} |
| transfer_nft | recipient | {recipientAddress} |
| message | module | nft |
| message | action | transfer_nft |
| message | sender | {senderAddress} |
### MsgEditNFTMetadata
| Type | Attribute Key | Attribute Value |
|-------------------|---------------|-------------------|
| edit_nft_metadata | denom | {nftDenom} |
| edit_nft_metadata | nft-id | {nftID} |
| message | module | nft |
| message | action | edit_nft_metadata |
| message | sender | {senderAddress} |
| message | token-uri | {tokenURI} |
### MsgMintNFT
| Type | Attribute Key | Attribute Value |
|----------|---------------|-----------------|
| mint_nft | denom | {nftDenom} |
| mint_nft | nft-id | {nftID} |
| message | module | nft |
| message | action | mint_nft |
| message | sender | {senderAddress} |
| message | token-uri | {tokenURI} |
### MsgBurnNFTs
| Type | Attribute Key | Attribute Value |
|----------|---------------|-----------------|
| burn_nft | denom | {nftDenom} |
| burn_nft | nft-id | {nftID} |
| message | module | nft |
| message | action | burn_nft |
| message | sender | {senderAddress} |

View File

@ -0,0 +1,5 @@
# Future Improvements
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.
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).

View File

@ -0,0 +1,7 @@
# Appendix
* Cosmos SDK: [PR #4209](https://github.com/cosmos/cosmos-sdk/pull/4209)
* Cosmos SDK: [Issue #4046](https://github.com/cosmos/cosmos-sdk/issues/4046)
* Interchain Standards: [ICS #17](https://github.com/cosmos/ics/issues/30)
* Binance: [BEP #7](https://github.com/binance-chain/BEPs/pull/7)
* Ethereum: [EIP #721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md)

99
docs/spec/nft/README.md Normal file
View File

@ -0,0 +1,99 @@
# NFT Specification
## Overview
The NFT Module described here is meant to be used as a module across chains for managing non-fungible token that represent individual assets with unique features. This standard was first developed on Ethereum within the ERC-721 and the subsequent EIP of the same name. This standard utilized the features of the Ethereum blockchain as well as the restrictions. The subsequent ERC-1155 standard addressed some of the restrictions of Ethereum regarding storage costs and semi-fungible assets.
NFTs on application specific blockchains share some but not all features as their Ethereum brethren. Since application specific blockchains are more flexible in how their resources are utilized it makes sense that should have the option of exploiting those resources. This includes the aility to use strings as IDs and to optionally store metadata on chain. The user-flow of composability with smart contracts should also be rethought on application specific blockchains with regard to Inter-Blockchain Communication as it is a different design experience from communication between smart contracts.
## Contents
1. **[Concepts](./01_concepts.md)**
- [NFT](./01_concepts.md#nft)
- [Collections](./01_concepts.md#collections)
2. **[State](./02_state.md)**
- [Collections](./02_state.md#collections)
- [Owners](./02_state.md#owners)
3. **[Messages](./03_messages.md)**
- [Transfer NFT](./03_messages.md#transfer-nft)
- [Edit Metadata](./03_messages.md#edit-metadata)
- [Mint NFT](./03_messages.md#mint-nft)
- [Burn NFT](./03_messages.md#burn-nft)
4. **[Events](./04_events.md)**
5. **[Future Improvements](./05_future_improvements.md)**
## A Note on Metadata & IBC
The BaseNFT includes `tokenURI` in order to be backwards compatible with Ethereum based NFTs. However the `NFT` type is an interface that allows arbitrary metadata to be stored on chain should it need be. Originally the module included `name`, `description` and `image` to demonstrate these capabilities. They were removed in order for the NFT to be more efficient for use cases that don't include a need for that information to be stored on chain. A demonstration of including them will be included in a sample app. It is also under discussion to move all metadata to a separate module that can handle arbitrary amounts of data on chain and can be used to describe assets beyond Non-Fungible Tokens, like normal Fungible Tokens `Coin` that could describe attributes like decimal places and vesting status.
A stand-alone metadata Module would allow for independent standards to evolve regarding arbitrary asset types with expanding precision. The standards supported by [http://schema.org](http://schema.org) and the process of adding nested information is being considered as a starting point for that standard. The Blockchain Gaming Alliance is working on a metadata standard to be used for specifically blockchain gaming assets.
With regards to Inter-Blockchain Communication the responsibility of the integrity of the metadata should be left to the origin chain. If a secondary chain was responsible for storing the source of truth of the metadata for an asset tracking that source of truth would become difficult if not impossible to track. Since origin chains are where the design and use of the NFT is determined, it should be up to that origin chain to decide who can update metadata and under what circumstances. Secondary chains can use IBC queriers to check needed metadata or keep redundant copies of the metadata locally when they receive the NFT originally. In that case it should be up to te secondary chain to keep the metadata in sync if need be, similar to how layer 2 solutions keep metadata in sync with a source of truth using events.
## Custom App-Specific Handlers
Each message type comes with a default handler that can be used by default but will most likely be too limited for each use case. In order to make them useful for as many situations as possible, there are very few limitations on who can execute the Messages and do things like mint, burn or edit metadata. We recommend that custom handlers are created to add in custom logic and restrictions over when the Message types can be executed. Below is an example implementation for initializing the module within the module manager so that a custom handler can be added. This can be seen in the example [NFT app](https://github.com/okwme/cosmos-nft).
```go
// custom-handler.go
// OverrideNFTModule overrides the NFT module for custom handlers
type OverrideNFTModule struct {
nft.AppModule
k nft.Keeper
}
// NewHandler overwrites the legacy NewHandler in order to allow custom logic for handling the messages
func (am OverrideNFTModule) NewHandler() sdk.Handler {
return CustomNFTHandler(am.k)
}
// NewOverrideNFTModule generates a new NFT Module
func NewOverrideNFTModule(appModule nft.AppModule, keeper nft.Keeper) OverrideNFTModule {
return OverrideNFTModule{
AppModule: appModule,
k: keeper,
}
}
```
You can see here that `OverrideNFTModule` is the same as `nft.AppModule` except for the `NewHandler()` method. This method now returns a new Handler called `CustomNFTHandler`. This custom handler can be seen below:
```go
// CustomNFTHandler routes the messages to the handlers
func CustomNFTHandler(k keeper.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case types.MsgTransferNFT:
return nft.HandleMsgTransferNFT(ctx, msg, k)
case types.MsgEditNFTMetadata:
return nft.HandleMsgEditNFTMetadata(ctx, msg, k)
case types.MsgMintNFT:
return HandleMsgMintNFTCustom(ctx, msg, k) // <-- This one is custom, the others fall back onto the default
case types.MsgBurnNFT:
return nft.HandleMsgBurnNFT(ctx, msg, k)
default:
errMsg := fmt.Sprintf("unrecognized nft message type: %T", msg)
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
// HandleMsgMintNFTCustom is a custom handler that handles MsgMintNFT
func HandleMsgMintNFTCustom(ctx sdk.Context, msg types.MsgMintNFT, k keeper.Keeper,
) sdk.Result {
isTwilight := checkTwilight(ctx)
if isTwilight {
return nft.HandleMsgMintNFT(ctx, msg, k)
}
errMsg := fmt.Sprintf("Can't mint astral bodies outside of twilight!")
return sdk.ErrUnknownRequest(errMsg).Result()
}
```
The default handlers are imported here with the NFT module and used for `MsgTransferNFT`, `MsgEditNFTMetadata` and `MsgBurnNFT`. The `MsgMintNFT` however is handled with a custom function called `HandleMsgMintNFTCustom`. This custom function also utilizes the imported NFT module handler `HandleMsgMintNFT`, but only after certain conditions are checked. In this case it checks a function called `checkTwilight` which returns a boolean. Only if `isTwilight` is true will the Message succeed.
This pattern of inheriting and utilizing the module handlers wrapped in custom logic should allow each application specific blockchain to use the NFT while customizing it to their specific requirements.

1
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d
github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8
github.com/cosmos/ledger-cosmos-go v0.10.3
github.com/cosmos/tools v0.0.0-20190729191304-444fa9c55188 // indirect
github.com/fortytw2/leaktest v1.3.0 // indirect
github.com/gogo/protobuf v1.2.1
github.com/golang/mock v1.3.1-0.20190508161146-9fa652df1129

4
go.sum
View File

@ -33,6 +33,10 @@ github.com/cosmos/ledger-cosmos-go v0.10.3 h1:Qhi5yTR5Pg1CaTpd00pxlGwNl4sFRdtK1J
github.com/cosmos/ledger-cosmos-go v0.10.3/go.mod h1:J8//BsAGTo3OC/vDLjMRFLW6q0WAaXvHnVc7ZmE8iUY=
github.com/cosmos/ledger-go v0.9.2 h1:Nnao/dLwaVTk1Q5U9THldpUMMXU94BOTWPddSmVB6pI=
github.com/cosmos/ledger-go v0.9.2/go.mod h1:oZJ2hHAZROdlHiwTg4t7kP+GKIIkBT+o6c9QWFanOyI=
github.com/cosmos/tools v0.0.0-20190729191304-444fa9c55188 h1:KZsNQXLq7ZUURaBjVrztEqZX+d7qpjQkS4a0jbCMHIY=
github.com/cosmos/tools v0.0.0-20190729191304-444fa9c55188/go.mod h1:ycjJZ351OX/Y/DYgZqNn1WLCgpmVH7j29THN8vjbb9U=
github.com/cosmos/tools/cmd/clog v0.0.0-20190722180430-ea942c183cba h1:YhVnGzBkE2TvfBW5fAYBdNVK/3bwTPYVbMaOIGRHFRY=
github.com/cosmos/tools/cmd/clog v0.0.0-20190722180430-ea942c183cba/go.mod h1:TdPuAVaU2rc6K24ejr/AnGznt9Fd2qjtMoRrTO4uFrI=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@ -22,6 +22,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/genutil"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/mint"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/params"
paramsclient "github.com/cosmos/cosmos-sdk/x/params/client"
"github.com/cosmos/cosmos-sdk/x/slashing"
@ -53,6 +54,7 @@ var (
params.AppModuleBasic{},
crisis.AppModuleBasic{},
slashing.AppModuleBasic{},
nft.AppModuleBasic{},
supply.AppModuleBasic{},
)
@ -100,6 +102,7 @@ type SimApp struct {
GovKeeper gov.Keeper
CrisisKeeper crisis.Keeper
ParamsKeeper params.Keeper
NFTKeeper nft.Keeper
// the module manager
mm *module.Manager
@ -122,7 +125,7 @@ func NewSimApp(
keys := sdk.NewKVStoreKeys(bam.MainStoreKey, auth.StoreKey, staking.StoreKey,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
gov.StoreKey, params.StoreKey)
gov.StoreKey, params.StoreKey, nft.StoreKey)
tkeys := sdk.NewTransientStoreKeys(staking.TStoreKey, params.TStoreKey)
app := &SimApp{
@ -156,6 +159,7 @@ func NewSimApp(
app.SlashingKeeper = slashing.NewKeeper(app.cdc, keys[slashing.StoreKey], &stakingKeeper,
slashingSubspace, slashing.DefaultCodespace)
app.CrisisKeeper = crisis.NewKeeper(crisisSubspace, invCheckPeriod, app.SupplyKeeper, auth.FeeCollectorName)
app.NFTKeeper = nft.NewKeeper(app.cdc, keys[nft.StoreKey])
// register the proposal types
govRouter := gov.NewRouter()
@ -185,6 +189,7 @@ func NewSimApp(
mint.NewAppModule(app.MintKeeper),
slashing.NewAppModule(app.SlashingKeeper, app.StakingKeeper),
staking.NewAppModule(app.StakingKeeper, app.AccountKeeper, app.SupplyKeeper),
nft.NewAppModule(app.NFTKeeper),
)
// During begin block slashing happens after distr.BeginBlocker so that

View File

@ -20,4 +20,8 @@ const (
OpWeightMsgUndelegate = "op_weight_msg_undelegate"
OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate"
OpWeightMsgUnjail = "op_weight_msg_unjail"
OpWeightMsgTransferNFT = "op_weight_msg_transfer_nft"
OpWeightMsgEditNFTMetadata = "op_weight_msg_edit_nft_metadata"
OpWeightMsgMintNFT = "op_weight_msg_mint_nft"
OpWeightMsgBurnNFT = "op_weight_msg_burn_nft"
)

View File

@ -24,6 +24,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/gov"
govsimops "github.com/cosmos/cosmos-sdk/x/gov/simulation/operations"
"github.com/cosmos/cosmos-sdk/x/mint"
nftsimops "github.com/cosmos/cosmos-sdk/x/nft/simulation/operations"
"github.com/cosmos/cosmos-sdk/x/params"
paramsimops "github.com/cosmos/cosmos-sdk/x/params/simulation/operations"
"github.com/cosmos/cosmos-sdk/x/simulation"
@ -53,6 +54,7 @@ func testAndRunTxs(app *SimApp, config simulation.Config) []simulation.WeightedO
cdc.MustUnmarshalJSON(bz, &ap)
}
// nolint: govet
return []simulation.WeightedOperation{
{
func(_ *rand.Rand) int {
@ -230,6 +232,50 @@ func testAndRunTxs(app *SimApp, config simulation.Config) []simulation.WeightedO
}(nil),
slashingsimops.SimulateMsgUnjail(app.SlashingKeeper),
},
{
func(_ *rand.Rand) int {
var v int
ap.GetOrGenerate(cdc, OpWeightMsgTransferNFT, &v, nil,
func(_ *rand.Rand) {
v = 100
})
return v
}(nil),
nftsimops.SimulateMsgTransferNFT(app.NFTKeeper),
},
{
func(_ *rand.Rand) int {
var v int
ap.GetOrGenerate(cdc, OpWeightMsgEditNFTMetadata, &v, nil,
func(_ *rand.Rand) {
v = 100
})
return v
}(nil),
nftsimops.SimulateMsgEditNFTMetadata(app.NFTKeeper),
},
{
func(_ *rand.Rand) int {
var v int
ap.GetOrGenerate(cdc, OpWeightMsgMintNFT, &v, nil,
func(_ *rand.Rand) {
v = 100
})
return v
}(nil),
nftsimops.SimulateMsgMintNFT(app.NFTKeeper),
},
{
func(_ *rand.Rand) int {
var v int
ap.GetOrGenerate(cdc, OpWeightMsgBurnNFT, &v, nil,
func(_ *rand.Rand) {
v = 100
})
return v
}(nil),
nftsimops.SimulateMsgBurnNFT(app.NFTKeeper),
},
}
}

View File

@ -13,6 +13,7 @@ import (
tmtypes "github.com/tendermint/tendermint/types"
"github.com/cosmos/cosmos-sdk/x/genaccounts"
nftsim "github.com/cosmos/cosmos-sdk/x/nft/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
@ -95,6 +96,7 @@ func AppStateRandomizedFn(
GenSupplyGenesisState(cdc, amount, numInitiallyBonded, int64(len(accs)), genesisState)
GenGovGenesisState(cdc, r, appParams, genesisState)
GenMintGenesisState(cdc, r, appParams, genesisState)
nftsim.GenNFTGenesisState(cdc, r, accs, appParams, genesisState)
GenDistrGenesisState(cdc, r, appParams, genesisState)
stakingGen := GenStakingGenesisState(cdc, r, accs, amount, numAccs, numInitiallyBonded, appParams, genesisState)
GenSlashingGenesisState(cdc, r, stakingGen, appParams, genesisState)

106
x/nft/alias.go Normal file
View File

@ -0,0 +1,106 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/cosmos/cosmos-sdk/x/nft/internal/keeper
// ALIASGEN: github.com/cosmos/cosmos-sdk/x/nft/internal/types
package nft
import (
"github.com/cosmos/cosmos-sdk/x/nft/internal/keeper"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
const (
QuerySupply = keeper.QuerySupply
QueryOwner = keeper.QueryOwner
QueryOwnerByDenom = keeper.QueryOwnerByDenom
QueryCollection = keeper.QueryCollection
QueryDenoms = keeper.QueryDenoms
QueryNFT = keeper.QueryNFT
DefaultCodespace = types.DefaultCodespace
CodeInvalidCollection = types.CodeInvalidCollection
CodeUnknownCollection = types.CodeUnknownCollection
CodeInvalidNFT = types.CodeInvalidNFT
CodeUnknownNFT = types.CodeUnknownNFT
CodeNFTAlreadyExists = types.CodeNFTAlreadyExists
CodeEmptyMetadata = types.CodeEmptyMetadata
ModuleName = types.ModuleName
StoreKey = types.StoreKey
QuerierRoute = types.QuerierRoute
RouterKey = types.RouterKey
)
var (
// functions aliases
RegisterInvariants = keeper.RegisterInvariants
AllInvariants = keeper.AllInvariants
SupplyInvariant = keeper.SupplyInvariant
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
RegisterCodec = types.RegisterCodec
NewCollection = types.NewCollection
EmptyCollection = types.EmptyCollection
NewCollections = types.NewCollections
ErrInvalidCollection = types.ErrInvalidCollection
ErrUnknownCollection = types.ErrUnknownCollection
ErrInvalidNFT = types.ErrInvalidNFT
ErrNFTAlreadyExists = types.ErrNFTAlreadyExists
ErrUnknownNFT = types.ErrUnknownNFT
ErrEmptyMetadata = types.ErrEmptyMetadata
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
GetCollectionKey = types.GetCollectionKey
SplitOwnerKey = types.SplitOwnerKey
GetOwnersKey = types.GetOwnersKey
GetOwnerKey = types.GetOwnerKey
NewMsgTransferNFT = types.NewMsgTransferNFT
NewMsgEditNFTMetadata = types.NewMsgEditNFTMetadata
NewMsgMintNFT = types.NewMsgMintNFT
NewMsgBurnNFT = types.NewMsgBurnNFT
NewBaseNFT = types.NewBaseNFT
NewNFTs = types.NewNFTs
NewIDCollection = types.NewIDCollection
NewOwner = types.NewOwner
NewQueryCollectionParams = types.NewQueryCollectionParams
NewQueryBalanceParams = types.NewQueryBalanceParams
NewQueryNFTParams = types.NewQueryNFTParams
// variable aliases
ModuleCdc = types.ModuleCdc
EventTypeTransfer = types.EventTypeTransfer
EventTypeEditNFTMetadata = types.EventTypeEditNFTMetadata
EventTypeMintNFT = types.EventTypeMintNFT
EventTypeBurnNFT = types.EventTypeBurnNFT
AttributeValueCategory = types.AttributeValueCategory
AttributeKeySender = types.AttributeKeySender
AttributeKeyRecipient = types.AttributeKeyRecipient
AttributeKeyOwner = types.AttributeKeyOwner
AttributeKeyNFTID = types.AttributeKeyNFTID
AttributeKeyNFTTokenURI = types.AttributeKeyNFTTokenURI
AttributeKeyDenom = types.AttributeKeyDenom
CollectionsKeyPrefix = types.CollectionsKeyPrefix
OwnersKeyPrefix = types.OwnersKeyPrefix
)
type (
Keeper = keeper.Keeper
Collection = types.Collection
Collections = types.Collections
CollectionJSON = types.CollectionJSON
CodeType = types.CodeType
GenesisState = types.GenesisState
MsgTransferNFT = types.MsgTransferNFT
MsgEditNFTMetadata = types.MsgEditNFTMetadata
MsgMintNFT = types.MsgMintNFT
MsgBurnNFT = types.MsgBurnNFT
BaseNFT = types.BaseNFT
NFTs = types.NFTs
NFTJSON = types.NFTJSON
IDCollection = types.IDCollection
IDCollections = types.IDCollections
Owner = types.Owner
QueryCollectionParams = types.QueryCollectionParams
QueryBalanceParams = types.QueryBalanceParams
QueryNFTParams = types.QueryNFTParams
)

249
x/nft/client/cli/query.go Normal file
View File

@ -0,0 +1,249 @@
package cli
import (
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/client"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/nft/exported"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
nftQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the NFT module",
}
nftQueryCmd.AddCommand(client.GetCommands(
GetCmdQueryCollectionSupply(queryRoute, cdc),
GetCmdQueryOwner(queryRoute, cdc),
GetCmdQueryCollection(queryRoute, cdc),
GetCmdQueryDenoms(queryRoute, cdc),
GetCmdQueryNFT(queryRoute, cdc),
)...)
return nftQueryCmd
}
// GetCmdQueryCollectionSupply queries the supply of a nft collection
func GetCmdQueryCollectionSupply(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "supply [denom]",
Short: "total supply of a collection of NFTs",
Long: strings.TrimSpace(
fmt.Sprintf(`Get the total count of NFTs that match a certain denomination.
Example:
$ %s query %s supply crypto-kitties
`, version.ClientName, types.ModuleName,
),
),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
denom := args[0]
params := types.NewQueryCollectionParams(denom)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/supply/%s", queryRoute, denom), bz)
if err != nil {
return err
}
var out exported.NFT
err = cdc.UnmarshalJSON(res, &out)
if err != nil {
return err
}
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdQueryOwner queries all the NFTs owned by an account
func GetCmdQueryOwner(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "owner [accountAddress] [denom]",
Short: "get the NFTs owned by an account address",
Long: strings.TrimSpace(
fmt.Sprintf(`Get the NFTs owned by an account address optionally filtered by the denom of the NFTs.
Example:
$ %s query %s owner cosmos1gghjut3ccd8ay0zduzj64hwre2fxs9ld75ru9p
$ %s query %s owner cosmos1gghjut3ccd8ay0zduzj64hwre2fxs9ld75ru9p cripto-kitties
`, version.ClientName, types.ModuleName, version.ClientName, types.ModuleName,
),
),
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
address, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
denom := ""
if len(args) == 2 {
denom = args[1]
}
params := types.NewQueryBalanceParams(address, denom)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
var res []byte
if denom == "" {
res, _, err = cliCtx.QueryWithData(fmt.Sprintf("custom/%s/owner", queryRoute), bz)
} else {
res, _, err = cliCtx.QueryWithData(fmt.Sprintf("custom/%s/ownerByDenom", queryRoute), bz)
}
if err != nil {
return err
}
var out types.Owner
err = cdc.UnmarshalJSON(res, &out)
if err != nil {
return err
}
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdQueryCollection queries all the NFTs from a collection
func GetCmdQueryCollection(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "collection [denom]",
Short: "get all the NFTs from a given collection",
Long: strings.TrimSpace(
fmt.Sprintf(`Get a list of all NFTs from a given collection.
Example:
$ %s query %s collection cripto-kitties
`, version.ClientName, types.ModuleName,
),
),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
denom := args[0]
params := types.NewQueryCollectionParams(denom)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/collection", queryRoute), bz)
if err != nil {
return err
}
var out types.Collection
err = cdc.UnmarshalJSON(res, &out)
if err != nil {
return err
}
return cliCtx.PrintOutput(out)
},
}
}
type stringArray []string
func (s stringArray) String() string { return strings.Join(s[:], ",") }
// GetCmdQueryDenoms queries all denoms
func GetCmdQueryDenoms(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "denoms",
Short: "queries all denominations of all collections of NFTs",
Long: strings.TrimSpace(
fmt.Sprintf(`Gets all denominations of all the available collections of NFTs that
are stored on the chain.
Example:
$ %s query %s denoms
`, version.ClientName, types.ModuleName,
),
),
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/denoms", queryRoute), nil)
if err != nil {
return err
}
var out stringArray
err = cdc.UnmarshalJSON(res, &out)
if err != nil {
return err
}
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdQueryNFT queries a single NFTs from a collection
func GetCmdQueryNFT(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "token [denom] [ID]",
Short: "query a single NFT from a collection",
Long: strings.TrimSpace(
fmt.Sprintf(`Get an NFT from a collection that has the given ID (SHA-256 hex hash).
Example:
$ %s query %s token cripto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa
`, version.ClientName, types.ModuleName,
),
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
denom := args[0]
id := args[1]
params := types.NewQueryNFTParams(denom, id)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/nft", queryRoute), bz)
if err != nil {
return err
}
var out exported.NFT
err = cdc.UnmarshalJSON(res, &out)
if err != nil {
return err
}
return cliCtx.PrintOutput(out)
},
}
}

188
x/nft/client/cli/tx.go Normal file
View File

@ -0,0 +1,188 @@
package cli
import (
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
authtypes "github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// Edit metadata flags
const (
flagTokenURI = "tokenURI"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command {
nftTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "NFT transactions subcommands",
}
nftTxCmd.AddCommand(client.PostCommands(
GetCmdTransferNFT(cdc),
GetCmdEditNFTMetadata(cdc),
GetCmdMintNFT(cdc),
GetCmdBurnNFT(cdc),
)...)
return nftTxCmd
}
// GetCmdTransferNFT is the CLI command for sending a TransferNFT transaction
func GetCmdTransferNFT(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "transfer [sender] [recipient] [denom] [tokenID]",
Short: "transfer a NFT to a recipient",
Long: strings.TrimSpace(
fmt.Sprintf(`Transfer a NFT from a given collection that has a
specific id (SHA-256 hex hash) to a specific recipient.
Example:
$ %s tx %s transfer
cosmos1gghjut3ccd8ay0zduzj64hwre2fxs9ld75ru9p cosmos1l2rsakp388kuv9k8qzq6lrm9taddae7fpx59wm \
cripto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa \
--from mykey
`,
version.ClientName, types.ModuleName,
),
),
Args: cobra.ExactArgs(4),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := authtypes.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
sender, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
recipient, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
denom := args[2]
tokenID := args[3]
msg := types.NewMsgTransferNFT(sender, recipient, denom, tokenID)
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdEditNFTMetadata is the CLI command for sending an EditMetadata transaction
func GetCmdEditNFTMetadata(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "edit-metadata [denom] [tokenID]",
Short: "edit the metadata of an NFT",
Long: strings.TrimSpace(
fmt.Sprintf(`Edit the metadata of an NFT from a given collection that has a
specific id (SHA-256 hex hash).
Example:
$ %s tx %s edit-metadata cripto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa \
--tokenURI path_to_token_URI_JSON --from mykey
`,
version.ClientName, types.ModuleName,
),
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := authtypes.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
denom := args[0]
tokenID := args[1]
tokenURI := viper.GetString(flagTokenURI)
msg := types.NewMsgEditNFTMetadata(cliCtx.GetFromAddress(), tokenID, denom, tokenURI)
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
cmd.Flags().String(flagTokenURI, "", "Extra properties available for querying")
return cmd
}
// GetCmdMintNFT is the CLI command for a MintNFT transaction
func GetCmdMintNFT(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "mint [denom] [tokenID] [recipient]",
Short: "mint an NFT and set the owner to the recipient",
Long: strings.TrimSpace(
fmt.Sprintf(`Mint an NFT from a given collection that has a
specific id (SHA-256 hex hash) and set the ownership to a specific address.
Example:
$ %s tx %s mint cripto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa \
cosmos1gghjut3ccd8ay0zduzj64hwre2fxs9ld75ru9p --from mykey
`,
version.ClientName, types.ModuleName,
),
),
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := authtypes.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
denom := args[0]
tokenID := args[1]
recipient, err := sdk.AccAddressFromBech32(args[2])
if err != nil {
return err
}
tokenURI := viper.GetString(flagTokenURI)
msg := types.NewMsgMintNFT(cliCtx.GetFromAddress(), recipient, tokenID, denom, tokenURI)
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
cmd.Flags().String(flagTokenURI, "", "URI for supplemental off-chain metadata (should return a JSON object)")
return cmd
}
// GetCmdBurnNFT is the CLI command for sending a BurnNFT transaction
func GetCmdBurnNFT(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "burn [denom] [tokenID]",
Short: "burn an NFT",
Long: strings.TrimSpace(
fmt.Sprintf(`Burn (i.e permanently delete) an NFT from a given collection that has a
specific id (SHA-256 hex hash).
Example:
$ %s tx %s burn cripto-kitties d04b98f48e8f8bcc15c6ae5ac050801cd6dcfd428fb5f9e65c4e16e7807340fa \
--from mykey
`,
version.ClientName, types.ModuleName,
),
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := authtypes.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
denom := args[0]
tokenID := args[1]
msg := types.NewMsgBurnNFT(cliCtx.GetFromAddress(), tokenID, denom)
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

177
x/nft/client/rest/query.go Normal file
View File

@ -0,0 +1,177 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, queryRoute string) {
// Get the total supply of a collection
r.HandleFunc(
"/nft/supply/{denom}", getSupply(cdc, cliCtx, queryRoute),
).Methods("GET")
// Get the collections of NFTs owned by an address
r.HandleFunc(
"/nft/owner/{delegatorAddr}", getOwner(cdc, cliCtx, queryRoute),
).Methods("GET")
// Get the NFTs owned by an address from a given collection
r.HandleFunc(
"/nft/owner/{delegatorAddr}/collection/{denom}", getOwnerByDenom(cdc, cliCtx, queryRoute),
).Methods("GET")
// Get all the NFT from a given collection
r.HandleFunc(
"/nft/collection/{denom}", getCollection(cdc, cliCtx, queryRoute),
).Methods("GET")
// Query all denoms
r.HandleFunc(
"/nft/denoms", getDenoms(cdc, cliCtx, queryRoute),
).Methods("GET")
// Query a single NFT
r.HandleFunc(
"/nft/collection/{denom}/nft/{id}", getNFT(cdc, cliCtx, queryRoute),
).Methods("GET")
}
func getSupply(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
denom := mux.Vars(r)["denom"]
params := types.NewQueryCollectionParams(denom)
bz, err := cdc.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/supply/%s", queryRoute, denom), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getOwner(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
address, err := sdk.AccAddressFromBech32(mux.Vars(r)["delegatorAddr"])
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
params := types.NewQueryBalanceParams(address, "")
bz, err := cdc.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/owner", queryRoute), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getOwnerByDenom(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
denom := vars["denom"]
address, err := sdk.AccAddressFromBech32(vars["delegatorAddr"])
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
params := types.NewQueryBalanceParams(address, denom)
bz, err := cdc.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/ownerByDenom", queryRoute), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getCollection(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
denom := mux.Vars(r)["denom"]
params := types.NewQueryCollectionParams(denom)
bz, err := cdc.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/collection", queryRoute), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getDenoms(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/denoms", queryRoute), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getNFT(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
denom := vars["denom"]
id := vars["id"]
params := types.NewQueryNFTParams(denom, id)
bz, err := cdc.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/nft", queryRoute), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}

14
x/nft/client/rest/rest.go Normal file
View File

@ -0,0 +1,14 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
)
// RegisterRoutes register distribution REST routes.
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, queryRoute string) {
registerQueryRoutes(cliCtx, r, cdc, queryRoute)
registerTxRoutes(cliCtx, r, cdc, queryRoute)
}

149
x/nft/client/rest/tx.go Normal file
View File

@ -0,0 +1,149 @@
package rest
import (
"net/http"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
"github.com/gorilla/mux"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router,
cdc *codec.Codec, queryRoute string) {
// Transfer an NFT to an address
r.HandleFunc(
"/nfts/transfer",
transferNFTHandler(cdc, cliCtx),
).Methods("POST")
// Update an NFT metadata
r.HandleFunc(
"/nfts/collection/{denom}/nft/{id}/metadata",
editNFTMetadataHandler(cdc, cliCtx),
).Methods("PUT")
// Mint an NFT
r.HandleFunc(
"/nfts/mint",
mintNFTHandler(cdc, cliCtx),
).Methods("POST")
// Burn an NFT
r.HandleFunc(
"/nfts/collection/{denom}/nft/{id}/burn",
burnNFTHandler(cdc, cliCtx),
).Methods("PUT")
}
type transferNFTReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Denom string `json:"denom"`
ID string `json:"id"`
Recipient string `json:"recipient"`
}
func transferNFTHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req transferNFTReq
if !rest.ReadRESTReq(w, r, cdc, &req) {
rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request")
return
}
baseReq := req.BaseReq.Sanitize()
if !baseReq.ValidateBasic(w) {
return
}
recipient, err := sdk.AccAddressFromBech32(req.Recipient)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// create the message
msg := types.NewMsgTransferNFT(cliCtx.GetFromAddress(), recipient, req.Denom, req.ID)
utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg})
}
}
type editNFTMetadataReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Denom string `json:"denom"`
ID string `json:"id"`
TokenURI string `json:"tokenURI"`
}
func editNFTMetadataHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req editNFTMetadataReq
if !rest.ReadRESTReq(w, r, cdc, &req) {
rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request")
return
}
baseReq := req.BaseReq.Sanitize()
if !baseReq.ValidateBasic(w) {
return
}
// create the message
msg := types.NewMsgEditNFTMetadata(cliCtx.GetFromAddress(), req.ID, req.Denom, req.TokenURI)
utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg})
}
}
type mintNFTReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Recipient sdk.AccAddress `json:"recipient"`
Denom string `json:"denom"`
ID string `json:"id"`
TokenURI string `json:"tokenURI"`
}
func mintNFTHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req mintNFTReq
if !rest.ReadRESTReq(w, r, cdc, &req) {
rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request")
return
}
baseReq := req.BaseReq.Sanitize()
if !baseReq.ValidateBasic(w) {
return
}
// create the message
msg := types.NewMsgMintNFT(cliCtx.GetFromAddress(), req.Recipient, req.ID, req.Denom, req.TokenURI)
utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg})
}
}
type burnNFTReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Denom string `json:"denom"`
ID string `json:"id"`
}
func burnNFTHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req burnNFTReq
if !rest.ReadRESTReq(w, r, cdc, &req) {
rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request")
return
}
baseReq := req.BaseReq.Sanitize()
if !baseReq.ValidateBasic(w) {
return
}
// create the message
msg := types.NewMsgBurnNFT(cliCtx.GetFromAddress(), req.ID, req.Denom)
utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg})
}
}

15
x/nft/exported/nft.go Normal file
View File

@ -0,0 +1,15 @@
package exported
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// NFT non fungible token interface
type NFT interface {
GetID() string
GetOwner() sdk.AccAddress
SetOwner(address sdk.AccAddress)
GetTokenURI() string
EditMetadata(tokenURI string)
String() string
}

20
x/nft/genesis.go Normal file
View File

@ -0,0 +1,20 @@
package nft
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// InitGenesis sets nft information for genesis.
func InitGenesis(ctx sdk.Context, k Keeper, data GenesisState) {
k.SetOwners(ctx, data.Owners)
for _, c := range data.Collections {
k.SetCollection(ctx, c.Denom, c)
}
}
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
return NewGenesisState(k.GetOwners(ctx), k.GetCollections(ctx))
}

62
x/nft/genesis_test.go Normal file
View File

@ -0,0 +1,62 @@
package nft_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/x/nft"
)
func TestInitGenesis(t *testing.T) {
app, ctx := createTestApp(false)
genesisState := nft.DefaultGenesisState()
require.Equal(t, 0, len(genesisState.Owners))
require.Equal(t, 0, len(genesisState.Collections))
ids := []string{id, id2, id3}
idCollection := nft.NewIDCollection(denom, ids)
idCollection2 := nft.NewIDCollection(denom2, ids)
owner := nft.NewOwner(address, idCollection)
owner2 := nft.NewOwner(address2, idCollection2)
owners := []nft.Owner{owner, owner2}
nft1 := nft.NewBaseNFT(id, address, tokenURI1)
nft2 := nft.NewBaseNFT(id2, address, tokenURI1)
nft3 := nft.NewBaseNFT(id3, address, tokenURI1)
nfts := nft.NewNFTs(&nft1, &nft2, &nft3)
collection := nft.NewCollection(denom, nfts)
nftx := nft.NewBaseNFT(id, address2, tokenURI1)
nft2x := nft.NewBaseNFT(id2, address2, tokenURI1)
nft3x := nft.NewBaseNFT(id3, address2, tokenURI1)
nftsx := nft.NewNFTs(&nftx, &nft2x, &nft3x)
collection2 := nft.NewCollection(denom2, nftsx)
collections := nft.NewCollections(collection, collection2)
genesisState = nft.NewGenesisState(owners, collections)
nft.InitGenesis(ctx, app.NFTKeeper, genesisState)
returnedOwners := app.NFTKeeper.GetOwners(ctx)
require.Equal(t, 2, len(owners))
require.Equal(t, returnedOwners[0].String(), owners[0].String())
require.Equal(t, returnedOwners[1].String(), owners[1].String())
returnedCollections := app.NFTKeeper.GetCollections(ctx)
require.Equal(t, 2, len(returnedCollections))
require.Equal(t, returnedCollections[0].String(), collections[0].String())
require.Equal(t, returnedCollections[1].String(), collections[1].String())
exportedGenesisState := nft.ExportGenesis(ctx, app.NFTKeeper)
require.Equal(t, len(genesisState.Owners), len(exportedGenesisState.Owners))
require.Equal(t, genesisState.Owners[0].String(), exportedGenesisState.Owners[0].String())
require.Equal(t, genesisState.Owners[1].String(), exportedGenesisState.Owners[1].String())
require.Equal(t, len(genesisState.Collections), len(exportedGenesisState.Collections))
require.Equal(t, genesisState.Collections[0].String(), exportedGenesisState.Collections[0].String())
require.Equal(t, genesisState.Collections[1].String(), exportedGenesisState.Collections[1].String())
}

157
x/nft/handler.go Normal file
View File

@ -0,0 +1,157 @@
package nft
import (
"fmt"
abci "github.com/tendermint/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/keeper"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// GenericHandler routes the messages to the handlers
func GenericHandler(k keeper.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case types.MsgTransferNFT:
return HandleMsgTransferNFT(ctx, msg, k)
case types.MsgEditNFTMetadata:
return HandleMsgEditNFTMetadata(ctx, msg, k)
case types.MsgMintNFT:
return HandleMsgMintNFT(ctx, msg, k)
case types.MsgBurnNFT:
return HandleMsgBurnNFT(ctx, msg, k)
default:
errMsg := fmt.Sprintf("unrecognized nft message type: %T", msg)
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
// HandleMsgTransferNFT handler for MsgTransferNFT
func HandleMsgTransferNFT(ctx sdk.Context, msg types.MsgTransferNFT, k keeper.Keeper,
) sdk.Result {
nft, err := k.GetNFT(ctx, msg.Denom, msg.ID)
if err != nil {
return err.Result()
}
// update NFT owner
nft.SetOwner(msg.Recipient)
// update the NFT (owners are updated within the keeper)
err = k.UpdateNFT(ctx, msg.Denom, nft)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeTransfer,
sdk.NewAttribute(types.AttributeKeyRecipient, msg.Recipient.String()),
sdk.NewAttribute(types.AttributeKeyDenom, msg.Denom),
sdk.NewAttribute(types.AttributeKeyNFTID, msg.ID),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
})
return sdk.Result{Events: ctx.EventManager().Events()}
}
// HandleMsgEditNFTMetadata handler for MsgEditNFTMetadata
func HandleMsgEditNFTMetadata(ctx sdk.Context, msg types.MsgEditNFTMetadata, k keeper.Keeper,
) sdk.Result {
nft, err := k.GetNFT(ctx, msg.Denom, msg.ID)
if err != nil {
return err.Result()
}
// update NFT
nft.EditMetadata(msg.TokenURI)
err = k.UpdateNFT(ctx, msg.Denom, nft)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeEditNFTMetadata,
sdk.NewAttribute(types.AttributeKeyDenom, msg.Denom),
sdk.NewAttribute(types.AttributeKeyNFTID, msg.ID),
sdk.NewAttribute(types.AttributeKeyNFTTokenURI, msg.TokenURI),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
})
return sdk.Result{Events: ctx.EventManager().Events()}
}
// HandleMsgMintNFT handles MsgMintNFT
func HandleMsgMintNFT(ctx sdk.Context, msg types.MsgMintNFT, k keeper.Keeper,
) sdk.Result {
nft := types.NewBaseNFT(msg.ID, msg.Recipient, msg.TokenURI)
err := k.MintNFT(ctx, msg.Denom, &nft)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeMintNFT,
sdk.NewAttribute(types.AttributeKeyRecipient, msg.Recipient.String()),
sdk.NewAttribute(types.AttributeKeyDenom, msg.Denom),
sdk.NewAttribute(types.AttributeKeyNFTID, msg.ID),
sdk.NewAttribute(types.AttributeKeyNFTTokenURI, msg.TokenURI),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
})
return sdk.Result{Events: ctx.EventManager().Events()}
}
// HandleMsgBurnNFT handles MsgBurnNFT
func HandleMsgBurnNFT(ctx sdk.Context, msg types.MsgBurnNFT, k keeper.Keeper,
) sdk.Result {
_, err := k.GetNFT(ctx, msg.Denom, msg.ID)
if err != nil {
return err.Result()
}
// remove NFT
err = k.DeleteNFT(ctx, msg.Denom, msg.ID)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeBurnNFT,
sdk.NewAttribute(types.AttributeKeyDenom, msg.Denom),
sdk.NewAttribute(types.AttributeKeyNFTID, msg.ID),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
})
return sdk.Result{Events: ctx.EventManager().Events()}
}
// EndBlocker is run at the end of the block
func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate {
return nil
}

252
x/nft/handler_test.go Normal file
View File

@ -0,0 +1,252 @@
package nft_test
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
const (
module = "module"
denom = "denom"
nftID = "nft-id"
sender = "sender"
recipient = "recipient"
tokenURI = "token-uri"
)
func TestInvalidMsg(t *testing.T) {
app, ctx := createTestApp(false)
h := nft.GenericHandler(app.NFTKeeper)
res := h(ctx, sdk.NewTestMsg())
require.False(t, res.IsOK())
require.True(t, strings.Contains(res.Log, "unrecognized nft message type"))
}
func TestTransferNFTMsg(t *testing.T) {
app, ctx := createTestApp(false)
h := nft.GenericHandler(app.NFTKeeper)
// An NFT to be transferred
nft := types.NewBaseNFT(id, address, "TokenURI")
// Define MsgTransferNft
transferNftMsg := types.NewMsgTransferNFT(address, address2, denom, id)
// handle should fail trying to transfer NFT that doesn't exist
res := h(ctx, transferNftMsg)
require.False(t, res.IsOK(), "%v", res)
// Create token (collection and owner)
app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.True(t, CheckInvariants(app.NFTKeeper, ctx))
// handle should succeed when nft exists and is transferred by owner
res = h(ctx, transferNftMsg)
require.True(t, res.IsOK(), "%v", res)
require.True(t, CheckInvariants(app.NFTKeeper, ctx))
// event events should be emitted correctly
for _, event := range res.Events {
for _, attribute := range event.Attributes {
value := string(attribute.Value)
switch key := string(attribute.Key); key {
case module:
require.Equal(t, value, types.ModuleName)
case denom:
require.Equal(t, value, denom)
case nftID:
require.Equal(t, value, id)
case sender:
require.Equal(t, value, address.String())
case recipient:
require.Equal(t, value, address2.String())
default:
require.Fail(t, fmt.Sprintf("unrecognized event %s", key))
}
}
}
// nft should have been transferred as a result of the message
nftAfterwards, err := app.NFTKeeper.GetNFT(ctx, denom, id)
require.NoError(t, err)
require.True(t, nftAfterwards.GetOwner().Equals(address2))
transferNftMsg = types.NewMsgTransferNFT(address2, address3, denom, id)
// handle should succeed when nft exists and is transferred by owner
res = h(ctx, transferNftMsg)
require.True(t, res.IsOK(), "%v", res)
require.True(t, CheckInvariants(app.NFTKeeper, ctx))
// Create token (collection and owner)
app.NFTKeeper.MintNFT(ctx, denom2, &nft)
require.True(t, CheckInvariants(app.NFTKeeper, ctx))
transferNftMsg = types.NewMsgTransferNFT(address2, address3, denom2, id)
// handle should succeed when nft exists and is transferred by owner
res = h(ctx, transferNftMsg)
require.True(t, res.IsOK(), "%v", res)
require.True(t, CheckInvariants(app.NFTKeeper, ctx))
}
func TestEditNFTMetadataMsg(t *testing.T) {
app, ctx := createTestApp(false)
h := nft.GenericHandler(app.NFTKeeper)
// An NFT to be edited
nft := types.NewBaseNFT(id, address, tokenURI)
// Create token (collection and address)
app.NFTKeeper.MintNFT(ctx, denom, &nft)
// Define MsgTransferNft
failingEditNFTMetadata := types.NewMsgEditNFTMetadata(address, id, denom2, tokenURI2)
res := h(ctx, failingEditNFTMetadata)
require.False(t, res.IsOK(), "%v", res)
// Define MsgTransferNft
editNFTMetadata := types.NewMsgEditNFTMetadata(address, id, denom, tokenURI2)
res = h(ctx, editNFTMetadata)
require.True(t, res.IsOK(), "%v", res)
// event events should be emitted correctly
for _, event := range res.Events {
for _, attribute := range event.Attributes {
value := string(attribute.Value)
switch key := string(attribute.Key); key {
case module:
require.Equal(t, value, types.ModuleName)
case denom:
require.Equal(t, value, denom)
case nftID:
require.Equal(t, value, id)
case sender:
require.Equal(t, value, address.String())
case tokenURI:
require.Equal(t, value, tokenURI2)
default:
require.Fail(t, fmt.Sprintf("unrecognized event %s", key))
}
}
}
nftAfterwards, err := app.NFTKeeper.GetNFT(ctx, denom, id)
require.NoError(t, err)
require.Equal(t, tokenURI2, nftAfterwards.GetTokenURI())
}
func TestMintNFTMsg(t *testing.T) {
app, ctx := createTestApp(false)
h := nft.GenericHandler(app.NFTKeeper)
// Define MsgMintNFT
mintNFT := types.NewMsgMintNFT(address, address, id, denom, tokenURI)
// minting a token should succeed
res := h(ctx, mintNFT)
require.True(t, res.IsOK(), "%v", res)
// event events should be emitted correctly
for _, event := range res.Events {
for _, attribute := range event.Attributes {
value := string(attribute.Value)
switch key := string(attribute.Key); key {
case module:
require.Equal(t, value, types.ModuleName)
case denom:
require.Equal(t, value, denom)
case nftID:
require.Equal(t, value, id)
case sender:
require.Equal(t, value, address.String())
case recipient:
require.Equal(t, value, address.String())
case tokenURI:
require.Equal(t, value, tokenURI)
default:
require.Fail(t, fmt.Sprintf("unrecognized event %s", key))
}
}
}
nftAfterwards, err := app.NFTKeeper.GetNFT(ctx, denom, id)
require.NoError(t, err)
require.Equal(t, tokenURI, nftAfterwards.GetTokenURI())
// minting the same token should fail
res = h(ctx, mintNFT)
require.False(t, res.IsOK(), "%v", res)
require.True(t, CheckInvariants(app.NFTKeeper, ctx))
}
func TestBurnNFTMsg(t *testing.T) {
app, ctx := createTestApp(false)
h := nft.GenericHandler(app.NFTKeeper)
// An NFT to be burned
nft := types.NewBaseNFT(id, address, tokenURI)
// Create token (collection and address)
app.NFTKeeper.MintNFT(ctx, denom, &nft)
exists := app.NFTKeeper.IsNFT(ctx, denom, id)
require.True(t, exists)
// burning a non-existent NFT should fail
failBurnNFT := types.NewMsgBurnNFT(address, id2, denom)
res := h(ctx, failBurnNFT)
require.False(t, res.IsOK(), "%s", res.Log)
// NFT should still exist
exists = app.NFTKeeper.IsNFT(ctx, denom, id)
require.True(t, exists)
// burning the NFt should succeed
burnNFT := types.NewMsgBurnNFT(address, id, denom)
res = h(ctx, burnNFT)
require.True(t, res.IsOK(), "%v", res)
// event events should be emitted correctly
for _, event := range res.Events {
for _, attribute := range event.Attributes {
value := string(attribute.Value)
switch key := string(attribute.Key); key {
case module:
require.Equal(t, value, types.ModuleName)
case denom:
require.Equal(t, value, denom)
case nftID:
require.Equal(t, value, id)
case sender:
require.Equal(t, value, address.String())
default:
require.Fail(t, fmt.Sprintf("unrecognized event %s", key))
}
}
}
// the NFT should not exist after burn
exists = app.NFTKeeper.IsNFT(ctx, denom, id)
require.False(t, exists)
ownerReturned := app.NFTKeeper.GetOwner(ctx, address)
require.Equal(t, 0, ownerReturned.Supply())
require.True(t, CheckInvariants(app.NFTKeeper, ctx))
}

61
x/nft/integration_test.go Normal file
View File

@ -0,0 +1,61 @@
package nft_test
import (
"fmt"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// nolint: deadcode unused
var (
denom1 = "test-denom"
denom2 = "test-denom2"
denom3 = "test-denom3"
id = "1"
id2 = "2"
id3 = "3"
address = types.CreateTestAddrs(1)[0]
address2 = types.CreateTestAddrs(2)[1]
address3 = types.CreateTestAddrs(3)[2]
tokenURI1 = "https://google.com/token-1.json"
tokenURI2 = "https://google.com/token-2.json"
)
func createTestApp(isCheckTx bool) (*simapp.SimApp, sdk.Context) {
app := simapp.Setup(isCheckTx)
ctx := app.BaseApp.NewContext(isCheckTx, abci.Header{})
return app, ctx
}
// CheckInvariants checks the invariants
func CheckInvariants(k nft.Keeper, ctx sdk.Context) bool {
collectionsSupply := make(map[string]int)
ownersCollectionsSupply := make(map[string]int)
k.IterateCollections(ctx, func(collection types.Collection) bool {
collectionsSupply[collection.Denom] = collection.Supply()
return false
})
owners := k.GetOwners(ctx)
for _, owner := range owners {
for _, idCollection := range owner.IDCollections {
ownersCollectionsSupply[idCollection.Denom] += idCollection.Supply()
}
}
for denom, supply := range collectionsSupply {
if supply != ownersCollectionsSupply[denom] {
fmt.Printf("denom is %s, supply is %d, ownerSupply is %d", denom, supply, ownersCollectionsSupply[denom])
return false
}
}
return true
}

View File

@ -0,0 +1,62 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// IterateCollections iterates over collections and performs a function
func (k Keeper) IterateCollections(ctx sdk.Context, handler func(collection types.Collection) (stop bool)) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.CollectionsKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var collection types.Collection
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &collection)
if handler(collection) {
break
}
}
}
// SetCollection sets the entire collection of a single denom
func (k Keeper) SetCollection(ctx sdk.Context, denom string, collection types.Collection) {
store := ctx.KVStore(k.storeKey)
collectionKey := types.GetCollectionKey(denom)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(collection)
store.Set(collectionKey, bz)
}
// GetCollection returns a collection of NFTs
func (k Keeper) GetCollection(ctx sdk.Context, denom string) (collection types.Collection, found bool) {
store := ctx.KVStore(k.storeKey)
collectionKey := types.GetCollectionKey(denom)
bz := store.Get(collectionKey)
if bz == nil {
return
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &collection)
return collection, true
}
// GetCollections returns all the NFTs collections
func (k Keeper) GetCollections(ctx sdk.Context) (collections []types.Collection) {
k.IterateCollections(ctx,
func(collection types.Collection) (stop bool) {
collections = append(collections, collection)
return false
},
)
return
}
// GetDenoms returns all the NFT denoms
func (k Keeper) GetDenoms(ctx sdk.Context) (denoms []string) {
k.IterateCollections(ctx,
func(collection types.Collection) (stop bool) {
denoms = append(denoms, collection.Denom)
return false
},
)
return
}

View File

@ -0,0 +1,67 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
func TestSetCollection(t *testing.T) {
app, ctx := createTestApp(false)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
// collection should exist
collection, exists := app.NFTKeeper.GetCollection(ctx, denom)
require.True(t, exists)
nft2 := types.NewBaseNFT(id2, address, tokenURI)
collection, err = collection.AddNFT(&nft2)
require.NoError(t, err)
app.NFTKeeper.SetCollection(ctx, denom, collection)
collection, exists = app.NFTKeeper.GetCollection(ctx, denom)
require.True(t, exists)
require.Len(t, collection.NFTs, 2)
}
func TestGetCollection(t *testing.T) {
app, ctx := createTestApp(false)
// collection shouldn't exist
collection, exists := app.NFTKeeper.GetCollection(ctx, denom)
require.Empty(t, collection)
require.False(t, exists)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
// collection should exist
collection, exists = app.NFTKeeper.GetCollection(ctx, denom)
require.True(t, exists)
require.NotEmpty(t, collection)
}
func TestGetCollections(t *testing.T) {
app, ctx := createTestApp(false)
// collections should be empty
collections := app.NFTKeeper.GetCollections(ctx)
require.Empty(t, collections)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
// collections should equal 1
collections = app.NFTKeeper.GetCollections(ctx)
require.NotEmpty(t, collections)
require.Equal(t, len(collections), 1)
}

View File

@ -0,0 +1,31 @@
package keeper_test
import (
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// nolint: deadcode unused
var (
denom = "test-denom"
denom2 = "test-denom2"
denom3 = "test-denom3"
id = "1"
id2 = "2"
id3 = "3"
address = types.CreateTestAddrs(1)[0]
address2 = types.CreateTestAddrs(2)[1]
address3 = types.CreateTestAddrs(3)[2]
tokenURI = "https://google.com/token-1.json"
tokenURI2 = "https://google.com/token-2.json"
)
func createTestApp(isCheckTx bool) (*simapp.SimApp, sdk.Context) {
app := simapp.Setup(isCheckTx)
ctx := app.BaseApp.NewContext(isCheckTx, abci.Header{})
return app, ctx
}

View File

@ -0,0 +1,59 @@
package keeper
// DONTCOVER
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// RegisterInvariants registers all supply invariants
func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
ir.RegisterRoute(
types.ModuleName, "supply",
SupplyInvariant(k),
)
}
// AllInvariants runs all invariants of the nfts module.
func AllInvariants(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
return SupplyInvariant(k)(ctx)
}
}
// SupplyInvariant checks that the total amount of nfts on collections matches the total amount owned by addresses
func SupplyInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
collectionsSupply := make(map[string]int)
ownersCollectionsSupply := make(map[string]int)
var msg string
count := 0
k.IterateCollections(ctx, func(collection types.Collection) bool {
collectionsSupply[collection.Denom] = collection.Supply()
return false
})
for _, owner := range k.GetOwners(ctx) {
for _, idCollection := range owner.IDCollections {
ownersCollectionsSupply[idCollection.Denom] += idCollection.Supply()
}
}
for denom, supply := range collectionsSupply {
if supply != ownersCollectionsSupply[denom] {
count++
msg += fmt.Sprintf("total %s NFTs supply invariance:\n"+
"\ttotal %s NFTs supply: %d\n"+
"\tsum of %s NFTs by owner: %d\n", denom, denom, supply, denom, ownersCollectionsSupply[denom])
}
}
broken := count != 0
return sdk.FormatInvariant(types.ModuleName, "supply", fmt.Sprintf(
"%d NFT supply invariants found\n%s", count, msg)), broken
}
}

View File

@ -0,0 +1,32 @@
package keeper
import (
"fmt"
"github.com/tendermint/tendermint/libs/log"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// Keeper maintains the link to data storage and exposes getter/setter methods for the various parts of the state machine
type Keeper struct {
storeKey sdk.StoreKey // Unexposed key to access store from sdk.Context
cdc *codec.Codec // The amino codec for binary encoding/decoding.
}
// NewKeeper creates new instances of the nft Keeper
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey) Keeper {
return Keeper{
storeKey: storeKey,
cdc: cdc,
}
}
// Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}

View File

@ -0,0 +1,108 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/exported"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// IsNFT returns whether an NFT exists
func (k Keeper) IsNFT(ctx sdk.Context, denom, id string) (exists bool) {
_, err := k.GetNFT(ctx, denom, id)
return err == nil
}
// GetNFT gets the entire NFT metadata struct for a uint64
func (k Keeper) GetNFT(ctx sdk.Context, denom, id string) (nft exported.NFT, err sdk.Error) {
collection, found := k.GetCollection(ctx, denom)
if !found {
return nil, types.ErrUnknownCollection(types.DefaultCodespace, fmt.Sprintf("collection of %s doesn't exist", denom))
}
nft, err = collection.GetNFT(id)
if err != nil {
return nil, err
}
return nft, err
}
// UpdateNFT updates an already existing NFTs
func (k Keeper) UpdateNFT(ctx sdk.Context, denom string, nft exported.NFT) (err sdk.Error) {
collection, found := k.GetCollection(ctx, denom)
if !found {
return types.ErrUnknownCollection(types.DefaultCodespace,
fmt.Sprintf("collection #%s doesn't exist", denom),
)
}
oldNFT, err := collection.GetNFT(nft.GetID())
if err != nil {
return err
}
// if the owner changed then update the owners KVStore too
if !oldNFT.GetOwner().Equals(nft.GetOwner()) {
err = k.SwapOwners(ctx, denom, nft.GetID(), oldNFT.GetOwner(), nft.GetOwner())
if err != nil {
return err
}
}
collection, err = collection.UpdateNFT(nft)
if err != nil {
return err
}
k.SetCollection(ctx, denom, collection)
return nil
}
// MintNFT mints an NFT and manages that NFTs existence within Collections and Owners
func (k Keeper) MintNFT(ctx sdk.Context, denom string, nft exported.NFT) (err sdk.Error) {
collection, found := k.GetCollection(ctx, denom)
if found {
collection, err = collection.AddNFT(nft)
if err != nil {
return err
}
} else {
collection = types.NewCollection(denom, types.NewNFTs(nft))
}
k.SetCollection(ctx, denom, collection)
ownerIDCollection, _ := k.GetOwnerByDenom(ctx, nft.GetOwner(), denom)
ownerIDCollection = ownerIDCollection.AddID(nft.GetID())
k.SetOwnerByDenom(ctx, nft.GetOwner(), denom, ownerIDCollection.IDs)
return
}
// DeleteNFT deletes an existing NFT from store
func (k Keeper) DeleteNFT(ctx sdk.Context, denom, id string) (err sdk.Error) {
collection, found := k.GetCollection(ctx, denom)
if !found {
return types.ErrUnknownCollection(types.DefaultCodespace, fmt.Sprintf("collection of %s doesn't exist", denom))
}
nft, err := collection.GetNFT(id)
if err != nil {
return err
}
ownerIDCollection, found := k.GetOwnerByDenom(ctx, nft.GetOwner(), denom)
if !found {
return types.ErrUnknownCollection(types.DefaultCodespace,
fmt.Sprintf("id collection #%s doesn't exist for owner %s", denom, nft.GetOwner()),
)
}
ownerIDCollection, err = ownerIDCollection.DeleteID(nft.GetID())
if err != nil {
return err
}
k.SetOwnerByDenom(ctx, nft.GetOwner(), denom, ownerIDCollection.IDs)
collection, err = collection.DeleteNFT(nft)
if err != nil {
return err
}
k.SetCollection(ctx, denom, collection)
return
}

View File

@ -0,0 +1,132 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
func TestMintNFT(t *testing.T) {
app, ctx := createTestApp(false)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
// MintNFT shouldn't fail when collection exists
nft2 := types.NewBaseNFT(id2, address, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom, &nft2)
require.NoError(t, err)
}
func TestGetNFT(t *testing.T) {
app, ctx := createTestApp(false)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
// GetNFT should get the NFT
receivedNFT, err := app.NFTKeeper.GetNFT(ctx, denom, id)
require.NoError(t, err)
require.Equal(t, receivedNFT.GetID(), id)
require.True(t, receivedNFT.GetOwner().Equals(address))
require.Equal(t, receivedNFT.GetTokenURI(), tokenURI)
// MintNFT shouldn't fail when collection exists
nft2 := types.NewBaseNFT(id2, address, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom, &nft2)
require.NoError(t, err)
// GetNFT should get the NFT when collection exists
receivedNFT2, err := app.NFTKeeper.GetNFT(ctx, denom, id2)
require.NoError(t, err)
require.Equal(t, receivedNFT2.GetID(), id2)
require.True(t, receivedNFT2.GetOwner().Equals(address))
require.Equal(t, receivedNFT2.GetTokenURI(), tokenURI)
}
func TestUpdateNFT(t *testing.T) {
app, ctx := createTestApp(false)
nft := types.NewBaseNFT(id, address, tokenURI)
// UpdateNFT should fail when NFT doesn't exists
err := app.NFTKeeper.UpdateNFT(ctx, denom, &nft)
require.Error(t, err)
// MintNFT shouldn't fail when collection does not exist
err = app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
nonnft := types.NewBaseNFT(id2, address, tokenURI)
// UpdateNFT should fail when NFT doesn't exists
err = app.NFTKeeper.UpdateNFT(ctx, denom, &nonnft)
require.Error(t, err)
// UpdateNFT shouldn't fail when NFT exists
nft2 := types.NewBaseNFT(id, address, tokenURI2)
err = app.NFTKeeper.UpdateNFT(ctx, denom, &nft2)
require.NoError(t, err)
// UpdateNFT shouldn't fail when NFT exists
nft2 = types.NewBaseNFT(id, address2, tokenURI2)
err = app.NFTKeeper.UpdateNFT(ctx, denom, &nft2)
require.NoError(t, err)
// GetNFT should get the NFT with new tokenURI
receivedNFT, err := app.NFTKeeper.GetNFT(ctx, denom, id)
require.NoError(t, err)
require.Equal(t, receivedNFT.GetTokenURI(), tokenURI2)
}
func TestDeleteNFT(t *testing.T) {
app, ctx := createTestApp(false)
// DeleteNFT should fail when NFT doesn't exist and collection doesn't exist
err := app.NFTKeeper.DeleteNFT(ctx, denom, id)
require.Error(t, err)
// MintNFT should not fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
// DeleteNFT should fail when NFT doesn't exist but collection does exist
err = app.NFTKeeper.DeleteNFT(ctx, denom, id2)
require.Error(t, err)
// DeleteNFT should not fail when NFT and collection exist
err = app.NFTKeeper.DeleteNFT(ctx, denom, id)
require.NoError(t, err)
// NFT should no longer exist
isNFT := app.NFTKeeper.IsNFT(ctx, denom, id)
require.False(t, isNFT)
owner := app.NFTKeeper.GetOwner(ctx, address)
require.Equal(t, 0, owner.Supply())
}
func TestIsNFT(t *testing.T) {
app, ctx := createTestApp(false)
// IsNFT should return false
isNFT := app.NFTKeeper.IsNFT(ctx, denom, id)
require.False(t, isNFT)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
// IsNFT should return true
isNFT = app.NFTKeeper.IsNFT(ctx, denom, id)
require.True(t, isNFT)
}

View File

@ -0,0 +1,132 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// GetOwners returns all the Owners ID Collections
func (k Keeper) GetOwners(ctx sdk.Context) (owners []types.Owner) {
var foundOwners = make(map[string]bool)
k.IterateOwners(ctx,
func(owner types.Owner) (stop bool) {
if _, ok := foundOwners[owner.Address.String()]; !ok {
foundOwners[owner.Address.String()] = true
owners = append(owners, owner)
}
return false
},
)
return
}
// GetOwner gets all the ID Collections owned by an address
func (k Keeper) GetOwner(ctx sdk.Context, address sdk.AccAddress) (owner types.Owner) {
var idCollections []types.IDCollection
k.IterateIDCollections(ctx, types.GetOwnersKey(address),
func(_ sdk.AccAddress, idCollection types.IDCollection) (stop bool) {
idCollections = append(idCollections, idCollection)
return false
},
)
return types.NewOwner(address, idCollections...)
}
// GetOwnerByDenom gets the ID Collection owned by an address of a specific denom
func (k Keeper) GetOwnerByDenom(ctx sdk.Context, owner sdk.AccAddress, denom string) (idCollection types.IDCollection, found bool) {
store := ctx.KVStore(k.storeKey)
b := store.Get(types.GetOwnerKey(owner, denom))
if b == nil {
return types.NewIDCollection(denom, []string{}), false
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &idCollection)
return idCollection, true
}
// SetOwnerByDenom sets a collection of NFT IDs owned by an address
func (k Keeper) SetOwnerByDenom(ctx sdk.Context, owner sdk.AccAddress, denom string, ids []string) {
store := ctx.KVStore(k.storeKey)
key := types.GetOwnerKey(owner, denom)
var idCollection types.IDCollection
idCollection.Denom = denom
idCollection.IDs = ids
store.Set(key, k.cdc.MustMarshalBinaryLengthPrefixed(idCollection))
}
// SetOwner sets an entire Owner
func (k Keeper) SetOwner(ctx sdk.Context, owner types.Owner) {
for _, idCollection := range owner.IDCollections {
k.SetOwnerByDenom(ctx, owner.Address, idCollection.Denom, idCollection.IDs)
}
}
// SetOwners sets all Owners
func (k Keeper) SetOwners(ctx sdk.Context, owners []types.Owner) {
for _, owner := range owners {
k.SetOwner(ctx, owner)
}
}
// IterateIDCollections iterates over the IDCollections by Owner and performs a function
func (k Keeper) IterateIDCollections(ctx sdk.Context, prefix []byte,
handler func(owner sdk.AccAddress, idCollection types.IDCollection) (stop bool)) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, prefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var idCollection types.IDCollection
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &idCollection)
owner, _ := types.SplitOwnerKey(iterator.Key())
if handler(owner, idCollection) {
break
}
}
}
// IterateOwners iterates over all Owners and performs a function
func (k Keeper) IterateOwners(ctx sdk.Context, handler func(owner types.Owner) (stop bool)) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.OwnersKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var owner types.Owner
address, _ := types.SplitOwnerKey(iterator.Key())
owner = k.GetOwner(ctx, address)
if handler(owner) {
break
}
}
}
// SwapOwners swaps the owners of a NFT ID
func (k Keeper) SwapOwners(ctx sdk.Context, denom string, id string, oldAddress sdk.AccAddress, newAddress sdk.AccAddress) (err sdk.Error) {
oldOwnerIDCollection, found := k.GetOwnerByDenom(ctx, oldAddress, denom)
if !found {
return types.ErrUnknownCollection(types.DefaultCodespace,
fmt.Sprintf("id collection %s doesn't exist for owner %s", denom, oldAddress),
)
}
oldOwnerIDCollection, err = oldOwnerIDCollection.DeleteID(id)
if err != nil {
return err
}
k.SetOwnerByDenom(ctx, oldAddress, denom, oldOwnerIDCollection.IDs)
newOwnerIDCollection, found := k.GetOwnerByDenom(ctx, newAddress, denom)
if !found {
newOwnerIDCollection = types.NewIDCollection(denom, []string{})
}
newOwnerIDCollection = newOwnerIDCollection.AddID(id)
k.SetOwnerByDenom(ctx, newAddress, denom, newOwnerIDCollection.IDs)
return nil
}

View File

@ -0,0 +1,108 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
func TestGetOwners(t *testing.T) {
app, ctx := createTestApp(false)
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
nft2 := types.NewBaseNFT(id2, address2, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom, &nft2)
require.NoError(t, err)
nft3 := types.NewBaseNFT(id3, address3, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom, &nft3)
require.NoError(t, err)
owners := app.NFTKeeper.GetOwners(ctx)
require.Equal(t, 3, len(owners))
nft = types.NewBaseNFT(id, address, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom2, &nft)
require.NoError(t, err)
nft2 = types.NewBaseNFT(id2, address2, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom2, &nft2)
require.NoError(t, err)
nft3 = types.NewBaseNFT(id3, address3, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom2, &nft3)
require.NoError(t, err)
owners = app.NFTKeeper.GetOwners(ctx)
require.Equal(t, 3, len(owners))
}
func TestSetOwner(t *testing.T) {
app, ctx := createTestApp(false)
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
idCollection := types.NewIDCollection(denom, []string{id, id2, id3})
owner := types.NewOwner(address, idCollection)
oldOwner := app.NFTKeeper.GetOwner(ctx, address)
app.NFTKeeper.SetOwner(ctx, owner)
newOwner := app.NFTKeeper.GetOwner(ctx, address)
require.NotEqual(t, oldOwner.String(), newOwner.String())
require.Equal(t, owner.String(), newOwner.String())
}
func TestSetOwners(t *testing.T) {
app, ctx := createTestApp(false)
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
nft = types.NewBaseNFT(id2, address2, tokenURI)
err = app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
idCollection := types.NewIDCollection(denom, []string{id, id2, id3})
owner := types.NewOwner(address, idCollection)
owner2 := types.NewOwner(address2, idCollection)
oldOwner := app.NFTKeeper.GetOwner(ctx, address)
oldOwner2 := app.NFTKeeper.GetOwner(ctx, address2)
app.NFTKeeper.SetOwners(ctx, []types.Owner{owner, owner2})
newOwner := app.NFTKeeper.GetOwner(ctx, address)
require.NotEqual(t, oldOwner.String(), newOwner.String())
require.Equal(t, owner.String(), newOwner.String())
newOwner2 := app.NFTKeeper.GetOwner(ctx, address2)
require.NotEqual(t, oldOwner2.String(), newOwner2.String())
require.Equal(t, owner2.String(), newOwner2.String())
}
func TestSwapOwners(t *testing.T) {
app, ctx := createTestApp(false)
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
err = app.NFTKeeper.SwapOwners(ctx, denom, id, address, address2)
require.NoError(t, err)
err = app.NFTKeeper.SwapOwners(ctx, denom, id, address, address2)
require.Error(t, err)
err = app.NFTKeeper.SwapOwners(ctx, denom2, id, address, address2)
require.Error(t, err)
}

View File

@ -0,0 +1,155 @@
package keeper
import (
"encoding/binary"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
abci "github.com/tendermint/tendermint/abci/types"
)
// query endpoints supported by the NFT Querier
const (
QuerySupply = "supply"
QueryOwner = "owner"
QueryOwnerByDenom = "ownerByDenom"
QueryCollection = "collection"
QueryDenoms = "denoms"
QueryNFT = "nft"
)
// NewQuerier is the module level router for state queries
func NewQuerier(k Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] {
case QuerySupply:
return querySupply(ctx, path[1:], req, k)
case QueryOwner:
return queryOwner(ctx, path[1:], req, k)
case QueryOwnerByDenom:
return queryOwnerByDenom(ctx, path[1:], req, k)
case QueryCollection:
return queryCollection(ctx, path[1:], req, k)
case QueryDenoms:
return queryDenoms(ctx, path[1:], req, k)
case QueryNFT:
return queryNFT(ctx, path[1:], req, k)
default:
return nil, sdk.ErrUnknownRequest("unknown nft query endpoint")
}
}
}
func querySupply(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) {
var params types.QueryCollectionParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
}
collection, found := k.GetCollection(ctx, params.Denom)
if !found {
return nil, types.ErrUnknownCollection(types.DefaultCodespace, fmt.Sprintf("unknown denom %s", params.Denom))
}
bz := make([]byte, 8)
binary.LittleEndian.PutUint64(bz, uint64(collection.Supply()))
return bz, nil
}
func queryOwner(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) {
var params types.QueryBalanceParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
}
owner := k.GetOwner(ctx, params.Owner)
bz, err := types.ModuleCdc.MarshalJSON(owner)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to JSON marshal result: %s", err.Error()))
}
return bz, nil
}
func queryOwnerByDenom(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) {
var params types.QueryBalanceParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
}
var owner types.Owner
idCollection, _ := k.GetOwnerByDenom(ctx, params.Owner, params.Denom)
owner.Address = params.Owner
owner.IDCollections = append(owner.IDCollections, idCollection)
bz, err := types.ModuleCdc.MarshalJSON(owner)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to JSON marshal result: %s", err.Error()))
}
return bz, nil
}
func queryCollection(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) {
var params types.QueryCollectionParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
}
collection, found := k.GetCollection(ctx, params.Denom)
if !found {
return nil, types.ErrUnknownCollection(types.DefaultCodespace, fmt.Sprintf("unknown denom %s", params.Denom))
}
// use Collections custom JSON to make the denom the key of the object
collections := types.NewCollections(collection)
bz, err := types.ModuleCdc.MarshalJSON(collections)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to JSON marshal result: %s", err.Error()))
}
return bz, nil
}
func queryDenoms(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) {
denoms := k.GetDenoms(ctx)
bz, err := types.ModuleCdc.MarshalJSON(denoms)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to JSON marshal result: %s", err.Error()))
}
return bz, nil
}
func queryNFT(ctx sdk.Context, path []string, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) {
var params types.QueryNFTParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error()))
}
nft, err := k.GetNFT(ctx, params.Denom, params.TokenID)
if err != nil {
return nil, types.ErrUnknownNFT(types.DefaultCodespace, fmt.Sprintf("invalid NFT #%s from collection %s", params.TokenID, params.Denom))
}
bz, err := types.ModuleCdc.MarshalJSON(nft)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to JSON marshal result: %s", err.Error()))
}
return bz, nil
}

View File

@ -0,0 +1,261 @@
package keeper_test
import (
"encoding/binary"
"testing"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/x/nft/exported"
keep "github.com/cosmos/cosmos-sdk/x/nft/internal/keeper"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
func TestNewQuerier(t *testing.T) {
app, ctx := createTestApp(false)
querier := keep.NewQuerier(app.NFTKeeper)
query := abci.RequestQuery{
Path: "",
Data: []byte{},
}
_, err := querier(ctx, []string{"foo", "bar"}, query)
require.Error(t, err)
}
func TestQuerySupply(t *testing.T) {
app, ctx := createTestApp(false)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
querier := keep.NewQuerier(app.NFTKeeper)
query := abci.RequestQuery{
Path: "",
Data: []byte{},
}
query.Path = "/custom/nft/supply"
query.Data = []byte("?")
res, err := querier(ctx, []string{"supply"}, query)
require.Error(t, err)
require.Nil(t, res)
queryCollectionParams := types.NewQueryCollectionParams(denom2)
bz, errRes := app.Codec().MarshalJSON(queryCollectionParams)
require.Nil(t, errRes)
query.Data = bz
res, err = querier(ctx, []string{"supply"}, query)
require.Error(t, err)
require.Nil(t, res)
queryCollectionParams = types.NewQueryCollectionParams(denom)
bz, errRes = app.Codec().MarshalJSON(queryCollectionParams)
require.Nil(t, errRes)
query.Data = bz
res, err = querier(ctx, []string{"supply"}, query)
require.NoError(t, err)
require.NotNil(t, res)
supplyResp := binary.LittleEndian.Uint64(res)
require.Equal(t, 1, int(supplyResp))
}
func TestQueryCollection(t *testing.T) {
app, ctx := createTestApp(false)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
querier := keep.NewQuerier(app.NFTKeeper)
query := abci.RequestQuery{
Path: "",
Data: []byte{},
}
query.Path = "/custom/nft/collection"
query.Data = []byte("?")
res, err := querier(ctx, []string{"collection"}, query)
require.Error(t, err)
require.Nil(t, res)
queryCollectionParams := types.NewQueryCollectionParams(denom2)
bz, errRes := app.Codec().MarshalJSON(queryCollectionParams)
require.Nil(t, errRes)
query.Data = bz
res, err = querier(ctx, []string{"collection"}, query)
require.Error(t, err)
require.Nil(t, res)
queryCollectionParams = types.NewQueryCollectionParams(denom)
bz, errRes = app.Codec().MarshalJSON(queryCollectionParams)
require.Nil(t, errRes)
query.Data = bz
res, err = querier(ctx, []string{"collection"}, query)
require.NoError(t, err)
require.NotNil(t, res)
var collections types.Collections
types.ModuleCdc.MustUnmarshalJSON(res, &collections)
require.Len(t, collections, 1)
require.Len(t, collections[0].NFTs, 1)
}
func TestQueryOwner(t *testing.T) {
app, ctx := createTestApp(false)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
denom2 := "test_denom2"
err = app.NFTKeeper.MintNFT(ctx, denom2, &nft)
require.NoError(t, err)
querier := keep.NewQuerier(app.NFTKeeper)
query := abci.RequestQuery{
Path: "",
Data: []byte{},
}
query.Path = "/custom/nft/ownerByDenom"
query.Data = []byte("?")
res, err := querier(ctx, []string{"ownerByDenom"}, query)
require.Error(t, err)
require.Nil(t, res)
// query the balance using the first denom
params := types.NewQueryBalanceParams(address, denom)
bz, err2 := app.Codec().MarshalJSON(params)
require.Nil(t, err2)
query.Data = bz
res, err = querier(ctx, []string{"ownerByDenom"}, query)
require.NoError(t, err)
require.NotNil(t, res)
var out types.Owner
app.Codec().MustUnmarshalJSON(res, &out)
// build the owner using only the first denom
idCollection1 := types.NewIDCollection(denom, []string{id})
owner := types.NewOwner(address, idCollection1)
require.Equal(t, out.String(), owner.String())
// query the balance using no denom so that all denoms will be returns
params = types.NewQueryBalanceParams(address, "")
bz, err2 = app.Codec().MarshalJSON(params)
require.Nil(t, err2)
query.Path = "/custom/nft/owner"
query.Data = []byte("?")
_, err = querier(ctx, []string{"owner"}, query)
require.Error(t, err)
query.Data = bz
res, err = querier(ctx, []string{"owner"}, query)
require.NoError(t, err)
require.NotNil(t, res)
app.Codec().MustUnmarshalJSON(res, &out)
// build the owner using both denoms TODO: add sorting to ensure the objects are the same
idCollection2 := types.NewIDCollection(denom2, []string{id})
owner = types.NewOwner(address, idCollection2, idCollection1)
require.Equal(t, out.String(), owner.String())
}
func TestQueryNFT(t *testing.T) {
app, ctx := createTestApp(false)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
querier := keep.NewQuerier(app.NFTKeeper)
query := abci.RequestQuery{
Path: "",
Data: []byte{},
}
query.Path = "/custom/nft/nft"
var res []byte
query.Data = []byte("?")
res, err = querier(ctx, []string{"nft"}, query)
require.Error(t, err)
require.Nil(t, res)
params := types.NewQueryNFTParams(denom2, id2)
bz, err2 := app.Codec().MarshalJSON(params)
require.Nil(t, err2)
query.Data = bz
res, err = querier(ctx, []string{"nft"}, query)
require.Error(t, err)
require.Nil(t, res)
params = types.NewQueryNFTParams(denom, id)
bz, err2 = app.Codec().MarshalJSON(params)
require.Nil(t, err2)
query.Data = bz
res, err = querier(ctx, []string{"nft"}, query)
require.NoError(t, err)
require.NotNil(t, res)
var out exported.NFT
app.Codec().MustUnmarshalJSON(res, &out)
require.Equal(t, out.String(), nft.String())
}
func TestQueryDenoms(t *testing.T) {
app, ctx := createTestApp(false)
// MintNFT shouldn't fail when collection does not exist
nft := types.NewBaseNFT(id, address, tokenURI)
err := app.NFTKeeper.MintNFT(ctx, denom, &nft)
require.NoError(t, err)
err = app.NFTKeeper.MintNFT(ctx, denom2, &nft)
require.NoError(t, err)
querier := keep.NewQuerier(app.NFTKeeper)
query := abci.RequestQuery{
Path: "",
Data: []byte{},
}
var res []byte
query.Path = "/custom/nft/denoms"
res, err = querier(ctx, []string{"denoms"}, query)
require.NoError(t, err)
require.NotNil(t, res)
denoms := []string{denom, denom2}
var out []string
app.Codec().MustUnmarshalJSON(res, &out)
for key, denomInQuestion := range out {
require.Equal(t, denomInQuestion, denoms[key])
}
}

View File

@ -0,0 +1,31 @@
package types
// DONTCOVER
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/nft/exported"
)
// RegisterCodec concrete types on codec
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterInterface((*exported.NFT)(nil), nil)
cdc.RegisterConcrete(&BaseNFT{}, "cosmos-sdk/BaseNFT", nil)
cdc.RegisterConcrete(&IDCollection{}, "cosmos-sdk/IDCollection", nil)
cdc.RegisterConcrete(&Collection{}, "cosmos-sdk/Collection", nil)
cdc.RegisterConcrete(&Owner{}, "cosmos-sdk/Owner", nil)
cdc.RegisterConcrete(MsgTransferNFT{}, "cosmos-sdk/MsgTransferNFT", nil)
cdc.RegisterConcrete(MsgEditNFTMetadata{}, "cosmos-sdk/MsgEditNFTMetadata", nil)
cdc.RegisterConcrete(MsgMintNFT{}, "cosmos-sdk/MsgMintNFT", nil)
cdc.RegisterConcrete(MsgBurnNFT{}, "cosmos-sdk/MsgBurnNFT", nil)
}
// ModuleCdc generic sealed codec to be used throughout this module
var ModuleCdc *codec.Codec
func init() {
ModuleCdc = codec.New()
codec.RegisterCrypto(ModuleCdc)
RegisterCodec(ModuleCdc)
ModuleCdc.Seal()
}

View File

@ -0,0 +1,232 @@
package types
import (
"encoding/json"
"fmt"
"sort"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/exported"
)
// Collection of non fungible tokens
type Collection struct {
Denom string `json:"denom,omitempty" yaml:"denom"` // name of the collection; not exported to clients
NFTs NFTs `json:"nfts" yaml:"nfts"` // NFTs that belong to a collection
}
// NewCollection creates a new NFT Collection
func NewCollection(denom string, nfts NFTs) Collection {
return Collection{
Denom: strings.TrimSpace(denom),
NFTs: nfts,
}
}
// EmptyCollection returns an empty collection
func EmptyCollection() Collection {
return NewCollection("", NewNFTs())
}
// GetNFT gets a NFT from the collection
func (collection Collection) GetNFT(id string) (nft exported.NFT, err sdk.Error) {
for _, nft := range collection.NFTs {
if nft.GetID() == id {
return nft, nil
}
}
return nil, ErrUnknownNFT(DefaultCodespace,
fmt.Sprintf("NFT #%s doesn't exist in collection %s", id, collection.Denom),
)
}
// ContainsNFT returns whether or not a Collection contains an NFT
func (collection Collection) ContainsNFT(id string) bool {
_, err := collection.GetNFT(id)
return err == nil
}
// AddNFT adds an NFT to the collection
func (collection Collection) AddNFT(nft exported.NFT) (Collection, sdk.Error) {
id := nft.GetID()
exists := collection.ContainsNFT(id)
if exists {
return collection, ErrNFTAlreadyExists(DefaultCodespace,
fmt.Sprintf("NFT #%s already exists in collection %s", id, collection.Denom),
)
}
collection.NFTs = append(collection.NFTs, nft)
return collection, nil
}
// UpdateNFT updates an NFT from a collection
func (collection Collection) UpdateNFT(nft exported.NFT) (Collection, sdk.Error) {
nfts, ok := collection.NFTs.Update(nft.GetID(), nft)
if !ok {
return collection, ErrUnknownNFT(DefaultCodespace,
fmt.Sprintf("NFT #%s doesn't exist on collection %s", nft.GetID(), collection.Denom),
)
}
collection.NFTs = nfts
return collection, nil
}
// DeleteNFT deletes an NFT from a collection
func (collection Collection) DeleteNFT(nft exported.NFT) (Collection, sdk.Error) {
nfts, ok := collection.NFTs.Remove(nft.GetID())
if !ok {
return collection, ErrUnknownNFT(DefaultCodespace,
fmt.Sprintf("NFT #%s doesn't exist on collection %s", nft.GetID(), collection.Denom),
)
}
collection.NFTs = nfts
return collection, nil
}
// Supply gets the total supply of NFTs of a collection
func (collection Collection) Supply() int {
return len(collection.NFTs)
}
// String follows stringer interface
func (collection Collection) String() string {
return fmt.Sprintf(`Denom: %s
NFTs:
%s`,
collection.Denom,
collection.NFTs.String(),
)
}
// ----------------------------------------------------------------------------
// Collections
// Collections define an array of Collection
type Collections []Collection
// NewCollections creates a new set of NFTs
func NewCollections(collections ...Collection) Collections {
if len(collections) == 0 {
return Collections{}
}
return Collections(collections)
}
// Add appends two sets of Collections
func (collections Collections) Add(collectionsB Collections) Collections {
return append(collections, collectionsB...)
}
// Find returns the searched collection from the set
func (collections Collections) Find(denom string) (Collection, bool) {
index := collections.find(denom)
if index == -1 {
return Collection{}, false
}
return collections[index], true
}
// Remove removes a collection from the set of collections
func (collections Collections) Remove(denom string) (Collections, bool) {
index := collections.find(denom)
if index == -1 {
return collections, false
}
collections[len(collections)-1], collections[index] = collections[index], collections[len(collections)-1]
return collections[:len(collections)-1], true
}
// String follows stringer interface
func (collections Collections) String() string {
if len(collections) == 0 {
return ""
}
out := ""
for _, collection := range collections {
out += fmt.Sprintf("%v\n", collection.String())
}
return out[:len(out)-1]
}
// Empty returns true if there are no collections and false otherwise.
func (collections Collections) Empty() bool {
return len(collections) == 0
}
func (collections Collections) find(denom string) (idx int) {
if len(collections) == 0 {
return -1
}
// TODO: ensure this is already sorted
// collections.Sort()
midIdx := len(collections) / 2
midCollection := collections[midIdx]
switch {
case strings.Compare(denom, midCollection.Denom) == -1:
return collections[:midIdx].find(denom)
case midCollection.Denom == denom:
return midIdx
default:
return collections[midIdx+1:].find(denom)
}
}
// ----------------------------------------------------------------------------
// Encoding
// CollectionJSON is the exported Collection format for clients
type CollectionJSON map[string]Collection
// MarshalJSON for Collections
func (collections Collections) MarshalJSON() ([]byte, error) {
collectionJSON := make(CollectionJSON)
for _, collection := range collections {
denom := collection.Denom
collection.Denom = ""
collectionJSON[denom] = collection
}
return json.Marshal(collectionJSON)
}
// UnmarshalJSON for Collections
func (collections *Collections) UnmarshalJSON(b []byte) error {
collectionJSON := make(CollectionJSON)
if err := json.Unmarshal(b, &collectionJSON); err != nil {
return err
}
for denom, collection := range collectionJSON {
*collections = append(*collections, NewCollection(denom, collection.NFTs))
}
return nil
}
//-----------------------------------------------------------------------------
// Sort interface
//nolint
func (collections Collections) Len() int { return len(collections) }
func (collections Collections) Less(i, j int) bool {
return strings.Compare(collections[i].Denom, collections[j].Denom) == -1
}
func (collections Collections) Swap(i, j int) {
collections[i], collections[j] = collections[j], collections[i]
}
var _ sort.Interface = Collections{}
// Sort is a helper function to sort the set of coins inplace
func (collections Collections) Sort() Collections {
sort.Sort(collections)
return collections
}

View File

@ -0,0 +1,347 @@
package types
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
// ---------------------------------------- Collection ---------------------------------------------------
func TestNewCollection(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address, tokenURI)
nfts := NewNFTs(&testNFT, &testNFT2)
collection := NewCollection(fmt.Sprintf(" %s ", denom), nfts)
require.Equal(t, collection.Denom, denom)
require.Equal(t, len(collection.NFTs), 2)
}
func TestEmptyCollection(t *testing.T) {
collection := EmptyCollection()
require.Equal(t, collection.Denom, "")
require.Equal(t, len(collection.NFTs), 0)
}
func TestCollectionGetNFTMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
returnedNFT, err := collection.GetNFT(id)
require.NoError(t, err)
require.Equal(t, testNFT.String(), returnedNFT.String())
returnedNFT, err = collection.GetNFT(id2)
require.Error(t, err)
require.Nil(t, returnedNFT)
}
func TestCollectionContainsNFTMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
contains := collection.ContainsNFT(id)
require.True(t, contains)
contains = collection.ContainsNFT(id2)
require.False(t, contains)
}
func TestCollectionAddNFTMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
newCollection, err := collection.AddNFT(&testNFT)
require.Error(t, err)
require.Equal(t, collection.String(), newCollection.String())
newCollection, err = collection.AddNFT(&testNFT2)
require.NoError(t, err)
require.NotEqual(t, collection.String(), newCollection.String())
require.Equal(t, len(newCollection.NFTs), 2)
}
func TestCollectionUpdateNFTMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
testNFT3 := NewBaseNFT(id, address2, tokenURI2)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
newCollection, err := collection.UpdateNFT(&testNFT2)
require.Error(t, err)
require.Equal(t, collection.String(), newCollection.String())
collection, err = collection.UpdateNFT(&testNFT3)
require.NoError(t, err)
returnedNFT, err := collection.GetNFT(id)
require.NoError(t, err)
require.Equal(t, returnedNFT.GetOwner(), address2)
require.Equal(t, returnedNFT.GetTokenURI(), tokenURI2)
}
func TestCollectionDeleteNFTMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
testNFT3 := NewBaseNFT(id3, address, tokenURI)
nfts := NewNFTs(&testNFT, &testNFT2)
collection := NewCollection(denom, nfts)
newCollection, err := collection.DeleteNFT(&testNFT3)
require.Error(t, err)
require.Equal(t, collection.String(), newCollection.String())
collection, err = collection.DeleteNFT(&testNFT2)
require.NoError(t, err)
require.Equal(t, len(collection.NFTs), 1)
returnedNFT, err := collection.GetNFT(id2)
require.Nil(t, returnedNFT)
require.Error(t, err)
}
func TestCollectionSupplyMethod(t *testing.T) {
empty := EmptyCollection()
require.Equal(t, empty.Supply(), 0)
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts := NewNFTs(&testNFT, &testNFT2)
collection := NewCollection(denom, nfts)
require.Equal(t, collection.Supply(), 2)
collection, err := collection.DeleteNFT(&testNFT)
require.Nil(t, err)
require.Equal(t, collection.Supply(), 1)
collection, err = collection.DeleteNFT(&testNFT2)
require.Nil(t, err)
require.Equal(t, collection.Supply(), 0)
collection, err = collection.AddNFT(&testNFT)
require.Nil(t, err)
require.Equal(t, collection.Supply(), 1)
}
func TestCollectionStringMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts := NewNFTs(&testNFT, &testNFT2)
collection := NewCollection(denom, nfts)
require.Equal(t, collection.String(),
fmt.Sprintf(`Denom: %s
NFTs:
ID: %s
Owner: %s
TokenURI: %s
ID: %s
Owner: %s
TokenURI: %s`, denom, id, address.String(), tokenURI,
id2, address2.String(), tokenURI2))
}
// ---------------------------------------- Collections ---------------------------------------------------
func TestNewCollections(t *testing.T) {
emptyCollections := NewCollections()
require.Empty(t, emptyCollections)
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts2 := NewNFTs(&testNFT2)
collection2 := NewCollection(denom2, nfts2)
collections := NewCollections(collection, collection2)
require.Equal(t, len(collections), 2)
}
func TestCollectionsAddMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
collections := NewCollections(collection)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts2 := NewNFTs(&testNFT2)
collection2 := NewCollection(denom2, nfts2)
collections2 := NewCollections(collection2)
collections = collections.Add(collections2)
require.Equal(t, len(collections), 2)
}
func TestCollectionsFindMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts2 := NewNFTs(&testNFT2)
collection2 := NewCollection(denom2, nfts2)
collections := NewCollections(collection)
foundCollection, found := collections.Find(denom2)
require.False(t, found)
require.Empty(t, foundCollection)
collections = NewCollections(collection, collection2)
foundCollection, found = collections.Find(denom2)
require.True(t, found)
require.Equal(t, foundCollection.String(), collection2.String())
collection3 := NewCollection(denom3, nfts)
collections = NewCollections(collection, collection2, collection3)
_, found = collections.Find(denom)
require.True(t, found)
_, found = collections.Find(denom2)
require.True(t, found)
_, found = collections.Find(denom3)
require.True(t, found)
}
func TestCollectionsRemoveMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
collections := NewCollections(collection)
returnedCollections, removed := collections.Remove(denom2)
require.False(t, removed)
require.Equal(t, returnedCollections.String(), collections.String())
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts2 := NewNFTs(&testNFT2)
collection2 := NewCollection(denom2, nfts2)
collections = NewCollections(collection, collection2)
returnedCollections, removed = collections.Remove(denom2)
require.True(t, removed)
require.NotEqual(t, returnedCollections.String(), collections.String())
require.Equal(t, 1, len(returnedCollections))
foundCollection, found := returnedCollections.Find(denom2)
require.False(t, found)
require.Empty(t, foundCollection)
}
func TestCollectionsStringMethod(t *testing.T) {
collections := NewCollections()
require.Equal(t, collections.String(), "")
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts2 := NewNFTs(&testNFT2)
collection2 := NewCollection(denom2, nfts2)
collections = NewCollections(collection, collection2)
require.Equal(t, fmt.Sprintf(`Denom: %s
NFTs:
ID: %s
Owner: %s
TokenURI: %s
Denom: %s
NFTs:
ID: %s
Owner: %s
TokenURI: %s`, denom, id, address.String(), tokenURI,
denom2, id2, address2.String(), tokenURI2), collections.String())
}
func TestCollectionsEmptyMethod(t *testing.T) {
collections := NewCollections()
require.True(t, collections.Empty())
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
collections = NewCollections(collection)
require.False(t, collections.Empty())
}
func TestCollectionsSortInterface(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts2 := NewNFTs(&testNFT2)
collection2 := NewCollection(denom2, nfts2)
collections := NewCollections(collection, collection2)
require.Equal(t, 2, collections.Len())
require.True(t, collections.Less(0, 1))
require.False(t, collections.Less(1, 0))
collections.Swap(0, 1)
require.False(t, collections.Less(0, 1))
require.True(t, collections.Less(1, 0))
collections.Sort()
require.True(t, collections.Less(0, 1))
require.False(t, collections.Less(1, 0))
}
func TestCollectionMarshalAndUnmarshalJSON(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
collection := NewCollection(denom, nfts)
testNFT2 := NewBaseNFT(id2, address2, tokenURI2)
nfts2 := NewNFTs(&testNFT2)
collection2 := NewCollection(denom2, nfts2)
collections := NewCollections(collection, collection2)
bz, err := collections.MarshalJSON()
require.NoError(t, err)
require.Equal(t, string(bz), fmt.Sprintf(`{"%s":{"nfts":{"%s":{"id":"%s","owner":"%s","token_uri":"%s"}}},"%s":{"nfts":{"%s":{"id":"%s","owner":"%s","token_uri":"%s"}}}}`,
denom, id, id, address.String(), tokenURI,
denom2, id2, id2, address2.String(), tokenURI2,
))
var newCollections Collections
err = newCollections.UnmarshalJSON(bz)
require.NoError(t, err)
err = newCollections.UnmarshalJSON([]byte{})
require.Error(t, err)
}

View File

@ -0,0 +1,62 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// CodeType definition
type CodeType = sdk.CodeType
// NFT error code
const (
DefaultCodespace sdk.CodespaceType = ModuleName
CodeInvalidCollection CodeType = 650
CodeUnknownCollection CodeType = 651
CodeInvalidNFT CodeType = 652
CodeUnknownNFT CodeType = 653
CodeNFTAlreadyExists CodeType = 654
CodeEmptyMetadata CodeType = 655
)
// ErrInvalidCollection is an error
func ErrInvalidCollection(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidCollection, "invalid NFT collection")
}
// ErrUnknownCollection is an error
func ErrUnknownCollection(codespace sdk.CodespaceType, msg string) sdk.Error {
if msg != "" {
return sdk.NewError(codespace, CodeUnknownCollection, msg)
}
return sdk.NewError(codespace, CodeUnknownCollection, "unknown NFT collection")
}
// ErrInvalidNFT is an error
func ErrInvalidNFT(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidNFT, "invalid NFT")
}
// ErrNFTAlreadyExists is an error when an invalid NFT is minted
func ErrNFTAlreadyExists(codespace sdk.CodespaceType, msg string) sdk.Error {
if msg != "" {
return sdk.NewError(codespace, CodeNFTAlreadyExists, msg)
}
return sdk.NewError(codespace, CodeNFTAlreadyExists, "NFT already exists")
}
// ErrUnknownNFT is an error
func ErrUnknownNFT(codespace sdk.CodespaceType, msg string) sdk.Error {
if msg != "" {
return sdk.NewError(codespace, CodeUnknownNFT, msg)
}
return sdk.NewError(codespace, CodeUnknownNFT, "unknown NFT")
}
// ErrEmptyMetadata is an error when metadata is empty
func ErrEmptyMetadata(codespace sdk.CodespaceType, msg string) sdk.Error {
if msg != "" {
return sdk.NewError(codespace, CodeEmptyMetadata, msg)
}
return sdk.NewError(codespace, CodeEmptyMetadata, "NFT metadata can't be empty")
}

View File

@ -0,0 +1,18 @@
package types
// NFT module event types
var (
EventTypeTransfer = "transfer_nft"
EventTypeEditNFTMetadata = "edit_nft_metadata"
EventTypeMintNFT = "mint_nft"
EventTypeBurnNFT = "burn_nft"
AttributeValueCategory = ModuleName
AttributeKeySender = "sender"
AttributeKeyRecipient = "recipient"
AttributeKeyOwner = "owner"
AttributeKeyNFTID = "nft-id"
AttributeKeyNFTTokenURI = "token-uri"
AttributeKeyDenom = "denom"
)

View File

@ -0,0 +1,26 @@
package types
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Owners []Owner `json:"owners"`
Collections Collections `json:"collections"`
}
// NewGenesisState creates a new genesis state.
func NewGenesisState(owners []Owner, collections Collections) GenesisState {
return GenesisState{
Owners: owners,
Collections: collections,
}
}
// DefaultGenesisState returns a default genesis state
func DefaultGenesisState() GenesisState {
return NewGenesisState([]Owner{}, NewCollections())
}
// ValidateGenesis performs basic validation of nfts genesis data returning an
// error for any failed validation criteria.
func ValidateGenesis(data GenesisState) error {
return nil
}

View File

@ -0,0 +1,74 @@
package types
import (
"fmt"
"github.com/tendermint/tendermint/crypto/tmhash"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName is the name of the module
ModuleName = "nft"
// StoreKey is the default store key for NFT
StoreKey = ModuleName
// QuerierRoute is the querier route for the NFT store.
QuerierRoute = ModuleName
// RouterKey is the message route for the NFT module
RouterKey = ModuleName
)
// NFTs are stored as follow:
//
// - Colections: 0x00<denom_bytes_key> :<Collection>
//
// - Owners: 0x01<address_bytes_key><denom_bytes_key>: <Owner>
var (
CollectionsKeyPrefix = []byte{0x00} // key for NFT collections
OwnersKeyPrefix = []byte{0x01} // key for balance of NFTs held by an address
)
// GetCollectionKey gets the key of a collection
func GetCollectionKey(denom string) []byte {
h := tmhash.New()
_, err := h.Write([]byte(denom))
if err != nil {
panic(err)
}
bs := h.Sum(nil)
return append(CollectionsKeyPrefix, bs...)
}
// SplitOwnerKey gets an address and denom from an owner key
func SplitOwnerKey(key []byte) (sdk.AccAddress, []byte) {
if len(key) != 53 {
panic(fmt.Sprintf("unexpected key length %d", len(key)))
}
address := key[1 : sdk.AddrLen+1]
denomHashBz := key[sdk.AddrLen+1:]
return sdk.AccAddress(address), denomHashBz
}
// GetOwnersKey gets the key prefix for all the collections owned by an account address
func GetOwnersKey(address sdk.AccAddress) []byte {
return append(OwnersKeyPrefix, address.Bytes()...)
}
// GetOwnerKey gets the key of a collection owned by an account address
func GetOwnerKey(address sdk.AccAddress, denom string) []byte {
h := tmhash.New()
_, err := h.Write([]byte(denom))
if err != nil {
panic(err)
}
bs := h.Sum(nil)
return append(GetOwnersKey(address), bs...)
}

View File

@ -0,0 +1,228 @@
package types
import (
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
)
/* --------------------------------------------------------------------------- */
// MsgTransferNFT
/* --------------------------------------------------------------------------- */
// MsgTransferNFT defines a TransferNFT message
type MsgTransferNFT struct {
Sender sdk.AccAddress
Recipient sdk.AccAddress
Denom string
ID string
}
// NewMsgTransferNFT is a constructor function for MsgSetName
func NewMsgTransferNFT(sender, recipient sdk.AccAddress, denom, id string) MsgTransferNFT {
return MsgTransferNFT{
Sender: sender,
Recipient: recipient,
Denom: strings.TrimSpace(denom),
ID: strings.TrimSpace(id),
}
}
// Route Implements Msg
func (msg MsgTransferNFT) Route() string { return RouterKey }
// Type Implements Msg
func (msg MsgTransferNFT) Type() string { return "transfer_nft" }
// ValidateBasic Implements Msg.
func (msg MsgTransferNFT) ValidateBasic() sdk.Error {
if strings.TrimSpace(msg.Denom) == "" {
return ErrInvalidCollection(DefaultCodespace)
}
if msg.Sender.Empty() {
return sdk.ErrInvalidAddress("invalid sender address")
}
if msg.Recipient.Empty() {
return sdk.ErrInvalidAddress("invalid recipient address")
}
if strings.TrimSpace(msg.ID) == "" {
return ErrInvalidNFT(DefaultCodespace)
}
return nil
}
// GetSignBytes Implements Msg.
func (msg MsgTransferNFT) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners Implements Msg.
func (msg MsgTransferNFT) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
/* --------------------------------------------------------------------------- */
// MsgEditNFTMetadata
/* --------------------------------------------------------------------------- */
// MsgEditNFTMetadata edits an NFT's metadata
type MsgEditNFTMetadata struct {
Sender sdk.AccAddress
ID string
Denom string
TokenURI string
}
// NewMsgEditNFTMetadata is a constructor function for MsgSetName
func NewMsgEditNFTMetadata(sender sdk.AccAddress, id,
denom, tokenURI string,
) MsgEditNFTMetadata {
return MsgEditNFTMetadata{
Sender: sender,
ID: strings.TrimSpace(id),
Denom: strings.TrimSpace(denom),
TokenURI: strings.TrimSpace(tokenURI),
}
}
// Route Implements Msg
func (msg MsgEditNFTMetadata) Route() string { return RouterKey }
// Type Implements Msg
func (msg MsgEditNFTMetadata) Type() string { return "edit_nft_metadata" }
// ValidateBasic Implements Msg.
func (msg MsgEditNFTMetadata) ValidateBasic() sdk.Error {
if msg.Sender.Empty() {
return sdk.ErrInvalidAddress("invalid sender address")
}
if strings.TrimSpace(msg.ID) == "" {
return ErrInvalidNFT(DefaultCodespace)
}
if strings.TrimSpace(msg.Denom) == "" {
return ErrInvalidNFT(DefaultCodespace)
}
return nil
}
// GetSignBytes Implements Msg.
func (msg MsgEditNFTMetadata) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners Implements Msg.
func (msg MsgEditNFTMetadata) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
/* --------------------------------------------------------------------------- */
// MsgMintNFT
/* --------------------------------------------------------------------------- */
// MsgMintNFT defines a MintNFT message
type MsgMintNFT struct {
Sender sdk.AccAddress
Recipient sdk.AccAddress
ID string
Denom string
TokenURI string
}
// NewMsgMintNFT is a constructor function for MsgMintNFT
func NewMsgMintNFT(sender, recipient sdk.AccAddress, id, denom, tokenURI string) MsgMintNFT {
return MsgMintNFT{
Sender: sender,
Recipient: recipient,
ID: strings.TrimSpace(id),
Denom: strings.TrimSpace(denom),
TokenURI: strings.TrimSpace(tokenURI),
}
}
// Route Implements Msg
func (msg MsgMintNFT) Route() string { return RouterKey }
// Type Implements Msg
func (msg MsgMintNFT) Type() string { return "mint_nft" }
// ValidateBasic Implements Msg.
func (msg MsgMintNFT) ValidateBasic() sdk.Error {
if strings.TrimSpace(msg.Denom) == "" {
return ErrInvalidNFT(DefaultCodespace)
}
if strings.TrimSpace(msg.ID) == "" {
return ErrInvalidNFT(DefaultCodespace)
}
if msg.Sender.Empty() {
return sdk.ErrInvalidAddress("invalid sender address")
}
if msg.Recipient.Empty() {
return sdk.ErrInvalidAddress("invalid recipient address")
}
return nil
}
// GetSignBytes Implements Msg.
func (msg MsgMintNFT) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners Implements Msg.
func (msg MsgMintNFT) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
/* --------------------------------------------------------------------------- */
// MsgBurnNFT
/* --------------------------------------------------------------------------- */
// MsgBurnNFT defines a BurnNFT message
type MsgBurnNFT struct {
Sender sdk.AccAddress
ID string
Denom string
}
// NewMsgBurnNFT is a constructor function for MsgBurnNFT
func NewMsgBurnNFT(sender sdk.AccAddress, id string, denom string) MsgBurnNFT {
return MsgBurnNFT{
Sender: sender,
ID: strings.TrimSpace(id),
Denom: strings.TrimSpace(denom),
}
}
// Route Implements Msg
func (msg MsgBurnNFT) Route() string { return RouterKey }
// Type Implements Msg
func (msg MsgBurnNFT) Type() string { return "burn_nft" }
// ValidateBasic Implements Msg.
func (msg MsgBurnNFT) ValidateBasic() sdk.Error {
if strings.TrimSpace(msg.ID) == "" {
return ErrInvalidNFT(DefaultCodespace)
}
if strings.TrimSpace(msg.Denom) == "" {
return ErrInvalidNFT(DefaultCodespace)
}
if msg.Sender.Empty() {
return sdk.ErrInvalidAddress("invalid sender address")
}
return nil
}
// GetSignBytes Implements Msg.
func (msg MsgBurnNFT) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners Implements Msg.
func (msg MsgBurnNFT) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}

View File

@ -0,0 +1,200 @@
package types
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
// ---------------------------------------- Msgs ---------------------------------------------------
func TestNewMsgTransferNFT(t *testing.T) {
newMsgTransferNFT := NewMsgTransferNFT(address, address2,
fmt.Sprintf(" %s ", denom),
fmt.Sprintf(" %s ", id))
require.Equal(t, newMsgTransferNFT.Sender, address)
require.Equal(t, newMsgTransferNFT.Recipient, address2)
require.Equal(t, newMsgTransferNFT.Denom, denom)
require.Equal(t, newMsgTransferNFT.ID, id)
}
func TestMsgTransferNFTValidateBasicMethod(t *testing.T) {
newMsgTransferNFT := NewMsgTransferNFT(address, address2, "", id)
err := newMsgTransferNFT.ValidateBasic()
require.Error(t, err)
newMsgTransferNFT = NewMsgTransferNFT(address, address2, denom, "")
err = newMsgTransferNFT.ValidateBasic()
require.Error(t, err)
newMsgTransferNFT = NewMsgTransferNFT(nil, address2, denom, "")
err = newMsgTransferNFT.ValidateBasic()
require.Error(t, err)
newMsgTransferNFT = NewMsgTransferNFT(address, nil, denom, "")
err = newMsgTransferNFT.ValidateBasic()
require.Error(t, err)
newMsgTransferNFT = NewMsgTransferNFT(address, address2, denom, id)
err = newMsgTransferNFT.ValidateBasic()
require.NoError(t, err)
}
func TestMsgTransferNFTGetSignBytesMethod(t *testing.T) {
newMsgTransferNFT := NewMsgTransferNFT(address, address2, denom, id)
sortedBytes := newMsgTransferNFT.GetSignBytes()
require.Equal(t, string(sortedBytes), fmt.Sprintf(`{"type":"cosmos-sdk/MsgTransferNFT","value":{"Denom":"%s","ID":"%s","Recipient":"%s","Sender":"%s"}}`,
denom, id, address2, address,
))
}
func TestMsgTransferNFTGetSignersMethod(t *testing.T) {
newMsgTransferNFT := NewMsgTransferNFT(address, address2, denom, id)
signers := newMsgTransferNFT.GetSigners()
require.Equal(t, 1, len(signers))
require.Equal(t, address.String(), signers[0].String())
}
func TestNewMsgEditNFTMetadata(t *testing.T) {
newMsgEditNFTMetadata := NewMsgEditNFTMetadata(address,
fmt.Sprintf(" %s ", id),
fmt.Sprintf(" %s ", denom),
fmt.Sprintf(" %s ", tokenURI))
require.Equal(t, newMsgEditNFTMetadata.Sender.String(), address.String())
require.Equal(t, newMsgEditNFTMetadata.ID, id)
require.Equal(t, newMsgEditNFTMetadata.Denom, denom)
require.Equal(t, newMsgEditNFTMetadata.TokenURI, tokenURI)
}
func TestMsgEditNFTMetadataValidateBasicMethod(t *testing.T) {
newMsgEditNFTMetadata := NewMsgEditNFTMetadata(nil, id, denom, tokenURI)
err := newMsgEditNFTMetadata.ValidateBasic()
require.Error(t, err)
newMsgEditNFTMetadata = NewMsgEditNFTMetadata(address, "", denom, tokenURI)
err = newMsgEditNFTMetadata.ValidateBasic()
require.Error(t, err)
newMsgEditNFTMetadata = NewMsgEditNFTMetadata(address, id, "", tokenURI)
err = newMsgEditNFTMetadata.ValidateBasic()
require.Error(t, err)
newMsgEditNFTMetadata = NewMsgEditNFTMetadata(address, id, denom, tokenURI)
err = newMsgEditNFTMetadata.ValidateBasic()
require.NoError(t, err)
}
func TestMsgEditNFTMetadataGetSignBytesMethod(t *testing.T) {
newMsgEditNFTMetadata := NewMsgEditNFTMetadata(address, id, denom, tokenURI)
sortedBytes := newMsgEditNFTMetadata.GetSignBytes()
require.Equal(t, string(sortedBytes), fmt.Sprintf(`{"type":"cosmos-sdk/MsgEditNFTMetadata","value":{"Denom":"%s","ID":"%s","Sender":"%s","TokenURI":"%s"}}`,
denom, id, address.String(), tokenURI,
))
}
func TestMsgEditNFTMetadataGetSignersMethod(t *testing.T) {
newMsgEditNFTMetadata := NewMsgEditNFTMetadata(address, id, denom, tokenURI)
signers := newMsgEditNFTMetadata.GetSigners()
require.Equal(t, 1, len(signers))
require.Equal(t, address.String(), signers[0].String())
}
func TestNewMsgMintNFT(t *testing.T) {
newMsgMintNFT := NewMsgMintNFT(address, address2,
fmt.Sprintf(" %s ", id),
fmt.Sprintf(" %s ", denom),
fmt.Sprintf(" %s ", tokenURI))
require.Equal(t, newMsgMintNFT.Sender.String(), address.String())
require.Equal(t, newMsgMintNFT.Recipient.String(), address2.String())
require.Equal(t, newMsgMintNFT.ID, id)
require.Equal(t, newMsgMintNFT.Denom, denom)
require.Equal(t, newMsgMintNFT.TokenURI, tokenURI)
}
func TestMsgMsgMintNFTValidateBasicMethod(t *testing.T) {
newMsgMintNFT := NewMsgMintNFT(nil, address2, id, denom, tokenURI)
err := newMsgMintNFT.ValidateBasic()
require.Error(t, err)
newMsgMintNFT = NewMsgMintNFT(address, nil, id, denom, tokenURI)
err = newMsgMintNFT.ValidateBasic()
require.Error(t, err)
newMsgMintNFT = NewMsgMintNFT(address, address2, "", denom, tokenURI)
err = newMsgMintNFT.ValidateBasic()
require.Error(t, err)
newMsgMintNFT = NewMsgMintNFT(address, address2, id, "", tokenURI)
err = newMsgMintNFT.ValidateBasic()
require.Error(t, err)
newMsgMintNFT = NewMsgMintNFT(address, address2, id, denom, tokenURI)
err = newMsgMintNFT.ValidateBasic()
require.NoError(t, err)
}
func TestMsgMintNFTGetSignBytesMethod(t *testing.T) {
newMsgMintNFT := NewMsgMintNFT(address, address2, id, denom, tokenURI)
sortedBytes := newMsgMintNFT.GetSignBytes()
require.Equal(t, string(sortedBytes), fmt.Sprintf(`{"type":"cosmos-sdk/MsgMintNFT","value":{"Denom":"%s","ID":"%s","Recipient":"%s","Sender":"%s","TokenURI":"%s"}}`,
denom, id, address2.String(), address.String(), tokenURI,
))
}
func TestMsgMintNFTGetSignersMethod(t *testing.T) {
newMsgMintNFT := NewMsgMintNFT(address, address2, id, denom, tokenURI)
signers := newMsgMintNFT.GetSigners()
require.Equal(t, 1, len(signers))
require.Equal(t, address.String(), signers[0].String())
}
func TestNewMsgBurnNFT(t *testing.T) {
newMsgBurnNFT := NewMsgBurnNFT(address,
fmt.Sprintf(" %s ", id),
fmt.Sprintf(" %s ", denom))
require.Equal(t, newMsgBurnNFT.Sender.String(), address.String())
require.Equal(t, newMsgBurnNFT.ID, id)
require.Equal(t, newMsgBurnNFT.Denom, denom)
}
func TestMsgMsgBurnNFTValidateBasicMethod(t *testing.T) {
newMsgBurnNFT := NewMsgBurnNFT(nil, id, denom)
err := newMsgBurnNFT.ValidateBasic()
require.Error(t, err)
newMsgBurnNFT = NewMsgBurnNFT(address, "", denom)
err = newMsgBurnNFT.ValidateBasic()
require.Error(t, err)
newMsgBurnNFT = NewMsgBurnNFT(address, id, "")
err = newMsgBurnNFT.ValidateBasic()
require.Error(t, err)
newMsgBurnNFT = NewMsgBurnNFT(address, id, denom)
err = newMsgBurnNFT.ValidateBasic()
require.NoError(t, err)
}
func TestMsgBurnNFTGetSignBytesMethod(t *testing.T) {
newMsgBurnNFT := NewMsgBurnNFT(address, id, denom)
sortedBytes := newMsgBurnNFT.GetSignBytes()
require.Equal(t, string(sortedBytes), fmt.Sprintf(`{"type":"cosmos-sdk/MsgBurnNFT","value":{"Denom":"%s","ID":"%s","Sender":"%s"}}`,
denom, id, address.String(),
))
}
func TestMsgBurnNFTGetSignersMethod(t *testing.T) {
newMsgBurnNFT := NewMsgBurnNFT(address, id, denom)
signers := newMsgBurnNFT.GetSigners()
require.Equal(t, 1, len(signers))
require.Equal(t, address.String(), signers[0].String())
}

189
x/nft/internal/types/nft.go Normal file
View File

@ -0,0 +1,189 @@
package types
import (
"encoding/json"
"fmt"
"sort"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/exported"
)
var _ exported.NFT = (*BaseNFT)(nil)
// BaseNFT non fungible token definition
type BaseNFT struct {
ID string `json:"id,omitempty" yaml:"id"` // id of the token; not exported to clients
Owner sdk.AccAddress `json:"owner" yaml:"owner"` // account address that owns the NFT
TokenURI string `json:"token_uri" yaml:"token_uri"` // optional extra properties available for querying
}
// NewBaseNFT creates a new NFT instance
func NewBaseNFT(id string, owner sdk.AccAddress, tokenURI string) BaseNFT {
return BaseNFT{
ID: id,
Owner: owner,
TokenURI: strings.TrimSpace(tokenURI),
}
}
// GetID returns the ID of the token
func (bnft BaseNFT) GetID() string { return bnft.ID }
// GetOwner returns the account address that owns the NFT
func (bnft BaseNFT) GetOwner() sdk.AccAddress { return bnft.Owner }
// SetOwner updates the owner address of the NFT
func (bnft *BaseNFT) SetOwner(address sdk.AccAddress) {
bnft.Owner = address
}
// GetTokenURI returns the path to optional extra properties
func (bnft BaseNFT) GetTokenURI() string { return bnft.TokenURI }
// EditMetadata edits metadata of an nft
func (bnft *BaseNFT) EditMetadata(tokenURI string) {
bnft.TokenURI = tokenURI
}
func (bnft BaseNFT) String() string {
return fmt.Sprintf(`ID: %s
Owner: %s
TokenURI: %s`,
bnft.ID,
bnft.Owner,
bnft.TokenURI,
)
}
// ----------------------------------------------------------------------------
// NFT
// NFTs define a list of NFT
type NFTs []exported.NFT
// NewNFTs creates a new set of NFTs
func NewNFTs(nfts ...exported.NFT) NFTs {
if len(nfts) == 0 {
return NFTs{}
}
return NFTs(nfts)
}
// Add appends two sets of NFTs
func (nfts NFTs) Add(nftsB NFTs) NFTs {
return append(nfts, nftsB...)
}
// Find returns the searched collection from the set
func (nfts NFTs) Find(id string) (nft exported.NFT, found bool) {
index := nfts.find(id)
if index == -1 {
return nft, false
}
return nfts[index], true
}
// Update removes and replaces an NFT from the set
func (nfts NFTs) Update(id string, nft exported.NFT) (NFTs, bool) {
index := nfts.find(id)
if index == -1 {
return nfts, false
}
return append(append(nfts[:index], nft), nfts[index+1:]...), true
}
// Remove removes an NFT from the set of NFTs
func (nfts NFTs) Remove(id string) (NFTs, bool) {
index := nfts.find(id)
if index == -1 {
return nfts, false
}
return append(nfts[:index], nfts[index+1:]...), true
}
// String follows stringer interface
func (nfts NFTs) String() string {
if len(nfts) == 0 {
return ""
}
out := ""
for _, nft := range nfts {
out += fmt.Sprintf("%v\n", nft.String())
}
return out[:len(out)-1]
}
// Empty returns true if there are no NFTs and false otherwise.
func (nfts NFTs) Empty() bool {
return len(nfts) == 0
}
func (nfts NFTs) find(id string) int {
if len(nfts) == 0 {
return -1
}
midIdx := len(nfts) / 2
nft := nfts[midIdx]
switch {
case strings.Compare(id, nft.GetID()) == -1:
return nfts[:midIdx].find(id)
case id == nft.GetID():
return midIdx
default:
return nfts[midIdx+1:].find(id)
}
}
// ----------------------------------------------------------------------------
// Encoding
// NFTJSON is the exported NFT format for clients
type NFTJSON map[string]BaseNFT
// MarshalJSON for NFTs
func (nfts NFTs) MarshalJSON() ([]byte, error) {
nftJSON := make(NFTJSON)
for _, nft := range nfts {
id := nft.GetID()
bnft := NewBaseNFT(id, nft.GetOwner(), nft.GetTokenURI())
nftJSON[id] = bnft
}
return json.Marshal(nftJSON)
}
// UnmarshalJSON for NFTs
func (nfts *NFTs) UnmarshalJSON(b []byte) error {
nftJSON := make(NFTJSON)
if err := json.Unmarshal(b, &nftJSON); err != nil {
return err
}
for id, nft := range nftJSON {
bnft := NewBaseNFT(id, nft.GetOwner(), nft.GetTokenURI())
*nfts = append(*nfts, &bnft)
}
return nil
}
//-----------------------------------------------------------------------------
// Sort interface
//nolint
func (nfts NFTs) Len() int { return len(nfts) }
func (nfts NFTs) Less(i, j int) bool { return strings.Compare(nfts[i].GetID(), nfts[j].GetID()) == -1 }
func (nfts NFTs) Swap(i, j int) { nfts[i], nfts[j] = nfts[j], nfts[i] }
var _ sort.Interface = NFTs{}
// Sort is a helper function to sort the set of coins inplace
func (nfts NFTs) Sort() NFTs {
sort.Sort(nfts)
return nfts
}

View File

@ -0,0 +1,183 @@
package types
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
// ---------------------------------------- BaseNFT ---------------------------------------------------
func TestBaseNFTGetMethods(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
require.Equal(t, id, testNFT.GetID())
require.Equal(t, address, testNFT.GetOwner())
require.Equal(t, tokenURI, testNFT.GetTokenURI())
}
func TestBaseNFTSetMethods(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT.SetOwner(address2)
require.Equal(t, address2, testNFT.GetOwner())
testNFT.EditMetadata(tokenURI2)
require.Equal(t, tokenURI2, testNFT.GetTokenURI())
}
func TestBaseNFTStringFormat(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
expected := fmt.Sprintf(`ID: %s
Owner: %s
TokenURI: %s`,
id, address, tokenURI)
require.Equal(t, expected, testNFT.String())
}
// ---------------------------------------- NFTs ---------------------------------------------------
func TestNewNFTs(t *testing.T) {
emptyNFTs := NewNFTs()
require.Equal(t, len(emptyNFTs), 0)
testNFT := NewBaseNFT(id, address, tokenURI)
oneNFTs := NewNFTs(&testNFT)
require.Equal(t, len(oneNFTs), 1)
testNFT2 := NewBaseNFT(id2, address, tokenURI)
twoNFTs := NewNFTs(&testNFT, &testNFT2)
require.Equal(t, len(twoNFTs), 2)
}
func TestNFTsAddMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
require.Equal(t, len(nfts), 1)
testNFT2 := NewBaseNFT(id2, address, tokenURI)
nfts2 := NewNFTs(&testNFT2)
nfts = nfts.Add(nfts2)
require.Equal(t, len(nfts), 2)
}
func TestNFTsFindMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address, tokenURI)
nfts := NewNFTs(&testNFT, &testNFT2)
nft, found := nfts.Find(id)
require.True(t, found)
require.Equal(t, nft.String(), testNFT.String())
nft, found = nfts.Find(id3)
require.False(t, found)
require.Nil(t, nft)
}
func TestNFTsUpdateMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address, tokenURI)
nfts := NewNFTs(&testNFT)
var success bool
nfts, success = nfts.Update(id, &testNFT2)
require.True(t, success)
nft, found := nfts.Find(id2)
require.True(t, found)
require.Equal(t, nft.String(), testNFT2.String())
nft, found = nfts.Find(id)
require.False(t, found)
require.Nil(t, nft)
var returnedNFTs NFTs
returnedNFTs, success = nfts.Update(id, &testNFT2)
require.False(t, success)
require.Equal(t, returnedNFTs.String(), nfts.String())
}
func TestNFTsRemoveMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address, tokenURI)
nfts := NewNFTs(&testNFT, &testNFT2)
var success bool
nfts, success = nfts.Remove(id)
require.True(t, success)
require.Equal(t, len(nfts), 1)
nfts, success = nfts.Remove(id2)
require.True(t, success)
require.Equal(t, len(nfts), 0)
var returnedNFTs NFTs
returnedNFTs, success = nfts.Remove(id2)
require.False(t, success)
require.Equal(t, nfts.String(), returnedNFTs.String())
}
func TestNFTsStringMethod(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
require.Equal(t, nfts.String(), fmt.Sprintf(`ID: %s
Owner: %s
TokenURI: %s`, id, address, tokenURI))
}
func TestNFTsEmptyMethod(t *testing.T) {
nfts := NewNFTs()
require.True(t, nfts.Empty())
testNFT := NewBaseNFT(id, address, tokenURI)
nfts = NewNFTs(&testNFT)
require.False(t, nfts.Empty())
}
func TestNFTsMarshalUnmarshalJSON(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
nfts := NewNFTs(&testNFT)
bz, err := nfts.MarshalJSON()
require.NoError(t, err)
require.Equal(t, string(bz),
fmt.Sprintf(`{"%s":{"id":"%s","owner":"%s","token_uri":"%s"}}`,
id, id, address.String(), tokenURI))
var unmarshaledNFTs NFTs
err = unmarshaledNFTs.UnmarshalJSON(bz)
require.NoError(t, err)
require.Equal(t, unmarshaledNFTs.String(), nfts.String())
bz = []byte{}
err = unmarshaledNFTs.UnmarshalJSON(bz)
require.Error(t, err)
}
func TestNFTsSortInterface(t *testing.T) {
testNFT := NewBaseNFT(id, address, tokenURI)
testNFT2 := NewBaseNFT(id2, address, tokenURI)
nfts := NewNFTs(&testNFT)
require.Equal(t, nfts.Len(), 1)
nfts = NewNFTs(&testNFT, &testNFT2)
require.Equal(t, nfts.Len(), 2)
require.True(t, nfts.Less(0, 1))
require.False(t, nfts.Less(1, 0))
nfts.Swap(0, 1)
require.False(t, nfts.Less(0, 1))
require.True(t, nfts.Less(1, 0))
nfts.Sort()
require.True(t, nfts.Less(0, 1))
require.False(t, nfts.Less(1, 0))
}

View File

@ -0,0 +1,202 @@
package types
import (
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// IDCollection defines a set of nft ids that belong to a specific
// collection
type IDCollection struct {
Denom string `json:"denom" yaml:"denom"`
IDs []string `json:"ids" yaml:"ids"`
}
// NewIDCollection creates a new IDCollection instance
func NewIDCollection(denom string, ids []string) IDCollection {
return IDCollection{
Denom: strings.TrimSpace(denom),
IDs: ids,
}
}
// Exists determines whether an ID is in the IDCollection
func (idCollection IDCollection) Exists(id string) (exists bool) {
// TODO: improve performance
for _, _id := range idCollection.IDs {
if _id == id {
return true
}
}
return false
}
// AddID adds an ID to the idCollection
func (idCollection IDCollection) AddID(id string) IDCollection {
idCollection.IDs = append(idCollection.IDs, id)
return idCollection
}
// DeleteID deletes an ID from an ID Collection
func (idCollection IDCollection) DeleteID(id string) (IDCollection, sdk.Error) {
index := stringArray(idCollection.IDs).find(id)
if index == -1 {
return idCollection, ErrUnknownNFT(DefaultCodespace,
fmt.Sprintf("ID #%s doesn't exist on ID Collection %s", id, idCollection.Denom),
)
}
idCollection.IDs = append(idCollection.IDs[:index], idCollection.IDs[index+1:]...)
return idCollection, nil
}
// Supply gets the total supply of NFTIDs of a balance
func (idCollection IDCollection) Supply() int {
return len(idCollection.IDs)
}
// String follows stringer interface
func (idCollection IDCollection) String() string {
return fmt.Sprintf(`Denom: %s
IDs: %s`,
idCollection.Denom,
strings.Join(idCollection.IDs, ","),
)
}
// ----------------------------------------------------------------------------
// Owners
// IDCollections is an array of ID Collections whose sole purpose is for find
type IDCollections []IDCollection
// String follows stringer interface
func (idCollections IDCollections) String() string {
if len(idCollections) == 0 {
return ""
}
out := ""
for _, idCollection := range idCollections {
out += fmt.Sprintf("%v\n", idCollection.String())
}
return out[:len(out)-1]
}
func (idCollections IDCollections) find(el string) int {
if len(idCollections) == 0 {
return -1
}
midIdx := len(idCollections) / 2
midIDCollection := idCollections[midIdx]
switch {
case strings.Compare(el, midIDCollection.Denom) == -1:
return idCollections[:midIdx].find(el)
case midIDCollection.Denom == el:
return midIdx
default:
return idCollections[midIdx+1:].find(el)
}
}
// Owner of non fungible tokens
type Owner struct {
Address sdk.AccAddress `json:"address" yaml:"address"`
IDCollections IDCollections `json:"idCollections" yaml:"idCollections"`
}
// NewOwner creates a new Owner
func NewOwner(owner sdk.AccAddress, idCollections ...IDCollection) Owner {
return Owner{
Address: owner,
IDCollections: idCollections,
}
}
// Supply gets the total supply of an Owner
func (owner Owner) Supply() int {
total := 0
for _, idCollection := range owner.IDCollections {
total += idCollection.Supply()
}
return total
}
// GetIDCollection gets the IDCollection from the owner
func (owner Owner) GetIDCollection(denom string) (IDCollection, bool) {
index := owner.IDCollections.find(denom)
if index == -1 {
return IDCollection{}, false
}
return owner.IDCollections[index], true
}
// UpdateIDCollection updates the ID Collection of an owner
func (owner Owner) UpdateIDCollection(idCollection IDCollection) (Owner, sdk.Error) {
denom := idCollection.Denom
index := owner.IDCollections.find(denom)
if index == -1 {
return owner, ErrUnknownCollection(DefaultCodespace,
fmt.Sprintf("ID Collection %s doesn't exist for owner %s", denom, owner.Address),
)
}
owner.IDCollections = append(append(owner.IDCollections[:index], idCollection), owner.IDCollections[index+1:]...)
return owner, nil
}
// DeleteID deletes an ID from an owners ID Collection
func (owner Owner) DeleteID(denom string, id string) (Owner, sdk.Error) {
idCollection, found := owner.GetIDCollection(denom)
if !found {
return owner, ErrUnknownNFT(DefaultCodespace,
fmt.Sprintf("ID #%s doesn't exist in ID Collection %s", id, denom),
)
}
idCollection, err := idCollection.DeleteID(id)
if err != nil {
return owner, err
}
owner, err = owner.UpdateIDCollection(idCollection)
if err != nil {
return owner, err
}
return owner, nil
}
// String follows stringer interface
func (owner Owner) String() string {
return fmt.Sprintf(`
Address: %s
IDCollections: %s`,
owner.Address,
owner.IDCollections.String(),
)
}
// stringArray is an array of strings whose sole purpose is to help with find
type stringArray []string
func (sa stringArray) find(el string) (idx int) {
if len(sa) == 0 {
return -1
}
midIdx := len(sa) / 2
stringArrayEl := sa[midIdx]
switch {
case strings.Compare(el, stringArrayEl) == -1:
return sa[:midIdx].find(el)
case stringArrayEl == el:
return midIdx
default:
return sa[midIdx+1:].find(el)
}
}

View File

@ -0,0 +1,199 @@
package types
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
// ---------------------------------------- IDCollection ---------------------------------------------------
func TestNewIDCollection(t *testing.T) {
ids := []string{id, id2, id3}
idCollection := NewIDCollection(denom, ids)
require.Equal(t, idCollection.Denom, denom)
require.Equal(t, len(idCollection.IDs), 3)
}
func TestIDCollectionExistsMethod(t *testing.T) {
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
require.True(t, idCollection.Exists(id))
require.True(t, idCollection.Exists(id2))
require.False(t, idCollection.Exists(id3))
}
func TestIDCollectionAddIDMethod(t *testing.T) {
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
idCollection = idCollection.AddID(id3)
require.Equal(t, len(idCollection.IDs), 3)
}
func TestIDCollectionDeleteIDMethod(t *testing.T) {
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
newIDCollection, err := idCollection.DeleteID(id3)
require.Error(t, err)
require.Equal(t, idCollection.String(), newIDCollection.String())
idCollection, err = idCollection.DeleteID(id2)
require.NoError(t, err)
require.Equal(t, len(idCollection.IDs), 1)
}
func TestIDCollectionSupplyMethod(t *testing.T) {
idCollectionEmpty := IDCollection{}
require.Equal(t, 0, idCollectionEmpty.Supply())
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
require.Equal(t, 2, idCollection.Supply())
idCollection, err := idCollection.DeleteID(id)
require.Nil(t, err)
require.Equal(t, idCollection.Supply(), 1)
idCollection, err = idCollection.DeleteID(id2)
require.Nil(t, err)
require.Equal(t, idCollection.Supply(), 0)
idCollection = idCollection.AddID(id)
require.Nil(t, err)
require.Equal(t, idCollection.Supply(), 1)
}
func TestIDCollectionStringMethod(t *testing.T) {
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
require.Equal(t, idCollection.String(), fmt.Sprintf(`Denom: %s
IDs: %s,%s`, denom, id, id2))
}
// ---------------------------------------- IDCollections ---------------------------------------------------
func TestIDCollectionsString(t *testing.T) {
emptyCollections := IDCollections([]IDCollection{})
require.Equal(t, emptyCollections.String(), "")
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
idCollection2 := NewIDCollection(denom2, ids)
idCollections := IDCollections([]IDCollection{idCollection, idCollection2})
require.Equal(t, idCollections.String(), fmt.Sprintf(`Denom: %s
IDs: %s,%s
Denom: %s
IDs: %s,%s`, denom, id, id2, denom2, id, id2))
}
// ---------------------------------------- Owner ---------------------------------------------------
func TestNewOwner(t *testing.T) {
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
idCollection2 := NewIDCollection(denom2, ids)
owner := NewOwner(address, idCollection, idCollection2)
require.Equal(t, owner.Address.String(), address.String())
require.Equal(t, len(owner.IDCollections), 2)
}
func TestOwnerSupplyMethod(t *testing.T) {
owner := NewOwner(address)
require.Equal(t, owner.Supply(), 0)
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
owner = NewOwner(address, idCollection)
require.Equal(t, owner.Supply(), 2)
idCollection2 := NewIDCollection(denom2, ids)
owner = NewOwner(address, idCollection, idCollection2)
require.Equal(t, owner.Supply(), 4)
}
func TestOwnerGetIDCollectionMethod(t *testing.T) {
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
owner := NewOwner(address, idCollection)
gotCollection, found := owner.GetIDCollection(denom2)
require.False(t, found)
require.Equal(t, gotCollection.Denom, "")
require.Equal(t, len(gotCollection.IDs), 0)
require.Equal(t, gotCollection.String(), IDCollection{}.String())
gotCollection, found = owner.GetIDCollection(denom)
require.True(t, found)
require.Equal(t, gotCollection.String(), idCollection.String())
idCollection2 := NewIDCollection(denom2, ids)
owner = NewOwner(address, idCollection, idCollection2)
gotCollection, found = owner.GetIDCollection(denom)
require.True(t, found)
require.Equal(t, gotCollection.String(), idCollection.String())
gotCollection, found = owner.GetIDCollection(denom2)
require.True(t, found)
require.Equal(t, gotCollection.String(), idCollection2.String())
}
func TestOwnerUpdateIDCollectionMethod(t *testing.T) {
ids := []string{id}
idCollection := NewIDCollection(denom, ids)
owner := NewOwner(address, idCollection)
require.Equal(t, owner.Supply(), 1)
ids2 := []string{id, id2}
idCollection2 := NewIDCollection(denom2, ids2)
// UpdateIDCollection should fail if denom doesn't exist
returnedOwner, err := owner.UpdateIDCollection(idCollection2)
require.Error(t, err)
idCollection3 := NewIDCollection(denom, ids2)
returnedOwner, err = owner.UpdateIDCollection(idCollection3)
require.NoError(t, err)
require.Equal(t, returnedOwner.Supply(), 2)
owner = returnedOwner
returnedCollection, _ := owner.GetIDCollection(denom)
require.Equal(t, len(returnedCollection.IDs), 2)
owner = NewOwner(address, idCollection, idCollection2)
require.Equal(t, owner.Supply(), 3)
returnedOwner, err = owner.UpdateIDCollection(idCollection3)
require.NoError(t, err)
require.Equal(t, returnedOwner.Supply(), 4)
}
func TestOwnerDeleteIDMethod(t *testing.T) {
ids := []string{id, id2}
idCollection := NewIDCollection(denom, ids)
owner := NewOwner(address, idCollection)
returnedOwner, err := owner.DeleteID(denom2, id)
require.Error(t, err)
require.Equal(t, owner.String(), returnedOwner.String())
returnedOwner, err = owner.DeleteID(denom, id3)
require.Error(t, err)
require.Equal(t, owner.String(), returnedOwner.String())
owner, err = owner.DeleteID(denom, id)
require.NoError(t, err)
returnedCollection, _ := owner.GetIDCollection(denom)
require.Equal(t, len(returnedCollection.IDs), 1)
}

View File

@ -0,0 +1,55 @@
package types
// DONTCOVER
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// QueryCollectionParams defines the params for queries:
// - 'custom/nft/supply'
// - 'custom/nft/collection'
type QueryCollectionParams struct {
Denom string
}
// NewQueryCollectionParams creates a new instance of QuerySupplyParams
func NewQueryCollectionParams(denom string) QueryCollectionParams {
return QueryCollectionParams{Denom: denom}
}
// Bytes exports the Denom as bytes
func (q QueryCollectionParams) Bytes() []byte {
return []byte(q.Denom)
}
// QueryBalanceParams params for query 'custom/nfts/balance'
type QueryBalanceParams struct {
Owner sdk.AccAddress
Denom string // optional
}
// NewQueryBalanceParams creates a new instance of QuerySupplyParams
func NewQueryBalanceParams(owner sdk.AccAddress, denom ...string) QueryBalanceParams {
if len(denom) > 0 {
return QueryBalanceParams{
Owner: owner,
Denom: denom[0],
}
}
return QueryBalanceParams{Owner: owner}
}
// QueryNFTParams params for query 'custom/nfts/nft'
type QueryNFTParams struct {
Denom string
TokenID string
}
// NewQueryNFTParams creates a new instance of QueryNFTParams
func NewQueryNFTParams(denom, id string) QueryNFTParams {
return QueryNFTParams{
Denom: denom,
TokenID: id,
}
}

View File

@ -0,0 +1,65 @@
package types
import (
"bytes"
"strconv"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// nolint: deadcode unused
var (
denom = "denom"
denom2 = "test-denom2"
denom3 = "test-denom3"
id = "1"
id2 = "2"
id3 = "3"
address = CreateTestAddrs(1)[0]
address2 = CreateTestAddrs(2)[1]
address3 = CreateTestAddrs(3)[2]
tokenURI = "https://google.com/token-1.json"
tokenURI2 = "https://google.com/token-2.json"
)
// CreateTestAddrs creates test addresses
func CreateTestAddrs(numAddrs int) []sdk.AccAddress {
var addresses []sdk.AccAddress
var buffer bytes.Buffer
// start at 100 so we can make up to 999 test addresses with valid test addresses
for i := 100; i < (numAddrs + 100); i++ {
numString := strconv.Itoa(i)
buffer.WriteString("A58856F0FD53BF058B4909A21AEC019107BA6") //base address string
buffer.WriteString(numString) //adding on final two digits to make addresses unique
res, _ := sdk.AccAddressFromHex(buffer.String())
bech := res.String()
addresses = append(addresses, testAddr(buffer.String(), bech))
buffer.Reset()
}
return addresses
}
// for incode address generation
func testAddr(addr string, bech string) sdk.AccAddress {
res, err := sdk.AccAddressFromHex(addr)
if err != nil {
panic(err)
}
bechexpected := res.String()
if bech != bechexpected {
panic("Bech encoding doesn't match reference")
}
bechres, err := sdk.AccAddressFromBech32(bech)
if err != nil {
panic(err)
}
if !bytes.Equal(bechres, res) {
panic("Bech decode and hex decode don't match")
}
return res
}

154
x/nft/module.go Normal file
View File

@ -0,0 +1,154 @@
package nft
// DONTCOVER
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/nft/client/cli"
"github.com/cosmos/cosmos-sdk/x/nft/client/rest"
"github.com/cosmos/cosmos-sdk/x/nft/simulation"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModuleSimulation{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
var _ module.AppModuleBasic = AppModuleBasic{}
// Name defines module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec registers module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data)
if err != nil {
return err
}
return ValidateGenesis(data)
}
// RegisterRESTRoutes registers rest routes
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr, ModuleCdc, RouterKey)
}
// GetTxCmd gets the root tx command of this module
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(StoreKey, cdc)
}
// GetQueryCmd gets the root query command of this module
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
//____________________________________________________________________________
// AppModuleSimulation defines the module simulation functions used by the gov module.
type AppModuleSimulation struct{}
// RegisterStoreDecoder performs a no-op.
func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[StoreKey] = simulation.DecodeStore
}
//____________________________________________________________________________
// AppModule supply app module
type AppModule struct {
AppModuleBasic
AppModuleSimulation
keeper Keeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
AppModuleSimulation: AppModuleSimulation{},
keeper: keeper,
}
}
// Name defines module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants registers the nft module invariants
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
RegisterInvariants(ir, am.keeper)
}
// Route module message route name
func (AppModule) Route() string {
return RouterKey
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return GenericHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return QuerierRoute
}
// NewQuerierHandler module querier
func (am AppModule) NewQuerierHandler() sdk.Querier {
return NewQuerier(am.keeper)
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return EndBlocker(ctx, am.keeper)
}

View File

@ -0,0 +1,31 @@
package simulation
import (
"bytes"
"fmt"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding gov type
func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string {
switch {
case bytes.Equal(kvA.Key[:1], types.CollectionsKeyPrefix):
var collectionA, collectionB types.Collection
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &collectionA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &collectionB)
return fmt.Sprintf("%v\n%v", collectionA, collectionB)
case bytes.Equal(kvA.Key[:1], types.OwnersKeyPrefix):
var idCollectionA, idCollectionB types.IDCollection
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &idCollectionA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &idCollectionB)
return fmt.Sprintf("%v\n%v", idCollectionA, idCollectionB)
default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
}
}

View File

@ -0,0 +1,60 @@
package simulation
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/ed25519"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
)
var (
delPk1 = ed25519.GenPrivKey().PubKey()
addr = sdk.AccAddress(delPk1.Address())
)
func makeTestCodec() (cdc *codec.Codec) {
cdc = codec.New()
sdk.RegisterCodec(cdc)
types.RegisterCodec(cdc)
return
}
func TestDecodeStore(t *testing.T) {
cdc := makeTestCodec()
nft := types.NewBaseNFT("1", addr, "token URI")
collection := types.NewCollection("kitties", types.NFTs{&nft})
idCollection := types.NewIDCollection("kitties", []string{"1", "2", "3"})
kvPairs := cmn.KVPairs{
cmn.KVPair{Key: types.GetCollectionKey("kitties"), Value: cdc.MustMarshalBinaryLengthPrefixed(collection)},
cmn.KVPair{Key: types.GetOwnerKey(addr, "kitties"), Value: cdc.MustMarshalBinaryLengthPrefixed(idCollection)},
cmn.KVPair{Key: []byte{0x99}, Value: []byte{0x99}},
}
tests := []struct {
name string
expectedLog string
}{
{"collections", fmt.Sprintf("%v\n%v", collection, collection)},
{"owners", fmt.Sprintf("%v\n%v", idCollection, idCollection)},
{"other", ""},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
switch i {
case len(tests) - 1:
require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
default:
require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
}
})
}
}

View File

@ -0,0 +1,55 @@
package simulation
import (
"encoding/json"
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
// GenNFTGenesisState generates a random GenesisState for nft
func GenNFTGenesisState(cdc *codec.Codec, r *rand.Rand, accs []simulation.Account, ap simulation.AppParams, genesisState map[string]json.RawMessage) {
const (
Kitties = "crypto-kitties"
Doggos = "crypto-doggos"
)
collections := types.NewCollections(types.NewCollection(Kitties, types.NFTs{}), types.NewCollection(Doggos, types.NFTs{}))
var ownerships []types.Owner
for _, acc := range accs {
if r.Intn(100) < 50 {
baseNFT := types.NewBaseNFT(
simulation.RandStringOfLength(r, 10), // id
acc.Address,
simulation.RandStringOfLength(r, 45), // tokenURI
)
var idCollection types.IDCollection
var err error
if r.Intn(100) < 50 {
collections[0], err = collections[0].AddNFT(&baseNFT)
if err != nil {
panic(err)
}
idCollection = types.NewIDCollection(Kitties, []string{baseNFT.ID})
} else {
collections[1], err = collections[1].AddNFT(&baseNFT)
if err != nil {
panic(err)
}
idCollection = types.NewIDCollection(Doggos, []string{baseNFT.ID})
}
ownership := types.NewOwner(acc.Address, idCollection)
ownerships = append(ownerships, ownership)
}
}
nftGenesis := types.NewGenesisState(ownerships, collections)
fmt.Printf("Selected randomly generated NFT parameters:\n%s\n", codec.MustMarshalJSONIndent(cdc, nftGenesis))
genesisState[types.ModuleName] = cdc.MustMarshalJSON(nftGenesis)
}

View File

@ -0,0 +1,138 @@
package operations
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft"
"github.com/cosmos/cosmos-sdk/x/nft/internal/keeper"
"github.com/cosmos/cosmos-sdk/x/nft/internal/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
// DONTCOVER
// SimulateMsgTransferNFT simulates the transfer of an NFT
func SimulateMsgTransferNFT(k keeper.Keeper) simulation.Operation {
handler := nft.GenericHandler(k)
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simulation.Account) (opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) {
ownerAddr, denom, nftID := getRandomNFTFromOwner(ctx, k, r)
if ownerAddr.Empty() {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
msg := types.NewMsgTransferNFT(
ownerAddr, // sender
simulation.RandomAcc(r, accs).Address, // recipient
denom,
nftID,
)
if msg.ValidateBasic() != nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
}
ctx, write := ctx.CacheContext()
ok := handler(ctx, msg).IsOK()
if ok {
write()
}
opMsg = simulation.NewOperationMsg(msg, ok, "")
return opMsg, nil, nil
}
}
// SimulateMsgEditNFTMetadata simulates an edit metadata transaction
func SimulateMsgEditNFTMetadata(k keeper.Keeper) simulation.Operation {
handler := nft.GenericHandler(k)
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simulation.Account) (opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) {
ownerAddr, denom, nftID := getRandomNFTFromOwner(ctx, k, r)
if ownerAddr.Empty() {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
msg := types.NewMsgEditNFTMetadata(
ownerAddr,
nftID,
denom,
simulation.RandStringOfLength(r, 45), // tokenURI
)
if msg.ValidateBasic() != nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
}
ctx, write := ctx.CacheContext()
ok := handler(ctx, msg).IsOK()
if ok {
write()
}
opMsg = simulation.NewOperationMsg(msg, ok, "")
return opMsg, nil, nil
}
}
// SimulateMsgMintNFT simulates a mint of an NFT
func SimulateMsgMintNFT(k keeper.Keeper) simulation.Operation {
handler := nft.GenericHandler(k)
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simulation.Account) (opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) {
msg := types.NewMsgMintNFT(
simulation.RandomAcc(r, accs).Address, // sender
simulation.RandomAcc(r, accs).Address, // recipient
simulation.RandStringOfLength(r, 10), // nft ID
simulation.RandStringOfLength(r, 10), // denom
simulation.RandStringOfLength(r, 45), // tokenURI
)
if msg.ValidateBasic() != nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
}
ctx, write := ctx.CacheContext()
ok := handler(ctx, msg).IsOK()
if ok {
write()
}
opMsg = simulation.NewOperationMsg(msg, ok, "")
return opMsg, nil, nil
}
}
// SimulateMsgBurnNFT simulates a burn of an existing NFT
func SimulateMsgBurnNFT(k keeper.Keeper) simulation.Operation {
handler := nft.GenericHandler(k)
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accs []simulation.Account) (opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) {
ownerAddr, denom, nftID := getRandomNFTFromOwner(ctx, k, r)
if ownerAddr.Empty() {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
msg := types.NewMsgBurnNFT(ownerAddr, nftID, denom)
if msg.ValidateBasic() != nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
}
ctx, write := ctx.CacheContext()
ok := handler(ctx, msg).IsOK()
if ok {
write()
}
opMsg = simulation.NewOperationMsg(msg, ok, "")
return opMsg, nil, nil
}
}

View File

@ -0,0 +1,42 @@
package operations
import (
"math/rand"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/nft/internal/keeper"
)
func getRandomNFTFromOwner(ctx sdk.Context, k keeper.Keeper, r *rand.Rand) (address sdk.AccAddress, denom, nftID string) {
owners := k.GetOwners(ctx)
ownersLen := len(owners)
if ownersLen == 0 {
return nil, "", ""
}
// get random owner
i := r.Intn(ownersLen)
owner := owners[i]
idCollectionsLen := len(owner.IDCollections)
if idCollectionsLen == 0 {
return nil, "", ""
}
// get random collection from owner's balance
i = r.Intn(idCollectionsLen)
idsCollection := owner.IDCollections[i] // nfts IDs
denom = idsCollection.Denom
idsLen := len(idsCollection.IDs)
if idsLen == 0 {
return nil, "", ""
}
// get random nft from collection
i = r.Intn(idsLen)
nftID = idsCollection.IDs[i]
return owner.Address, denom, nftID
}