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 and burn tokens, 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 class issuer or the module authority. The holder cannot burn. |
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.
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 a burn always succeeds over a lock.
Two modules take locks: nftmarket escrows a listed token under the order id
nftmarket/{listing_id}, and the tokenfactory swap subsystem escrows an NFT input
under the swap's own order id.
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.
This module registers no gRPC-gateway routes, so it is gRPC only.
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. |
CLI
The module defines no autocli commands of its own. The class lifecycle is
reachable over gRPC, and the underlying cosmossdk.io/x/nft module keeps its
own CLI surface under the nft prefix.