TokenID Structure
There's no right or wrong way to create your token IDs, as long as they make sense to you and your project. The goal is to end up with a single unique 128bit integer per token, and because of this you have the opportunity to use this number to store some information about the token right in the ID. With that in mind the Enjin Development team has made some suggestions for various different approaches to organizing and structuring token IDs on the Enjin Matrixchain.
Bitmasks, Ranges, and Hashes
Let's suppose that we are going to create a collection with three subclasses, game
, server
and item_class
.
Bitmasks
The first thing we would need to define is the size of each class. The size MUST be a power of 2; As an example, we can have up to 16 games (4bits), 256 servers (8bits), and 65536 items classes (16bit). A TokenId
is 128 bits, so the remaining 100 bits will store the ID
of the token.
// 4 bits, stored at indices 124 - 127
0x01 -> Game 01
0x02 -> Game 02
0x03 -> Game 03
...
// 8 bits, stored at indices 115 - 123
0x01 -> server 01
0xff -> server 255
...
// 16 bits, stored at indices 100 - 114
a34f -> Sword
51b4 -> Helmet
...
// bits 0 - 99 store the id
| GAME | SERVER | CLASS | ID
Sword-01 | 05 | ff | a34f | 0
Sword-02 | 05 | ff | a34f | 1
Helmet-01 | 05 | ff | 51b4 | 0
If we create a token with the ID: 0x05ffa34f000000000000000000000001
, it means it belongs to Game 05, server 255, sword of id #1. On-chain this ID would resolve to 7973487969641194258956085157284544513
.
Ranges
A granular approach in contrast with bitmasks.
|- Game 01: TokenID between 0~1000
|---- Server 01: 0~300
|--------- Class A: 0~20
|--------- Class B: 20~40
|--------- Class B: 40~60
|---- Server 02: 300~600
|--------- Class A: 300~320
|--------- Class B: 320~340
|-Game 02 -> TokenID between 1000~2000
...
token_id = (game_id * 1000) + (server_id * 300) + (class_id * 20) + item_id
In this case, a token with an id 325
belongs to Game 01, Server 02, Class B, item_id #5.
The advantage of ranges is that you can use the bits more efficiently, and to extract the subclasses from the token, you would need to:
|- Game 01: TokenID between 0~1000
|---- Server 01: 0~300
|--------- Class A: 0~20
|--------- Class B: 20~40
|--------- Class B: 40~60
|---- Server 02: 300~600
|--------- Class A: 300~320
|--------- Class B: 320~340
|-Game 02 -> TokenID between 1000~2000
...
token_id = (game_id * 1000) + (server_id * 300) + (class_id * 20) + item_id
Hashes
Given the hash is a collision-resistant cryptographic hash function (ex: sha256, keccak256, blake2, etc), the token is guaranteed to be unique.
class_1 = "Game01"
class_2 = "south-america-server-01"
...
class_n = 567
token_id = hash(hash(class_1) II hash(class_2) II ... II hash(class_n))
Collections
The most used approach on Ethereum is to use ERC-721 contracts, which usually means separating the projects by contract. As an example, we can use the Happy cat and Happy dog collections.
The project/wallet owners have split each and every contract as a single collection, which appears as follows:
Separate by Collection
Separate by collection is probably the easiest method to understand. There is no need to encode/decode the token IDs, making it easier for the users and other developers to know what is happening inside the collection.
The disadvantage is that this method might cost more as one contract or collection is needed for each “project”. This is especially true on Ethereum, where the transaction fees are very high.
Separate by Bitmask
Separate by Bitmask is the most cost-efficient method. It allows the developer to have all its projects/games inside just one collection. It is one less layer to worry about in terms of the organization since you only interact with a single collection/project.
The disadvantage of this method is that it is harder for the users to understand and differ the purpose of each token, especially other developers that will have to work with encoding and decoding the data.
Separate by Hashes
Separate by hashes have no restriction of classes you can use in the token id, and it's the recommended mechanism for teleporting Ethereum ERC721 to Enjin Blockchain.
Ethereum ERC721 and ERC1155 token ids are 256bit in size, while on Enjin Blockchain the token id is 128bit long, so it is not possible to use the same id in both chains; one possible solution is using token_id = hash(chain_id, erc721_address, erc721_id) to uniquely identify an Ethereum NFT on Enjin.
The disadvantage is that with this method, it wouldn't be possible to extract the subclasses from the token_id
Inside each of these contracts, we have a set of tokens, and each token can be assigned to its own metadata; the same approach can be applied to Enjin, as explained by the diagram.
Once you have your integer token ID you can pass it into the various GraphQL queries and mutations that take an EncodableTokenIdInput
using the integer
encoder type which simply passes through the supplied integer without modification:
{
integer: 1001
}
Platform Token ID Encoders
Alongside the above manual methods for generating token IDs the Platform also comes with some built-in token ID encoders you can use to convert data in to integers. These are as follows:
- ERC1155
- Hash
- StringId
ERC1155
This encoder input type accepts a 16bit hex token ID and a 16bit integer index and converts it to a 128bit integer. Although the resulting integer isn't identical to the 256bit ints used on Ethereum you can at least use the same base hex token ID and index, omitting the 'middle' 128bits from the Etherem token ID which were normally not used, for example:
Ethereum ID:
0x78c000000000337700000000000000000000000000000000000000000000000a
Remove middle 128bit zeros:
0x78c0000000003377000000000000000a
Token ID Encoder Input:
{
erc1155: {
tokenId: "78c0000000003377",
index: 10,
}
}
Resulting on-chain token ID:
160504280491028834688987873652194148362
This resulting token ID can be converted back into hex and split into the token ID and index which will then be the same as the familiar hex token IDs from Etheruem. To obtain the exact same Ethereum token ID you can simply re-insert the middle 128bit of zeros, which can then also be converted back into the 256bit int if needed.
Hash
The Hash encoder take an arbitrary array or object and calculates its hash to use as the token ID, e.g.
Token ID Encoder Input:
Token ID Encoder Input:
{
hash:["gameName", 123]
}
Resulting on-chain token ID:
47022901548931199993807182593510291332516584015190854220397276662650872775180
Please note that token IDs created this way cannot be reversed or decoded back into the origin data as hashing is a one-way process.
StringId
The StringId encoder will take a string and attempt to convert it to an int:
Token Id Encoder Input:
{
stringId:"MyToken-0001"
}
Resulting on-chain token ID:
23977024514528806328972881969
The resulting token ID can be converted back into it's original string by first converting the int to hex, and then converting the hex into a string. Please note that some strings may convert into ints larger that 128bit. The encoder will throw an exception if that's the case, so it's best to stick with shorter strings if possible.
Updated 2 months ago