TokenID structure best-practices

Discover more about the token ID structures customized by the Enjin Development team.
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.


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
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.
You can see an example of encoding the type of a token as a bitmask by looking at the WrappedTokenId struct in our demo smart contract game.


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:
token_id = 325;
game_id = token_id / GAME_RANGE
server_id = token_id % GAME_RANGE / SERVER_RANGE
class_id = token_id % GAME_RANGE % SERVER_RANGE / CLASS_RANGE
item_id = token_id % GAME_RANGE % SERVER_RANGE % CLASS_RANGE


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))


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 Efinity.
Ethereum ERC721 and ERC1155 token ids have 256bit in size, while in Efinity 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 in Efinity.
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 Efinity, 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


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:
Remove middle 128bit zeros:
Token ID Encoder Input:
erc1155: {
tokenId: "78c0000000003377",
index: 10,
Resulting on-chain token ID:
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.


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:
hash:["gameName", 123]
Resulting on-chain token ID:
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.


The StringId encoder will take a string and attempt to convert it to an int:
Token Id Encoder Input:
Resulting on-chain token ID:
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.
© 2023 Enjin Pte. Ltd.