NFT
Module name and store key: nftrwa. Consensus version 1.
x/nft is an overlay on the stock cosmossdk.io/x/nft keeper, which is
vendored under third_party/nft in the chain repository. The class and token
records live in the upstream module's store; this module keeps a parallel
metadata store and adds the rules the RWA use case needs: an issuer-scoped class
id, a technical issuer per class, sequential token ids, immutable metadata, and a
lock that other modules use to escrow a token.
Class ids are self-scoping
{issuer}/{product_ref}
The first segment must be the signer's own bech32 address, so no account can
create a class inside another account's namespace. Both segments are printable
ASCII, the separator appears exactly once, and neither side may contain a /.
| Field | Rule |
|---|---|
| Class id | Printable ASCII, at most 256 bytes, exactly one /, both sides non-empty. |
product_ref | 1 to 128 printable ASCII bytes, no /. |
name | 1 to 128 characters. |
symbol | At most 32 characters. |
description, uri | At most 512 characters each. |
uri_hash | At most 128 characters. |
Class metadata
ClassMeta {
class_id // the full {issuer}/{product_ref} string
issuer // the technical issuer for this class
restricted // transfers require a known counterparty
token_metadata_immutable // set once, at creation, never changed
}
issuer is the class's technical issuer: it can mint tokens, burn tokens it
itself owns, set metadata, and answer the nftmarket class-configuration
message. Only governance can move it, and only through MsgChangeClassIssuer.
restricted is read by tokenfactory's NFT send check
rather than by this module. Under it, a transfer to an address that holds no
token of that class is rejected unless the recipient is the class issuer or the
module authority.
Token ids
Token ids are assigned by the module, never chosen by the sender:
{class_id}/{sequence}
The sequence is a per-class counter that starts at 1 and only ever increases.
The stored class id and the class id embedded in the token id always agree, and
nftmarket re-checks that agreement when it validates a listing.
Messages
| Message | Signer | Requirement |
|---|---|---|
MsgCreateClass | sender | Nothing. The sender becomes the issuer. Charges class_creation_fee and consumes the class-creation gas. |
MsgMintNFT | sender | The class issuer. |
MsgBurnNFT | sender | The token's own owner. Since v1.1 neither the class issuer nor the module authority can burn a token that belongs to another holder — they are rejected with ErrHolderAssetProtection. |
MsgSetNFTMetadata | sender | The class issuer, and the class must not be token_metadata_immutable. |
MsgChangeClassIssuer | authority | The module authority, which is the gov account by default. |
There is no transfer message in this module. NFT transfers go through the
upstream nft module's MsgSend, and this module decides whether they are
allowed. Since v1.2, a holder can also transfer with a
single-transaction MsgClaimSendNFT permit submitted by an executor; see
Claim permits.
Metadata immutability is set once
token_metadata_immutable is fixed when the class is created and no message
changes it. When it is set, MsgSetNFTMetadata fails with
ErrImmutableTokenMetadata — but only after the sender has been confirmed as the
class issuer, so an unauthorized caller still gets ErrUnauthorized rather than
learning the policy.
:::note MsgSetNFTMetadata emits no event
The metadata update is applied and the message succeeds, but nothing is written
to the event stream. A consumer that mirrors token state must re-query rather
than wait for an event.
:::
The lock
A lock escrows a token without transferring it. The holder stays the owner; the token simply cannot be moved or burned by anyone else.
NFTLock { class_id, token_id, order_id, holder }
- One lock per token. Locking a locked token returns
ErrNFTLocked. UnlockNFTis a no-op when no lock exists, and returnsErrLockOrderMismatchif the order id does not match. That lets each consumer release only its own lock.- Burning clears the lock first, so the owner's own burn always succeeds over a lock. Since v1.1 only the owner may burn, so a burn can no longer be driven by the issuer over the holder's lock.
Two modules used to take locks. Since v1.2 only
nftmarket does: it escrows a listed token under the order id
nftmarket/{listing_id}. A tokenfactory authorization no longer locks an NFT
input. TX1 performs a read-only check that the token is not already locked by the
market, and settlement fails closed if ownership or the lock changed. The claim
permit path (MsgClaimSendNFT) re-runs the same checks.
Burn hooks
The module keeps a named hook registry that runs on every burn, in sorted name order. The first error aborts the burn and rolls the transaction back.
| Hook | Registered by | Effect |
|---|---|---|
income | x/income | Releases the token's earmarked claimable back into the shared yield pool. |
nftmarket | x/nftmarket | Refunds the seller's listing deposit and deletes the listing. |
Because the registry is sorted, income runs before nftmarket.
Queries
| Query | Returns |
|---|---|
Params | The class creation fee and gas. |
ClassIssuer | The ClassMeta for a class. |
NFTLock | Whether a token is locked, and by which order. |
ClassesByIssuer | Paginated classes for an address. |
TokensByClass | Paginated tokens in a class. |
TokensByOwner | Paginated tokens held by an address. |
ClassDetail | The class metadata, total supply, and a page of tokens. |
ClassDetail also returns holder_count, which is deprecated and always zero.
Every query is also served over the gRPC gateway under
/centerchain/nft/v1/.... A class id can contain /, so the gateway maps these
queries to a fixed path and binds the identifiers as query parameters instead of
path segments.
Events
| Event | Attributes |
|---|---|
class_created | class_id, technical_issuer, uri_hash, token_metadata_immutable, fee |
nft_minted | class_id, token_id, owner, uri_hash |
nft_burned | class_id, token_id, owner |
nft_locked | class_id, token_id, order_id, holder |
nft_unlocked | class_id, token_id |
class_issuer_changed | class_id, old_issuer, new_issuer |
Errors
| Code | Identifier | Raised when |
|---|---|---|
| 1200 | ErrInvalidSigner | A governance message was not signed by the authority. |
| 1201 | ErrUnauthorized | The sender is not the class issuer. |
| 1202 | ErrInvalidClassID | The class id is malformed or not self-scoping. |
| 1203 | ErrClassExists | The class already exists. |
| 1204 | ErrClassNotFound | No such class. |
| 1205 | ErrInvalidTokenID | The token id is malformed or belongs to another class. |
| 1206 | ErrNFTNotFound | No such token. |
| 1207 | ErrNFTLocked | The token is locked. |
| 1210 | ErrUnknownHolder | A restricted transfer targeted a non-holder. |
| 1211 | ErrInvalidAuthority | A governance message was signed by the wrong account. |
| 1212 | ErrLockOrderMismatch | The unlock used a different order id. |
| 1213 | ErrImmutableTokenMetadata | The class forbids metadata changes. |
| 1214 | ErrHolderAssetProtection | A burn targeted a token the signer does not own. Added in v1.1. |
CLI
x/nftrwa exposes its queries and messages through autocli under the nftrwa
prefix. The underlying cosmossdk.io/x/nft module keeps its own CLI surface
under the separate nft prefix.
centerchaind query nftrwa params
centerchaind query nftrwa class-issuer [class-id]
centerchaind query nftrwa nft-lock [class-id] [token-id]
centerchaind query nftrwa classes-by-issuer [issuer]
centerchaind query nftrwa tokens-by-class [class-id]
centerchaind query nftrwa tokens-by-owner [owner]
centerchaind query nftrwa class-detail [class-id]
centerchaind tx nftrwa create-class [class-id] [name] [symbol]
centerchaind tx nftrwa mint [class-id] [to]
centerchaind tx nftrwa burn [class-id] [token-id]
centerchaind tx nftrwa set-nft-metadata [class-id] [token-id]
centerchaind tx nftrwa change-class-issuer [class-id] [new-issuer]