LIP: 0052
Title: Introduce NFT module
Author: Maxime Gagnebin <[email protected]>
Miroslav Jerkovic <[email protected]>
Discussions-To: https://research.lisk.com/t/introduce-nft-module/297
Status: Active (Lisk SDK only)
Type: Standards Track
Created: 2021-05-22
Updated: 2024-01-04
Requires: 0045
The NFT (non-fungible token) module is used in the Lisk ecosystem for creating, destroying NFTs, and transferring them in the ecosystem. NFTs are uniquely identified assets. They can be transferred similarly to fungible tokens, but their unique identifiers can never be modified. In this module, NFTs also carry attributesArray
that are used to store information specific to the NFT.
In this LIP, we specify the properties of the NFT module, along with their serialization and default values. Furthermore, we specify the state transitions logic defined within this module, i.e. the commands, the protocol logic injected during the block lifecycle, and the functions that can be called from other modules or off-chain services.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
NFTs are very common in the blockchain space and have uses in a wide range of applications. This can go from being the virtual representation of a real world object (art, fashion, event tickets ...) to purely virtual collectibles (crypto kitties, ...).
Therefore, providing a unified module to handle, transfer and modify NFTs is a necessity for the Lisk ecosystem. The module presented here contains all the basic features that are needed to incorporate NFTs in a blockchain ecosystem without being restrictive on the way NFTs will be used by custom modules and applications.
- Native chain: with regards to an NFT, this is the chain where the NFT was created.
- Native NFT: with regards to a chain, all NFTs created on this chain.
- Foreign chain: with regards to an NFT, all chains other than the native chain.
Figure 1: The NFT module store is divided into four substores. All NFTs held by users are stored sequentially in the user substore with keys given by the user address and the NFT ID.
The NFT store contains entries for all NFTs present on the native chain, as well as entries for all native NFTs that have been sent cross-chain to a foreign chain. Each entry contains two properties:
- The
owner
property can either be aLENGTH_ADDRESS
bytes long user address or aLENGTH_CHAIN_ID
bytes long chain ID. In the latter case, the NFT is a native NFT that has been sent cross-chain to a foreign chain and is escrowed. - The
attributesArray
property can be used by custom applications to store information about the NFT, or modify interactions with the NFT.
In the proposed solution, all NFTs associated with a given address are stored sequentially in the user substore part of the state. In this way, getting all NFTs of a given account can be done efficiently. This is in contrast to specifications (like ERC 721 without optional extensions) where the NFT owner is only stored as one of the NFTs properties. We think that this feature is useful in an account-based blockchain ecosystem and the user substore is designed accordingly.
The lockingModule
property stores the information regarding the locking status of the NFT. If the NFT is unlocked, this property will have the value NFT_NOT_LOCKED
, whereas if the NFT is locked, this property will store the locking module name.
To identify NFTs in the Lisk ecosystem, we introduce the nftID
, a unique NFT identifier in the ecosystem. It is a LENGTH_NFT_ID
bytes long concatenation of the LENGTH_CHAIN_ID
bytes long chainID
, the chain ID of the chain creating the NFT, the LENGTH_COLLECTION_ID
bytes long collectionID
, chosen when the NFT is created, and a 8 bytes long serialization of an index
integer, automatically assigned at the NFT creation.
This allows chains to define multiple sets of NFTs, each identified by their respective collection. Each collection can then easily have its own attributes schema and custom logic. For example, an art NFT exchange could have a different collection per artist, index
being then a unique integer associated with each art piece of this artist.
To allow cross-chain transfers of NFTs, we define a specific command which makes use of the Interoperability module and creates a cross-chain message with the relevant information. When sending NFTs cross-chain, it is crucial that every native chain can correctly escrow its native NFTs sent to a foreign chain. In this way, a native NFT can never be created by a foreign chain and sent across the ecosystem. When receiving non-native NFTs on a chain, users can query this NFT's native chain to make sure that the NFT is properly escrowed.
These specifications only allow NFTs to be transferred from or to their native chain. In particular, this means that NFT created on chain A cannot be transferred directly from chain B to chain C. This is required to allow the native chain to maintain escrowed NFTs correctly.
Each NFT is stored with an array of attributes specified by various modules, with each attribute
property being a byte sequence that is not deserialized by the NFT module. Each custom module using NFTs should define schemas to serialize and deserialize their attributes
property of NFTs.
When an NFT is sent to another chain, the attributes
properties of the NFT can be modified according to specifications set on the receiving chain. When the NFT is received back on its native chain, the returned modified attributes are disregarded and the original attributes are restored, as currently defined by getNewAttributes function. If needed, custom modules can implement a more fine-grained approach towards the attributes that are modified cross-chain.
Note that the attributes
properties are not limited in size by default, which can potentially cause the CCM validateFormat failure during the cross-chain NFT transfer.
The NFT module provides the following functions to modify the NFT state. Any other modules should use those functions to modify the NFT state. The NFT state should never be modified from outside the module without using one of the provided functions as this could result in unexpected behavior and could cause an improper state transition.
This function is used to create a new NFT. The NFT will always be native to the chain creating it.
This function is used to destroy NFTs. The NFT will be removed from the NFT substore and cannot be retrieved, except in the case of destroying NFT on a foreign chain: the information about the NFT (e.g., the attributes) will still be available in the corresponding escrow entry of the NFT substore in the native chain.
This function is used to lock an NFT to a module. A locked NFT cannot be transferred (within the chain or across chains). This can be useful, for example, when the NFT is used as a deposit for a service. Module is specified both when locking and unlocking the NFT, thus preventing NFTs being accidentally locked and unlocked by different modules.
This function is used to unlock an NFT that was locked to a module.
This function is used to modify the attributes of NFTs. Each custom module can define the rules surrounding modifying NFT attributes and should call this function. This function will be executed even if the NFT is locked.
This function is used to transfer ownership of NFTs within one chain.
This function is used to transfer ownership of NFTs across chains in the Lisk ecosystem.
This function should only be called by the Interoperability module to trigger the recovery of NFTs escrowed to terminated chains.
The following constants are used throughout the document:
Name | Type | Value |
---|---|---|
Interoperability Constants | ||
CCM_STATUS_CODE_OK |
uint32 | 0 |
MAX_RESERVED_ERROR_STATUS |
uint64 | 63 |
NFT Module Constants | ||
MODULE_NAME_NFT |
string | "nft" |
COMMAND_NAME_TRANSFER |
string | "transfer" |
COMMAND_NAME_CROSS_CHAIN_TRANSFER |
string | "transferCrossChain" |
CROSS_CHAIN_COMMAND_NAME_TRANSFER |
string | TBD |
CCM_STATUS_NFT_NOT_SUPPORTED |
uint32 | 64 |
CCM_STATUS_PROTOCOL_VIOLATION |
uint32 | 65 |
NFT_NOT_LOCKED |
string | MODULE_NAME_NFT |
ALL_SUPPORTED_NFTS_KEY |
bytes | EMPTY_BYTES |
NFT Store Constants | ||
SUBSTORE_PREFIX_NFT |
bytes | 0x0000 |
SUBSTORE_PREFIX_USER |
bytes | 0x8000 |
SUBSTORE_PREFIX_ESCROW |
bytes | 0x4000 |
SUBSTORE_PREFIX_SUPPORTED_NFTS |
bytes | 0xc000 |
Configurable Constants | Mainchain Value | |
FEE_CREATE_NFT |
uint64 | 5000000 |
General Constants | ||
OWN_CHAIN_ID |
bytes | chainID of the chain. |
LENGTH_ADDRESS |
uint32 | 20 |
MIN_LENGTH_MODULE_NAME |
uint32 | 1 |
MAX_LENGTH_MODULE_NAME |
uint32 | 32 |
LENGTH_NFT_ID |
uint32 | 16 |
LENGTH_CHAIN_ID |
uint32 | 4 |
LENGTH_COLLECTION_ID |
uint32 | 4 |
MAX_LENGTH_DATA |
uint32 | 64 |
EMPTY_BYTES |
bytes | "" |
Name | Type | Value | Description |
---|---|---|---|
Names | |||
EVENT_NAME_TRANSFER |
string | "transfer" | Name of the events emitted during NFT transfer. |
EVENT_NAME_TRANSFER_CROSS_CHAIN |
string | "transferCrossChain" | Name of the events emitted during cross-chain NFT transfer. |
EVENT_NAME_CCM_TRANSFER |
string | "ccmTransfer" | Name of the events emitted during execution of cross-chain NFT transfer messages. |
EVENT_NAME_CREATE |
string | "create" | Name of the events emitted during calls to the create function. |
EVENT_NAME_DESTROY |
string | "destroy" | Name of the events emitted during calls to the destroy function. |
EVENT_NAME_LOCK |
string | "lock" | Name of the events emitted during calls to the lock function. |
EVENT_NAME_UNLOCK |
string | "unlock" | Name of the events emitted during calls to the unlock function. |
EVENT_NAME_SET_ATTRIBUTES |
string | "setAttributes" | Name of the events emitted during calls to the setAttributes function. |
EVENT_NAME_RECOVER |
string | "recover" | Name of the events emitted during calls to the recover function. |
EVENT_NAME_SUPPORT_ALL_NFTS |
string | "supportAllNFTs" | Name of the event emitted during calls to the supportAllNFTs function |
EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS |
string | "removeSupportAllNFTs" | Name of the event emitted during calls to the removeSupportAllNFTs function |
EVENT_NAME_SUPPORT_ALL_NFTS_FROM_CHAIN |
string | "supportAllNFTsFromChain" | Name of the event emitted during calls to the supportAllNFTsFromChain function |
EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS_FROM_CHAIN |
string | "removeSupportAllNFTsFromChain" | Name of the event emitted during calls to the removeSupportAllNFTsFromChain function |
EVENT_NAME_SUPPORT_ALL_NFTS_FROM_COLLECTION |
string | "supportAllNFTsFromCollection" | Name of the event emitted during calls to the supportAllNFTsFromCollection function |
EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS_FROM_COLLECTION |
string | "removeSupportAllNFTsFromCollection" | Name of the event emitted during calls to the removeSupportAllNFTsFromCollection function |
Result codes | |||
RESULT_SUCCESSFUL |
uint32 | 0 | Successful result code for events. |
RESULT_NFT_DOES_NOT_EXIST |
uint32 | 1 | Used when NFT substore entry does not exist. |
RESULT_NFT_NOT_NATIVE |
uint32 | 2 | Used when NFT is not native to either the sending chain or the receiving chain. |
RESULT_NFT_NOT_SUPPORTED |
uint32 | 3 | Used when NFT is not supported in the receiving chain. |
RESULT_NFT_LOCKED |
uint32 | 4 | Used when destroy, lock or transfer functions fail due to NFT being locked. |
RESULT_NFT_NOT_LOCKED |
uint32 | 5 | Used when unlock function fails due to NFT being unlocked. |
RESULT_UNAUTHORIZED_UNLOCK |
uint32 | 6 | Used when NFT unlocking fails due to being requested by a module that did not lock it. |
RESULT_NFT_ESCROWED |
uint32 | 7 | Used when destroy, lock or transfer functions fail due to NFT being escrowed. |
RESULT_NFT_NOT_ESCROWED |
uint32 | 8 | Used when recover function fails due to NFT not being escrowed. |
RESULT_INITIATED_BY_NONNATIVE_CHAIN |
uint32 | 9 | Used when recover function fails due to not being initiated by the native chain. |
RESULT_INITIATED_BY_NONOWNER |
uint32 | 10 | Used when destroy or transfer functions fail due to not being initiated by the NFT owner. |
RESULT_RECOVER_FAIL_INVALID_INPUTS |
uint32 | 11 | Used when the recover function fails due to invalid inputs. |
RESULT_INSUFFICIENT_BALANCE |
uint32 | 12 | Used when the balance is not sufficient to pay for the cross-chain message fee. |
RESULT_DATA_TOO_LONG |
uint32 | 13 | Used when the data input is too long. |
INVALID_RECEIVING_CHAIN |
uint32 | 14 | Used when, during the cross-chain token transfer, the receiving chain is set to be equal to the sending chain. |
RESULT_INVALID_ACCOUNT |
uint32 | 15 | Used when recover function fails due to invalid account. |
Name | Type | Validation | Description |
---|---|---|---|
Address |
bytes | Must be of length LENGTH_ADDRESS . |
Address of an account. |
Module |
string | Must be of length at least MIN_LENGTH_MODULE_NAME and at most MAX_LENGTH_MODULE_NAME . |
Used for identifying modules. |
NFTID |
bytes | Must be of length LENGTH_NFT_ID . |
Used for NFT identifiers. |
NFT |
dict | Dictionary obtained by merging the NFT substore value for key nftID , deserialized using NFTStoreSchema , and the corresponding user substore value for key address + nftID , deserialized using userStoreSchema . |
NFT and user substores information for a provided nftID . |
ChainID |
bytes | Must be of length LENGTH_CHAIN_ID . |
Used for chain identifiers. |
CollectionID |
bytes | Must be of length LENGTH_COLLECTION_ID . |
Used for NFT collection identifiers. |
AttributesArray |
(Module | bytes)[] | Two-dimensional array consisting of Module names and corresponding attributes . |
Used to store information specific to the NFT. |
The function uint64be(x)
returns the big endian uint64 serialization of an integer x
, with 0 <= x < 2^64
. This serialization is always 8 bytes long.
Calling a function fct
from the Interoperability module is represented by Interoperability.fct(required inputs)
, from the Fee module by Fee.fct(required inputs)
, and from the Token module by Token.fct(required inputs)
.
All NFTs in the ecosystem are identified by using the following three values:
chainID
, always the chain ID of the chain that created the NFT,collectionID
, aLENGTH_COLLECTION_ID
bytes long array, specified at NFT creation,index
, assigned at NFT creation to the next available index in the collection.
In this LIP, the NFT identifier nftID
is a LENGTH_NFT_ID
bytes long concatenation of the chainID
of the NFT native chain, collectionID
and the serialization of index
: nftID = chainID + collectionID + uint64be(index)
. This is for example used in all input formats for the module's exposed logics. This allows the exposed logic interfaces to be simple and uniform.
The NFT module contains a function used when receiving cross-chain NFT transfers to assert the support for non-native NFTs. It should return a boolean, depending on the configuration of the NFT module. For the rest of this LIP, this function is written isNFTSupported(nftID)
.
The store keys and schemas for value serialization of the NFT module store are set as follows:
- The substore prefix is set to
SUBSTORE_PREFIX_NFT
. - Each store key is an NFT ID:
nftID
. - Each store value is the serialization of an object following
NFTStoreSchema
presented below. - Notation: For the rest of this proposal, let
NFTStore[nftID]
be the object value stored in the NFT substore with store keynftID
, deserialized usingNFTStoreSchema
.
NFTStoreSchema = {
"type": "object",
"required": [
"owner",
"attributesArray"
],
"properties": {
"owner": {
"dataType": "bytes",
"fieldNumber": 1
},
"attributesArray": {
"type": "array",
"fieldNumber": 2,
"items": {
"type": "object",
"required": [
"module",
"attributes"
],
"properties": {
"module": {
"dataType": "string",
"minLength": MIN_LENGTH_MODULE_NAME,
"maxLength": MAX_LENGTH_MODULE_NAME,
"pattern": "^[a-zA-Z0-9]*$",
"fieldNumber": 1
},
"attributes": {
"dataType": "bytes",
"fieldNumber": 2
}
}
}
}
}
}
owner
: Either aLENGTH_ADDRESS
bytes long NFT owner address or aLENGTH_CHAIN_ID
bytes long chain ID in the case of a native NFT that has been escrowed.attributesArray
: An array containing attributes set by various modules. Elements include:module
: The name of the module that set the attributes.attributes
: The attributes set by the module.
Here, the attributesArray
array is lexicographically ordered by module
, which guarantees that serialization is consistent across nodes maintaining the chain.
If, for some module
, a state transition deletes the attributes
property , the corresponding entry in attributesArray
is removed.
If, for some module
, a state transition creates the attributes
property of a non-existent store entry, this entry is created following NFTStoreSchema
with the attributesArray
entry set accordingly.
- The substore prefix is set to
SUBSTORE_PREFIX_USER
. - Each store key is a concatenation of an address and a NFT ID:
address + nftID
. - Each store value is the serialization of an object following
userStoreSchema
presented below. - Notation: For the rest of this proposal, let
userStore[address, nftID]
be the object value stored in the user substore with store keyaddress + nftID
, deserialized usinguserStoreSchema
.
userStoreSchema = {
"type": "object",
"required": ["lockingModule"],
"properties": {
"lockingModule": {
"dataType": "string",
"minLength": MIN_LENGTH_MODULE_NAME,
"maxLength": MAX_LENGTH_MODULE_NAME,
"pattern": "^[a-zA-Z0-9]*$",
"fieldNumber": 1
}
}
}
lockingModule
: The name of the module that locked the NFT. The default value for thelockingModule
property isNFT_NOT_LOCKED
.
- The substore prefix is set to
SUBSTORE_PREFIX_ESCROW
. - Each store key is the identifier of the chain to which the NFTs are escrowed, and the NFT ID of the escrowed NFT:
escrowedChainID + nftID
. - Each store value follows the
escrowStoreSchema
schema presented below, which does not have any properties. - Notation: For the rest of this proposal, let
escrowStore[escrowedChainID, nftID]
be theEMPTY_BYTES
value stored in the escrow substore with store keyescrowedChainID + nftID
.
escrowStoreSchema = {
"type": "object",
"required": [],
"properties": {}
}
- The substore prefix is set to
SUBSTORE_PREFIX_SUPPORTED_NFTS
. - Each store key is the identifier of the chain to which the supported NFTs are native:
chainID
, orALL_SUPPORTED_NFTS_KEY
. - Each store value is the serialization of an object following
supportedNFTsStoreSchema
presented below. - Notation: For the rest of this proposal, let
supportedNFTsStore[chainID]
be the object value stored in the supported NFTs substore with store keychainID
, deserialized usingsupportedNFTsStoreSchema
.
supportedNFTsStoreSchema = {
"type": "object",
"required": ["supportedCollectionIDArray"],
"properties": {
"supportedCollectionIDArray" : {
"type": "array",
"fieldNumber": 1,
"items": {
"type": "object",
"required": ["collectionID"],
"properties": {
"collectionID": {
"dataType": "bytes",
"length": LENGTH_COLLECTION_ID,
"fieldNumber": 1
}
}
}
}
}
}
supportedCollectionIDArray
: The array ofcollectionID
, specifying all the supported NFT collections of the foreign chain.
If all NFTs are supported, the substore contains an entry for the key ALL_SUPPORTED_NFTS_KEY and no other entries.
If not all NFTs are supported, but all NFTs from a chain with chainID
are supported, the substore contains an entry for key chainID
with an empty array as value.
Since the native NFTs are always supported, no entry with key OWN_CHAIN_ID
is added to the substore.
For all entries in this substore, the entries of the supportedCollectionIDArray
are ordered lexicographically.
The module provides the following commands to modify the NFT store.
Transactions executing this command have:
module = MODULE_NAME_NFT
command = COMMAND_NAME_TRANSFER
The params
property of a NFT transfer transaction follows the schema NFTTransferParamsSchema
.
NFTTransferParamsSchema = {
"type": "object",
"required": [
"nftID",
"recipientAddress",
"data"
],
"properties": {
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 1
},
"recipientAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 2
},
"data": {
"dataType": "string",
"maxLength": MAX_LENGTH_DATA,
"fieldNumber": 3
}
}
}
def verify(trs: Transaction) -> None:
trsParams = decode(NFTTransferParamsSchema, trs.params)
senderAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]
recipientAddress = trsParams.recipientAddress
nftID = trsParams.nftID
verifyTransferInternal(senderAddress, recipientAddress, nftID, False)
def execute(trs: Transaction) -> None:
trsParams = decode(NFTTransferParamsSchema, trs.params)
senderAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]
recipientAddress = trsParams.recipientAddress
nftID = trsParams.nftID
transferInternal(senderAddress, recipientAddress, nftID)
The transferInternal function transfers the ownership of the NFT within the chain.
Transactions executing this command have:
module = MODULE_NAME_NFT
command = COMMAND_NAME_CROSS_CHAIN_TRANSFER
The params
property of a cross-chain NFT transfer transaction follows the crossChainNFTTransferParamsSchema
schema.
crossChainNFTTransferParamsSchema = {
"type": "object",
"required": [
"nftID",
"receivingChainID",
"recipientAddress",
"data",
"messageFee",
"includeAttributes"
],
"properties": {
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 1
},
"receivingChainID": {
"dataType": "bytes",
"length": LENGTH_CHAIN_ID,
"fieldNumber": 2
},
"recipientAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 3
},
"data": {
"dataType": "string",
"maxLength": MAX_LENGTH_DATA,
"fieldNumber": 4
},
"messageFee": {
"dataType": "uint64",
"fieldNumber": 5
},
"includeAttributes": {
"dataType": "boolean",
"fieldNumber": 6
}
}
}
def verify(trs: Transaction) -> None:
trsParams = decode(crossChainNFTTransferParamsSchema, trs.params)
senderAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]
nftID = trsParams.nftID
receivingChainID = trsParams.receivingChainID
recipientAddress = trsParams.recipientAddress
data = trsParams.data
messageFee = trsParams.messageFee
includeAttributes = trsParams.includeAttributes
verifyTransferCrossChainInternal(
senderAddress,
recipientAddress,
nftID,
receivingChainID,
messageFee,
data,
includeAttributes,
False
)
def execute(trs: Transaction) -> None:
trsParams = decode(crossChainNFTTransferParamsSchema, trs.params)
senderAddress = sha256(trs.senderPublicKey)[:LENGTH_ADDRESS]
nftID = trsParams.nftID
receivingChainID = trsParams.receivingChainID
recipientAddress = trsParams.recipientAddress
data = trsParams.data
messageFee = trsParams.messageFee
includeAttributes = trsParams.includeAttributes
transferCrossChainInternal(
senderAddress,
recipientAddress,
nftID,
receivingChainID,
messageFee,
data,
includeAttributes
)
The transferCrossChainInternal function transfers ownership of NFTs across chains in the Lisk ecosystem and calls the interoperability module in order to create a CCM.
Cross-chain messages executing this cross-chain command have:
module = MODULE_NAME_NFT
,crossChainCommand = CROSS_CHAIN_COMMAND_NAME_TRANSFER
The params
property of a cross-chain NFT transfer message follows the crossChainNFTTransferMessageParamsSchema
.
crossChainNFTTransferMessageParamsSchema = {
"type": "object",
"required": [
"nftID",
"senderAddress",
"recipientAddress",
"attributesArray",
"data"
],
"properties": {
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 1
},
"senderAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 2
},
"recipientAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 3
},
"attributesArray": {
"type": "array",
"fieldNumber": 4,
"items": {
"type": "object",
"required": ["module", "attributes"],
"properties": {
"module": {
"dataType": "string",
"minLength": MIN_LENGTH_MODULE_NAME,
"maxLength": MAX_LENGTH_MODULE_NAME,
"pattern": "^[a-zA-Z0-9]*$",
"fieldNumber": 1
},
"attributes": {
"dataType": "bytes",
"fieldNumber": 2
}
}
}
},
"data": {
"dataType": "string",
"maxLength": MAX_LENGTH_DATA,
"fieldNumber": 5
}
}
}
def verify(
trs: Transaction,
ccm: CCM
) -> None:
ccmParams = decode(crossChainNFTTransferMessageParamsSchema, ccm.params)
nftID = ccmParams.nftID
sendingChainID = ccm.sendingChainID
if ccm.status > MAX_RESERVED_ERROR_STATUS:
raise Exception("Invalid CCM error code")
if getChainID(nftID) not in [OWN_CHAIN_ID, sendingChainID]:
raise Exception("NFT is not native to either the sending chain or the receiving chain")
if getChainID(nftID) == OWN_CHAIN_ID and NFTStore[nftID] entry does not exist:
raise Exception("Non-existent entry in the NFT substore")
if getChainID(nftID) == OWN_CHAIN_ID and NFTStore[nftID].owner != ccm.sendingChainID:
raise Exception("NFT has not been properly escrowed")
if getChainID(nftID) != OWN_CHAIN_ID and (ccm.status == CCM_STATUS_CODE_MODULE_NOT_SUPPORTED or ccm.status == CCM_STATUS_CODE_CROSS_CHAIN_COMMAND_NOT_SUPPORTED):
raise Exception("Module or cross-chain command not supported")
if getChainID(nftID) != OWN_CHAIN_ID and NFTStore[nftID] entry exists:
raise Exception("NFT substore entry already exists")
When executing a cross-chain NFT transfer message ccm
, the logic below is followed.
def execute(
trs: Transaction,
ccm: CCM
) -> None:
ccmParams = decode(crossChainNFTTransferMessageParamsSchema, ccm.params)
nftID = ccmParams.nftID
senderAddress = ccmParams.senderAddress
recipientAddress = ccmParams.recipientAddress
receivedAttributes = ccmParams.attributesArray
data = ccmParams.data
receivingChainID = ccm.receivingChainID
sendingChainID = ccm.sendingChainID
if getChainID(nftID) == OWN_CHAIN_ID: # Execution on the native chain
storedAttributes = NFTStore[nftID].attributesArray
if ccm.status == CCM_STATUS_CODE_OK:
NFTStore[nftID].owner = recipientAddress
NFTStore[nftID].attributesArray = getNewAttributes(nftID, storedAttributes, receivedAttributes)
createUserEntry(recipientAddress, nftID)
delete entry escrowEntry(sendingChainID, nftID) from the escrow substore
else: # Return the NFT to the sender
recipientAddress = senderAddress
NFTStore[nftID].owner = recipientAddress
createUserEntry(recipientAddress, nftID)
delete entry escrowEntry(sendingChainID, nftID) from the escrow substore
else: # Execution on the foreign chain
if isNFTSupported(nftID) == False:
emitPersistentEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_CCM_TRANSFER,
data = {
"senderAddress": senderAddress,
"recipientAddress": recipientAddress,
"nftID": nftID,
"sendingChainID": sendingChainID,
"receivingChainID": receivingChainID,
"result": RESULT_NFT_NOT_SUPPORTED
},
topics = [senderAddress, recipientAddress]
)
raise Exception("Non-supported NFT")
if ccm.status == CCM_STATUS_CODE_OK:
Fee.payFee(FEE_CREATE_NFT)
createNFTEntry(recipientAddress, nftID, receivedAttributes)
createUserEntry(recipientAddress, nftID)
else: # Return the NFT to the sender
Fee.payFee(FEE_CREATE_NFT)
recipientAddress = senderAddress
createNFTEntry(recipientAddress, nftID, receivedAttributes)
createUserEntry(recipientAddress, nftID)
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_CCM_TRANSFER,
data = {
"senderAddress": senderAddress,
"recipientAddress": recipientAddress,
"nftID": nftID,
"sendingChainID": sendingChainID,
"receivingChainID": receivingChainID,
"result": RESULT_SUCCESSFUL
},
topics = [senderAddress, recipientAddress]
)
This event has name = EVENT_NAME_TRANSFER
, and is emitted when the transfer and transferInternal functions are called.
senderAddress
: The address of the sending account.recipientAddress
: The address of the receiving account.
transferEventDataSchema = {
"type": "object",
"required": [
"senderAddress",
"recipientAddress",
"nftID",
"result"
],
"properties": {
"senderAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 1
},
"recipientAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 2
},
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 3
},
"result": {
"dataType": "uint32",
"fieldNumber": 4
}
}
}
This event has name = EVENT_NAME_TRANSFER_CROSS_CHAIN
, and is emitted when the transferCrossChain and transferCrossChainInternal functions are called.
senderAddress
: The address of the sending account.recipientAddress
: The address of the receiving account.receivingChainID
: The chain ID of the receiving chain.
transferCrossChainEventDataSchema = {
"type": "object",
"required": [
"senderAddress",
"recipientAddress",
"nftID",
"receivingChainID",
"result"
],
"properties": {
"senderAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 1
},
"recipientAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 2
},
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 3
},
"receivingChainID": {
"dataType": "bytes",
"length": LENGTH_CHAIN_ID,
"fieldNumber": 4
},
"includeAttributes": {
"dataType": "boolean",
"fieldNumber": 5
},
"result": {
"dataType": "bytes",
"length": "uint32",
"fieldNumber": 6
}
}
}
This event has name = EVENT_NAME_CCM_TRANSFER
, and is emitted during the execution of cross-chain NFT transfer messages.
senderAddress
: The address of the sending account.recipientAddress
: The address of the receiving account.
ccmTransferEventDataSchema = {
"type": "object",
"required": [
"senderAddress",
"recipientAddress",
"nftID",
"sendingChainID",
"receivingChainID",
"result"
],
"properties": {
"senderAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 1
},
"recipientAddress": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 2
},
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 3
},
"sendingChainID": {
"dataType": "bytes",
"length": LENGTH_CHAIN_ID,
"fieldNumber": 4
},
"receivingChainID": {
"dataType": "bytes",
"length": LENGTH_CHAIN_ID,
"fieldNumber": 5
},
"result": {
"dataType": "bytes",
"length": "uint32",
"fieldNumber": 6
}
}
}
This event has name = EVENT_NAME_CREATE
, and is emitted when the create function is called.
address
: The address of the NFT owner.nftID
: ID of the created NFT.
createEventDataSchema = {
"type": "object",
"required": [
"address",
"nftID",
"result"
],
"properties": {
"address": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 1
},
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 2
},
"result": {
"dataType": "uint32",
"fieldNumber": 3
}
}
}
This event has name = EVENT_NAME_DESTROY
, and is emitted when the destroy function is called.
address
: The address of the NFT owner.nftID
: ID of the destroyed NFT.
destroyEventDataSchema = {
"type": "object",
"required": [
"address",
"nftID",
"result"
],
"properties": {
"address": {
"dataType": "bytes",
"length": LENGTH_ADDRESS,
"fieldNumber": 1
},
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 2
},
"result": {
"dataType": "uint32",
"fieldNumber": 3
}
}
}
This event has name = EVENT_NAME_LOCK
, and is emitted when the lock function is called.
module
: Name of the module that locked the NFT.nftID
: ID of the locked NFT.
lockEventDataSchema = {
"type": "object",
"required": [
"module",
"nftID",
"result"
],
"properties": {
"module": {
"dataType": "string",
"minLength": MIN_LENGTH_MODULE_NAME,
"maxLength": MAX_LENGTH_MODULE_NAME,
"fieldNumber": 1
},
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 2
},
"result": {
"dataType": "uint32",
"fieldNumber": 3
}
}
}
This event has name = EVENT_NAME_UNLOCK
, and is emitted when the unlock function is called.
module
: Name of the module that unlocked the NFT.nftID
: ID of the unlocked NFT.
unlockEventDataSchema = {
"type": "object",
"required": [
"module",
"nftID",
"result"
],
"properties": {
"module": {
"dataType": "string",
"minLength": MIN_LENGTH_MODULE_NAME,
"maxLength": MAX_LENGTH_MODULE_NAME,
"fieldNumber": 1
},
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 2
},
"result": {
"dataType": "uint32",
"fieldNumber": 3
}
}
}
This event has name = EVENT_NAME_SET_ATTRIBUTES
, and is emitted when the setAttributes function is called.
nftID
: ID of the NFT.
setAttributesEventDataSchema = {
"type": "object",
"required": [
"nftID",
"attributes",
"result"
],
"properties": {
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 1
},
"attributes": {
"dataType": "bytes",
"fieldNumber": 2
},
"result": {
"dataType": "uint32",
"fieldNumber": 3
}
}
}
This event has name = EVENT_NAME_RECOVER
, and is emitted when the recover function is called.
nftID
: ID of the recovered NFT.
recoverEventDataSchema = {
"type": "object",
"required": [
"terminatedChainID",
"nftID",
"result"
],
"properties": {
"terminatedChainID": {
"dataType": "bytes",
"maxLength": LENGTH_CHAIN_ID,
"fieldNumber": 1
},
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 2
},
"result": {
"dataType": "uint32",
"fieldNumber": 3
}
}
}
This event has name = EVENT_NAME_SUPPORT_ALL_NFTS
, and is emitted when the supportAllNFTs
function is called.
supportAllNFTsDataSchema = {
"type": "object",
"required": [],
"properties": {}
}
This event has name = EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS
, and is emitted when the removeSupportAllNFTs
function is called.
removeSupportAllNFTsDataSchema = {
"type": "object",
"required": [],
"properties": {}
}
This event has name = EVENT_NAME_SUPPORT_ALL_NFTS_FROM_CHAIN
, and is emitted when the supportAllNFTsFromChain
function is called.
chainID
: The ID of the chain for which all NFTs are supported.
supportAllNFTsFromChainEventDataSchema = {
"type": "object",
"required": ["chainID"],
"properties": {
"chainID": {
"dataType": "bytes",
"length": LENGTH_CHAIN_ID,
"fieldNumber": 1
}
}
}
This event has name = EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS_FROM_CHAIN
, and is emitted when the removeSupportAllNftsFromChain
function is called.
chainID
: The ID of the chain for which all NFTs are supported.
Same as in previous event, i.e., follow the supportAllNFTsFromChainEventDataSchema
.
This event has name = EVENT_NAME_SUPPORT_ALL_NFTS_FROM_COLLECTION
, and is emitted when the supportAllNftsFromCollection function is called.
chainID
: The ID of the native chain of the supported collection.collectionID
: The ID of the collection from which all NFTs are supported.
supportAllNFTsFromCollectionEventDataSchema = {
"type": "object",
"required": [
"chainID",
"collectionID"
],
"properties": {
"chainID": {
"dataType": "bytes",
"length": LENGTH_CHAIN_ID,
"fieldNumber": 1
},
"collectionID": {
"dataType": "bytes",
"length": LENGTH_COLLECTION_ID,
"fieldNumber": 2
}
}
}
This event has name = EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS_FROM_COLLECTION
, and is emitted when the removeSupportAllNFTsFromCollection function is called.
chainID
: The ID of the native chain of the supported collection.collectionID
: The ID of the collection from which all NFTs are supported.
Same as in previous event, i.e., follow the supportAllNFTsFromCollectionEventDataSchema
.
def createNFTEntry(
address: Address,
nftID: NFTID,
attributesArray: AttributesArray
) -> None:
if len(attributesArray) != len(set(item.module for item in attributesArray)):
raise Exception("Invalid attributes array provided")
create substore entry with
substorePrefix = SUBSTORE_PREFIX_NFT
key = nftID
value = encode(
schema = NFTStoreSchema,
object = {
"owner": address,
"attributesArray": attributesArray
}
)
def createUserEntry(
address: Address,
nftID: NFTID
) -> None:
create substore entry with
substorePrefix = SUBSTORE_PREFIX_USER
key = address + nftID
value = encode(
schema = userStoreSchema,
object = {
"lockingModule": NFT_NOT_LOCKED
}
)
def createEscrowEntry(
receivingChainID: ChainID,
nftID: NFTID
) -> None:
create substore entry with
substorePrefix = SUBSTORE_PREFIX_ESCROW
key = receivingChainID + nftID
def verifyTransferInternal(
senderAddress: Address,
recipientAddress: Address,
nftID: NFTID,
emitEvent: bool
) -> None:
try:
nft = getNFT(nftID)
except:
if emitEvent:
emitFailedTransferEvent(senderAddress, recipientAddress, nftID, RESULT_NFT_DOES_NOT_EXIST)
raise Exception("NFT does not exist")
owner = nft["owner"]
if isNFTEscrowed(nft):
if emitEvent:
emitFailedTransferEvent(senderAddress, recipientAddress, nftID, RESULT_NFT_ESCROWED)
raise Exception("NFT is escrowed to another chain")
if owner != senderAddress:
if emitEvent:
emitFailedTransferEvent(senderAddress, recipientAddress, nftID, RESULT_INITIATED_BY_NONOWNER)
raise Exception("Transfer not initiated by the NFT owner")
if isNFTLocked(nft):
if emitEvent:
emitFailedTransferEvent(senderAddress, recipientAddress, nftID, RESULT_NFT_LOCKED)
raise Exception("Locked NFTs cannot be transferred")
def emitFailedTransferEvent(
senderAddress: Address,
recipientAddress: Address,
nftID: NFTID,
result: uint32
) -> None:
emitPersistentEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_TRANSFER,
data = {
"senderAddress": senderAddress,
"recipientAddress": recipientAddress,
"nftID": nftID,
"result": result
},
topics = [senderAddress, recipientAddress]
)
def transferInternal(
senderAddress: Address,
recipientAddress: Address,
nftID: NFTID
) -> None:
delete entry userStore[senderAddress, nftID] from the user substore
createUserEntry(recipientAddress, nftID)
NFTStore[nftID].owner = recipientAddress
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_TRANSFER,
data = {
"senderAddress": senderAddress,
"recipientAddress": recipientAddress,
"nftID": nftID,
"result": RESULT_SUCCESSFUL
},
topics = [senderAddress, recipientAddress]
)
def verifyTransferCrossChainInternal(
senderAddress: Address,
recipientAddress: Address,
nftID: NFTID,
receivingChainID: ChainID,
messageFee: uint64,
data: str,
includeAttributes: bool,
emitEvent: bool
) -> None:
try:
nft = getNFT(nftID)
except:
if emitEvent:
emitFailedTransferCrossChainEvent(senderAddress, recipientAddress, nftID, receivingChainID, RESULT_NFT_DOES_NOT_EXIST)
raise Exception("NFT does not exist")
owner = nft["owner"]
if getChainID(nftID) not in [OWN_CHAIN_ID, receivingChainID]:
if emitEvent:
emitFailedTransferCrossChainEvent(senderAddress, recipientAddress, nftID, receivingChainID, RESULT_NFT_NOT_NATIVE)
raise Exception("NFT must be native either to the sending chain or the receiving chain")
if receivingChainID == OWN_CHAIN_ID:
if emitEvent:
emitFailedTransferCrossChainEvent(senderAddress, recipientAddress, nftID, receivingChainID, INVALID_RECEIVING_CHAIN)
raise Exception("Receiving chain cannot be the sending chain.")
if len(data) > MAX_LENGTH_DATA:
if emitEvent:
emitFailedTransferCrossChainEvent(senderAddress, recipientAddress, nftID, receivingChainID, RESULT_DATA_TOO_LONG)
raise Exception("Data field is too long")
if isNFTEscrowed(nft):
if emitEvent:
emitFailedTransferCrossChainEvent(senderAddress, recipientAddress, nftID, receivingChainID, RESULT_NFT_ESCROWED)
raise Exception("NFT is escrowed to another chain")
if owner != senderAddress:
if emitEvent:
emitFailedTransferCrossChainEvent(senderAddress, recipientAddress, nftID, receivingChainID, RESULT_INITIATED_BY_NONOWNER)
raise Exception("Transfer not initiated by the NFT owner")
if isNFTLocked(nft):
if emitEvent:
emitFailedTransferCrossChainEvent(senderAddress, recipientAddress, nftID, receivingChainID, RESULT_NFT_LOCKED)
raise Exception("Locked NFTs cannot be transferred")
messageFeeTokenID = Interoperability.getMessageFeeTokenID(receivingChainID)
if Token.getAvailableBalance(senderAddress, messageFeeTokenID) < messageFee:
if emitEvent:
emitFailedTransferCrossChainEvent(senderAddress, recipientAddress, nftID, receivingChainID, RESULT_INSUFFICIENT_BALANCE)
raise Exception("Insufficient balance for the message fee")
def emitFailedTransferCrossChainEvent(
senderAddress: Address,
recipientAddress: Address,
nftID: NFTID,
receivingChainID: ChainID,
result: uint32
) -> None:
emitPersistentEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_TRANSFER_CROSS_CHAIN,
data = {
"senderAddress": senderAddress,
"recipientAddress": recipientAddress,
"nftID": nftID,
"receivingChainID": receivingChainID,
"includeAttributes": includeAttributes,
"result": result
},
topics = [senderAddress, recipientAddress, receivingChainID]
)
def transferCrossChainInternal(
senderAddress: Address,
recipientAddress: Address,
nftID: NFTID,
receivingChainID: ChainID,
messageFee: uint64,
data: str,
includeAttributes: bool
) -> None:
if getChainID(nftID) == OWN_CHAIN_ID:
NFTStore[nftID].owner = receivingChainID
delete entry userStore[senderAddress, nftID] from the user substore
if escrowStore[receivingChainID, nftID] does not exist:
createEscrowEntry(receivingChainID, nftID)
if getChainID(nftID) == receivingChainID:
destroy(senderAddress, nftID)
if includeAttributes:
attributesArray = NFT[nftID].attributesArray
else:
attributesArray = []
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_TRANSFER_CROSS_CHAIN,
data = {
"senderAddress": senderAddress,
"recipientAddress": recipientAddress,
"nftID": nftID,
"receivingChainID": receivingChainID,
"includeAttributes": includeAttributes,
"result": RESULT_SUCCESSFUL
},
topics = [senderAddress, recipientAddress, receivingChainID]
)
Interoperability.send(
sendingAddress = senderAddress,
module = MODULE_NAME_NFT,
crossChainCommand = CROSS_CHAIN_COMMAND_NAME_TRANSFER,
receivingChainID = receivingChainID,
fee = messageFee,
params = encode(
schema = crossChainNFTTransferMessageParamsSchema,
object = {
"nftID": nftID,
"senderAddress": senderAddress,
"recipientAddress": recipientAddress,
"attributesArray": attributesArray,
"data": data
}
)
)
This function is used when the native NFT is received from a foreign chain, with a default behavior is to always rewrite the received attributes with the ones in the NFT substore.
def getNewAttributes(
nftID: NFTID,
storedAttributes: AttributesArray,
receivedAttributes: AttributesArray
) -> AttributesArray:
return storedAttributes
This function returns the escrow status of an NFT.
def isNFTEscrowed(nft: NFT) -> bool:
owner = nft["owner"]
return len(owner) != LENGTH_ADDRESS
This function returns the locking status of an NFT.
def isNFTLocked(nft: NFT) -> bool:
if "lockingModule" not in nft:
return False
lockingModule = nft["lockingModule"]
return lockingModule != NFT_NOT_LOCKED
This function returns the support status of an NFT.
def isNFTSupported(nftID: NFTID) -> bool:
chainID = getChainID(nftID)
collectionID = getCollectionID(nftID)
if getChainID(nftID) == OWN_CHAIN_ID:
return True
if supportedNFTsStore[ALL_SUPPORTED_NFTS_KEY] exists:
return True
if supportedNFTsStore[chainID] exists:
if supportedNFTsStore[chainID].supportedCollectionIDArray == []:
return True
if collectionID is in supportedNFTsStore[chainID].supportedCollectionIDArray:
return True
return False
This function returns the native chain chainID
of an NFT.
def getChainID(nftID: NFTID) -> ChainID:
return nftID[:LENGTH_CHAIN_ID]
This function returns the collectionID
of an NFT.
def getCollectionID(nftID: NFTID) -> CollectionID:
return nftID[LENGTH_CHAIN_ID:(LENGTH_CHAIN_ID + LENGTH_COLLECTION_ID)]
This function returns NFT and user substores information for a provided nftID
.
def getNFT(nftID: NFTID) -> dict:
if NFTStore[nftID] does not exist:
raise Exception("NFT substore entry does not exist")
owner = NFTStore[nftID].owner
attributesArray = [
{
"module": attribute.module,
"attributes": attribute.attributes
}
for attribute in NFTStore[nftID].attributesArray
]
nftIDInfo = {
"owner": owner,
"attributesArray": attributesArray
}
if len(owner) == LENGTH_ADDRESS:
if userStore[owner, nftID] does not exist:
raise Exception("User substore entry does not exist")
lockingModule = userStore[owner, nftID].lockingModule
nftIDInfo["lockingModule"] = lockingModule
return nftIDInfo
This function returns the next available index of a collection.
def getNextAvailableIndex(collectionID: CollectionID) -> uint64:
start = OWN_CHAIN_ID + collectionID + uint64be(0)
end = OWN_CHAIN_ID + collectionID + uint64be(2**64 - 1)
largestKey = lexicographically largest element in NFTStore with key between start and end
if largestKey does not exist:
return 0
largestIndex = int.from_bytes(largestKey[(LENGTH_CHAIN_ID + LENGTH_COLLECTION_ID):], byteorder='big')
if largestIndex == 2**64 - 1:
raise Exception("No more available indexes")
return largestIndex + 1
This function creates an NFT.
def create(
address: Address,
collectionID: CollectionID,
attributesArray: AttributesArray
) -> None:
index = getNextAvailableIndex(collectionID)
nftID = OWN_CHAIN_ID + collectionID + uint64be(index)
if len(attributesArray) != len(set(item.module for item in attributesArray)):
raise Exception("Invalid attributes array provided")
Fee.payFee(FEE_CREATE_NFT)
createNFTEntry(address, nftID, attributesArray)
createUserEntry(address, nftID)
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_CREATE,
data = {
"address": address,
"nftID": nftID,
"result": RESULT_SUCCESSFUL
},
topics = [address, nftID]
)
This function destroys an NFT.
def destroy(
address: Address,
nftID: NFTID
) -> None:
try:
nft = getNFT(nftID)
except:
emitFailedDestroyEvent(address, nftID, RESULT_NFT_DOES_NOT_EXIST)
raise Exception("NFT does not exist")
owner = nft["owner"]
if isNFTEscrowed(nft):
emitFailedDestroyEvent(address, nftID, RESULT_NFT_ESCROWED)
raise Exception("NFT is escrowed to another chain")
if owner != address:
emitFailedDestroyEvent(address, nftID, RESULT_INITIATED_BY_NONOWNER)
raise Exception("Not initiated by the NFT owner")
if isNFTLocked(nft):
emitFailedDestroyEvent(address, nftID, RESULT_NFT_LOCKED)
raise Exception("Locked NFTs cannot be destroyed")
delete entry NFTStore[nftID] from the NFT substore
delete entry userStore[address, nftID] from the user substore
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_DESTROY,
data = {
"address": address,
"nftID": nftID,
"result": RESULT_SUCCESSFUL
},
topics = [address, nftID]
)
def emitFailedDestroyEvent(
address: Address,
nftID: NFTID,
result: uint32
) -> None:
emitPersistentEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_DESTROY,
data = {
"address": address,
"nftID": nftID,
"result": result
},
topics = [address, nftID]
)
This function locks an NFT to a given module. Note that an NFT can not be locked to the NFT module.
def lock(
module: Module,
nftID: NFTID
) -> None:
if module == NFT_NOT_LOCKED:
raise Exception("Cannot be locked by NFT module")
try:
nft = getNFT(nftID)
except:
emitFailedLockEvent(module, nftID, RESULT_NFT_DOES_NOT_EXIST)
raise Exception("NFT does not exist")
if isNFTEscrowed(nft):
emitFailedLockEvent(module, nftID, RESULT_NFT_ESCROWED)
raise Exception("NFT is escrowed to another chain")
if isNFTLocked(nft):
emitFailedLockEvent(module, nftID, RESULT_NFT_LOCKED)
raise Exception("NFT is already locked")
owner = nft["owner"]
userStore[owner, nftID].lockingModule = module
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_LOCK,
data = {
"module": module,
"nftID": nftID,
"result": RESULT_SUCCESSFUL
},
topics = [module, nftID]
)
def emitFailedLockEvent(
module: Module,
nftID: NFTID,
result: uint32
) -> None:
emitPersistentEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_LOCK,
data = {
"module": module,
"nftID": nftID,
"result": result
},
topics = [module, nftID]
)
This function unlocks an NFT that was previously locked to a module.
def unlock(
module: Module,
nftID: NFTID
) -> None:
try:
nft = getNFT(nftID)
except:
emitFailedUnlockEvent(module, nftID, RESULT_NFT_DOES_NOT_EXIST)
raise Exception("NFT does not exist")
if isNFTEscrowed(nft):
emitFailedUnlockEvent(module, nftID, RESULT_NFT_ESCROWED)
raise Exception("NFT is escrowed to another chain")
if not isNFTLocked(nft):
emitFailedUnlockEvent(module, nftID, RESULT_NFT_NOT_LOCKED)
raise Exception("NFT is not locked")
lockingModule = nft["lockingModule"]
if lockingModule != module:
emitFailedUnlockEvent(module, nftID, RESULT_UNAUTHORIZED_UNLOCK)
raise Exception("Unlocking NFT via module that did not lock it")
owner = nft["owner"]
userStore[owner, nftID].lockingModule = NFT_NOT_LOCKED
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_UNLOCK,
data = {
"module": module,
"nftID": nftID,
"result": RESULT_SUCCESSFUL
},
topics = [module, nftID]
)
def emitFailedUnlockEvent(
module: Module,
nftID: NFTID,
result: uint32
) -> None:
emitPersistentEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_UNLOCK,
data = {
"module": module,
"nftID": nftID,
"result": result
},
topics = [module, nftID]
)
This function modifies the attributes of an NFT.
def setAttributes(
module: Module,
nftID: NFTID,
attributes: bytes
) -> None:
if NFTStore[nftID] does not exist:
emitFailedSetAttributesEvent(nftID, attributes, RESULT_NFT_DOES_NOT_EXIST)
raise Exception("NFT substore entry does not exist")
NFTStore[nftID].attributesArray[module] = attributes
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_SET_ATTRIBUTES,
data = {
"nftID": nftID,
"attributes": attributes,
"result": RESULT_SUCCESSFUL
},
topics = [nftID]
)
def emitFailedSetAttributesEvent(
module: Module,
nftID: NFTID,
attributes: bytes,
result: uint32
) -> None:
emitPersistentEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_SET_ATTRIBUTES,
data = {
"nftID": nftID,
"attributes": attributes,
"result": result
},
topics = [nftID]
)
This function transfers ownership of an NFT within one chain.
def transfer(
senderAddress: Address,
recipientAddress: Address,
nftID: NFTID
) -> None:
verifyTransferInternal(senderAddress, recipientAddress, nftID, True)
transferInternal(senderAddress, recipientAddress, nftID)
This function transfers ownership of an NFT across chains in the Lisk ecosystem.
def transferCrossChain(
senderAddress: Address,
recipientAddress: Address,
nftID: NFTID,
receivingChainID: ChainID,
messageFee: uint64,
data: str,
includeAttributes: bool
) -> None:
verifyTransferCrossChainInternal(
senderAddress,
recipientAddress,
nftID,
receivingChainID,
messageFee,
data,
includeAttributes,
True
)
transferCrossChainInternal(
senderAddress,
recipientAddress,
nftID,
receivingChainID,
messageFee,
data,
includeAttributes
)
This function should only be called by the interoperability module. It recovers an NFT escrowed to a terminated chain.
def recover(
terminatedChainID: ChainID,
substorePrefix: bytes,
storeKey: bytes,
storeValue: bytes
) -> None:
if (
substorePrefix != SUBSTORE_PREFIX_NFT
or len(storeKey) != LENGTH_NFT_ID
or storeValue cannot be deserialized using NFTStoreSchema
):
emitFailedRecoverEvent(terminatedChainID, nftID, RESULT_RECOVER_FAIL_INVALID_INPUTS)
raise Exception("Invalid inputs")
chainID = storeKey[:LENGTH_CHAIN_ID]
nftID = storeKey
nftValue = decode(schema = NFTStoreSchema, object = storeValue)
if getChainID(nftID) != OWN_CHAIN_ID:
emitFailedRecoverEvent(terminatedChainID, nftID, RESULT_INITIATED_BY_NONNATIVE_CHAIN)
raise Exception("Recovery called by a foreign chain")
try:
nft = getNFT(nftID)
except:
emitFailedRecoverEvent(terminatedChainID, nftID, RESULT_NFT_NOT_FOUND)
raise Exception("NFT substore entry does not exist")
owner = nft["owner"]
attributesArray = nft["attributesArray"]
if owner != terminatedChainID:
emitFailedRecoverEvent(terminatedChainID, nftID, RESULT_NFT_NOT_ESCROWED)
raise Exception("NFT was not escrowed to terminated chain")
if len(nftValue.owner) != LENGTH_ADDRESS:
emitFailedRecoverEvent(terminatedChainID, nftID, RESULT_INVALID_ACCOUNT)
raise Exception("Invalid account information")
NFTStore[nftID].owner = nftValue.owner
storedAttributes = attributesArray
receivedAttributes = nftValue.attributes
NFTStore[nftID].attributes = getNewAttributes(nftID, storedAttributes, receivedAttributes)
createUserEntry(nftValue.owner, nftID)
delete entry escrowStore[terminatedChainID, nftID] from the escrow substore
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_RECOVER,
data = {
"terminatedChainID": terminatedChainID,
"nftID": nftID,
"result": RESULT_SUCCESSFUL
},
topics = [nftID]
)
def emitFailedRecoverEvent(
terminatedChainID: ChainID,
nftID: NFTID,
result: uint32
) -> None:
emitPersistentEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_RECOVER,
data = {
"terminatedChainID": terminatedChainID,
"nftID": nftID,
"result": result
},
topics = [nftID]
)
This function updates the supported NFTs substore to support all NFTs of the Lisk ecosystem.
def supportAllNFTs() -> None:
if supportedNFTsStore[ALL_SUPPORTED_NFTS_KEY] exists:
return
remove all entries from the supported NFTs substore
create substore entry with
substorePrefix = SUBSTORE_PREFIX_SUPPORTED_NFTS
key = ALL_SUPPORTED_NFTS_KEY
value = encode(
schema = supportedNFTsStoreSchema,
object = {"supportedCollectionIDArray": []}
)
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_SUPPORT_ALL_NFTS,
data = {},
topics = []
)
This function removes support for all non-native NFTs.
def removeSupportAllNFTs() -> None:
remove all entries from the supported NFTs substore
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS,
data = {},
topics = []
)
This function updates the supported NFTs substore to support all non-native NFTs of a specified foreign chain.
def supportAllNFTsFromChain(chainID: ChainID) -> None:
if chainID == OWN_CHAIN_ID:
return
if supportedNFTsStore[ALL_SUPPORTED_NFTS_KEY] exists:
return
if supportedNFTsStore[chainID] exists:
supportedNFTsStore[chainID] = {"supportedCollectionIDArray": []}
else:
create substore entry with
substorePrefix = SUBSTORE_PREFIX_SUPPORTED_NFTS
key = chainID
value = encode(
schema = supportedNFTsStoreSchema,
object = {"supportedCollectionIDArray": []}
)
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_SUPPORT_ALL_NFTS_FROM_CHAIN,
data = {"chainID": chainID},
topics = [chainID]
)
This function removes support for all non-native NFTs of a specified foreign chain.
def removeSupportAllNFTsFromChain(chainID: ChainID) -> None:
if chainID == OWN_CHAIN_ID:
raise Exception("Invalid operation. Support for native NFTs cannot be removed.")
if supportedNFTsStore[ALL_SUPPORTED_NFTS_KEY] exists:
raise Exception("Invalid operation. All NFTs from all chains are supported.")
if supportedNFTsStore[chainID] does not exist:
return
delete entry supportedNFTsStore[chainID] from the supported NFTs substore
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS_FROM_CHAIN,
data = {"chainID": chainID},
topics = [chainID]
)
This function updates the supported NFTs substore to support all non-native NFTs of a specified collection.
def supportAllNFTsFromCollection(chainID: ChainID, collectionID: CollectionID) -> None:
if chainID == OWN_CHAIN_ID:
return
if supportedNFTsStore[ALL_SUPPORTED_NFTS_KEY] exists:
return
if supportedNFTsStore[chainID] exists:
if supportedNFTsStore[chainID].supportedCollectionIDArray == []:
return
add collectionID to supportedNFTsStore[chainID].supportedCollectionIDArray, maintaining the array in lexicographical order
else:
create an entry in the supported NFTs substore with
key = chainID
value = encode(
schema = supportedNFTsStoreSchema,
object = {"supportedCollectionIDArray": [collectionID]}
)
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_SUPPORT_ALL_NFTS_FROM_COLLECTION,
data = {
"chainID": chainID,
"collectionID": collectionID
},
topics = [chainID, collectionID]
)
This function removes support for all non-native NFTs of a specified collection.
def removeSupportAllNFTsFromCollection(chainID: ChainID, collectionID: CollectionID) -> None:
if chainID == OWN_CHAIN_ID:
raise Exception("Invalid operation. Support for native NFTs cannot be removed.")
if supportedNFTsStore[ALL_SUPPORTED_NFTS_KEY] exists:
raise Exception("Invalid operation. All NFTs from all chains are supported.")
if supportedNFTsStore[chainID] exists:
if supportedNFTsStore[chainID].supportedCollectionIDArray == []:
raise Exception("Invalid operation. All NFTs from the specified chain are supported.")
if there exist an item in array supportedNFTsStore[chainID].supportedCollectionIDArray with value collectionID:
remove collectionID from supportedNFTsStore[chainID].supportedCollectionIDArray
if supportedNFTsStore[chainID].supportedCollectionIDArray is empty:
remove supportedNFTsStore[chainID] from the supported NFTs substore
emitEvent(
module = MODULE_NAME_NFT,
name = EVENT_NAME_REMOVE_SUPPORT_ALL_NFTS_FROM_COLLECTION,
data = {
"chainID": chainID,
"collectionID": collectionID
},
topics = [chainID, collectionID]
)
genesisNFTStoreSchema = {
"type": "object",
"required": [
"NFTSubstore",
"supportedNFTsSubstore"
],
"properties": {
"NFTSubstore": {
"type": "array",
"fieldNumber": 1,
"items": {
"type": "object",
"required": [
"nftID",
"owner",
"attributesArray"
],
"properties": {
"nftID": {
"dataType": "bytes",
"length": LENGTH_NFT_ID,
"fieldNumber": 1
},
"owner": {
"dataType": "bytes",
"fieldNumber": 2
},
"attributesArray": {
"type": "array",
"fieldNumber": 3,
"items": {
"type": "object",
"required": ["module", "attributes"],
"properties": {
"module": {
"dataType": "string",
"minLength": MIN_LENGTH_MODULE_NAME,
"maxLength": MAX_LENGTH_MODULE_NAME,
"pattern": "^[a-zA-Z0-9]*$",
"fieldNumber": 1
},
"attributes": {
"dataType": "bytes",
"fieldNumber": 2
}
}
}
}
}
}
},
"supportedNFTsSubstore": {
"type": "array",
"fieldNumber": 2,
"items": {
"type": "object",
"required": [
"chainID",
"supportedCollectionIDArray"
],
"properties": {
"chainID": {
"dataType": "bytes",
"fieldNumber": 1
},
"supportedCollectionIDArray": {
"type": "array",
"fieldNumber": 2,
"items": {
"dataType": "bytes",
"length": LENGTH_COLLECTION_ID
}
}
}
}
}
}
}
During the genesis state initialization stage, the following steps are executed. If any step fails, the block is discarded and has no further effect.
Let genesisBlockAssetBytes
be the data
bytes included in the block assets for the NFT module and let genesisBlockAssetObject
be the deserialization of genesisBlockAssetBytes
according to the genesisNFTStoreSchema
schema, given above.
-
Initial checks on the properties of
genesisBlockAssetObject
:- The
NFTSubstore
must be in lexicographical order ofnftID
. Across all elements of theNFTSubstore
array:- All values given for
nftID
must be unique. - All values given for
owner
must have either lengthLENGTH_ADDRESS
bytes (representing a user address) orLENGTH_CHAIN_ID
bytes (representing a chain ID). - The
attributesArray
must be in lexicographic order ofmodule
name, and all values ofmodule
must be unique.
- All values given for
- The
supportedNFTsSubstore
array, if non-empty, must adhere to one of the following two mutually exclusive conditions:- Have a single entry with
chainID
of length0
and no other entries. This indicates that all NFTs are supported, i.e.,chainID == ALL_SUPPORTED_NFTS_KEY
. In this case, thesupportedCollectionIDArray
should be an empty array. - Otherwise, across all elements, the
chainID
must be unique and have lengthLENGTH_CHAIN_ID
. Furthermore, thesupportedNFTsSubstore
array must be in lexicographic order ofchainID
. Also, for each entry of this array, thesupportedCollectionIDArray
should be in lexicographic order.
- Have a single entry with
- The
-
For each entry
NFTEntry
ingenesisBlockAssetObject.NFTSubstore
:-
Create an entry in the NFT substore with:
storeKey = NFTEntry.nftID storeValue = encode( schema = NFTStoreSchema, object = { "owner": NFTEntry.owner, "attributesArray": NFTEntry.attributesArray } )
-
If
NFTEntry.owner
has lengthLENGTH_ADDRESS
bytes, create an entry in the user substore with:storeKey = NFTEntry.owner + NFTEntry.nftID storeValue = encode( schema = userStoreSchema, object = { "lockingModule": NFT_NOT_LOCKED } )
-
If
NFTEntry.owner
has lengthLENGTH_CHAIN_ID
bytes, create an entry in the escrow substore with:storeKey = NFTEntry.owner + NFTEntry.nftID storeValue = EMPTY_BYTES
-
-
For each entry
supportedNFTsEntry
ingenesisBlockAssetObject.supportedNFTsSubstore
, create an entry in the supported NFTs substore with:storeKey = supportedNFTsEntry.chainID storeValue = encode( schema = supportedNFTsStoreSchema, object = { "supportedCollectionIDArray": [collectionID for each collectionID in supportedNFTsEntry.supportedCollectionIDArray] } )
This section specifies the non-trivial or recommended endpoints of the NFT module and does not include all endpoints.
This function checks whether an NFT exists and is owned by a specific user.
def hasNFT(address: Address, nftID: NFTID) -> bool:
if NFTStore[nftID] does not exist:
return False
return NFTStore[nftID].owner == address
This function has exactly the same logic, inputs and outputs as the getNFT
function specified in Protocol Logic for Other Modules section.
This function returns NFT and user substores information for all NFTs owned by a specific user.
def getNFTs(address: Address) -> list[dict]:
ownedNFTIDs = [nftID for nftID in NFTStore if NFTStore[nftID].owner == address]
ownedNFTIDsInfo = []
for nftID in ownedNFTIDs:
attributesArray = [
{
"module": attribute.module,
"attributes": attribute.attributes
}
for attribute in NFTStore[nftID].attributesArray
]
lockingModule = userStore[address, nftID].lockingModule
ownedNFTIDInfo = {
"nftID": nftID,
"attributesArray": attributesArray,
"lockingModule": lockingModule
}
ownedNFTIDsInfo.append(ownedNFTIDInfo)
return ownedNFTIDsInfo
This function returns a list of all escrowed native NFTs for a given chainID
.
def getEscrowedNFTIDs(chainID: ChainID) -> list[NFTID]:
escrowedNFTIDs = [nftID for nftID in NFTStore if NFTStore[nftID].owner == chainID]
return escrowedNFTIDs
Chains adding support for the NFT module specified in this document need to do so with a hard fork. This proposal does not imply a fork for the Lisk mainchain.