Assembly
Overview
The Assembly system, built on top of Otoms, is a flexible ERC1155 implementation enabling the creation, crafting, and management of both fungible and non-fungible items on the blockchain. Built with upgradeability in mind, the system allows for component-based crafting where items can be created using blueprints that define required components with specific properties.
It’s a community-driven and open source project, source code is available here on GitHub , where you’ll find both code for the web dApp implementation and the contracts.
We greatly welcome contributions to the project! Please reach out on Discord or Twitter/X if you’d like to get involved.
Key Components
- OtomItems: Main ERC1155 token contract that implements the token standard and handles token operations
- OtomItemsCore: Manages item definitions, crafting logic, and item usage behaviors
- OtomItemsTracking: Tracks ownership and supplies of items without requiring an offchain indexer
Item Types
The system supports two primary item types:
- Fungible Items: Standard fungible tokens that stack in inventory, useful for resources or consumables that all have identical properties at all times e.g. bricks, oak logs, gold ore
- Non-Fungible Items: Unique items that can have tiers (1-7) and dynamic properties, suitable for equipment or collectibles where each instance of an item could have different properties at any given time e.g. a car with differing amounts of fuel in the tank
Item and Token ID System
The Otom Items system uses two ID types to track items and their instances:
- Item IDs: Sequential identifiers that represent item definitions or blueprints. Each distinct item type (like “Steel Sword” or “Health Potion”) has a unique item ID assigned when it’s created. Item IDs define what an item is, including its blueprint requirements, traits, and behaviors.
- Token IDs: ERC1155 token identifiers that represent specific instances or quantities of items owned by users. For fungible items, the token ID == item ID, as all instances are identical. For non-fungible items, each crafted instance receives a unique token ID that maps back to its parent item ID. The system maintains this mapping through
getItemIdForToken()
. Non-fungible tokens also store tier information and the actual components used in crafting, enabling dynamic properties based on crafting input. Practically, token IDs are derived by concatenating the item id and the mint count of that item
The system uses a specific function getNonFungibleTokenId(itemId, mintIndex)
to generate token IDs for non-fungible items:
function getNonFungibleTokenId(uint256 itemId, uint256 mintIndex) {
return (uint256(keccak256(abi.encodePacked(itemId, mintIndex))) % 2 ** 128) + 2 ** 128;
}
This function creates a unique token ID by hashing the item ID and mint index together, then ensuring the result is above 2^128. This approach guarantees that:
- Each non-fungible token has a unique ID
- The token ID range is distinct from fungible tokens (all non-fungible token IDs are greater than 2^128), and all fungible token ids are less than 2^128
- ID collisions are prevented by the cryptographic properties of keccak256 hashing, which creates a unique output for each unique (itemId, mintIndex) pair
Blueprint System
Items are defined by blueprints that specify:
- Required components (Otoms, variable Otoms, or other items)
- Property criteria that components must meet
- Quantities of each component needed
Component Types
OTOM - Specific Otom token required
VARIABLE_OTOM - Any Otom meeting property criteria
FUNGIBLE_ITEM - Fungible item component
NON_FUNGIBLE_ITEM - Non-fungible item component
Property Criteria
Components can be required to have specific properties within defined ranges:
PropertyCriterion:
- PropertyType (e.g., ATOM_RADIUS, MASS, ELECTRONEGATIVITY)
- Min/Max values for numerical properties
- Boolean values for flag properties
- String values for categorical properties e.g. the decay type must match some string
- Bytes32 values for the universe hash property
Item Creation and Crafting
Creation Flow
- User creates item definitions with
createFungibleItem
orcreateNonFungibleItem
- Each item receives a unique ID and blueprint definition
- Base traits can be defined at creation time, and for non fungible tokens, get updated as the item is used, or at craft time if the blueprint is variable. Fungible token base traits never change.
Crafting Flow
- User calls
craftItem
with item ID, amount, and any variable components- When crafting variable blueprints, two arrays are provided, 1 for otom token ids used in the variable slots, and 1 for the item token ids used in the variable slots. The ordering of ids within those arrays matter - the first VARIABLE_OTOM blueprint component will be filled by the first element in the first array, and so on.
- Arrays are not needed for OTOM and FUNGIBLE ITEM blueprint components because the token ids are predefined in the blueprint.
- System verifies user has required components meeting criteria
- System burns the components as part of crafting
- For non-fungible items, tier is calculated via mutator contract
- New item tokens are minted to the user with appropriate traits
Items can be “frozen”, meaning their core properties (name, description, mutator address, initial traits) can no longer be updated, and the behaviour of mutation is set in stone. Freezing is a one way operation.
Tier System
Non-fungible items have optional tiers (1-7) that are calculated by mutator contracts based on components used and price paid in crafting.
The intent is for users to seek out the most powerful otoms to use in blueprints, so that they can get the top tier version of the item.
Tiers are not named, they should just be referred to as T1 through T7. This is so that at a glance every user knows the relative power the item is considered to have.
If all instances of a non fungible item are equal, the developer can set the tier to 0 and no tier will be set.
Item Usage
Items can be:
- Crafted: Trigger custom logic in mutator contracts through
onCraft
- This hook allows custom checks to be made when crafting an item e.g. a limited edition craft only available for a set time period.
- Used: Trigger custom logic in mutator contracts through
useItem
- ONLY non fungible items can be used in this way
- The intent is that “using” an item then causes the traits to be altered in some way. Differing traits for the same item is only possible if the item is non fungible
- Using an item may destroy it, if the mutator contract called in the use function says so
- Consumed: Burned via
consumeItem
- ONLY fungible items can be consumed
- Fungible items have no reason to be “used” because the tokens are all identical. But they can still be “consumed”, e.g., using 10 bricks to build a house, the 10 bricks will be consumed.
The useItem
and consumeItem
functions are expected to be called by contracts, not by users directly. In order for the calls to succeed, the user must have granted approval to the caller to be allowed to use their OtomItems nfts. There are 3 ways to grant approval:
- Call the ERC1155
setApprovalForAll
function on the nft contract itself - unsafe because it means the operator will have access to all of the user’s items even if they aren’t relevant - Call
setApprovalForItemIds
on the core contract, which approves an operator to consume that item or use any instance of a non fungible item - Call
setApprovalForTokenIds
on the core contract, which approves an operator to use/consume that specific token id
Trait System
Items have dynamic traits that:
Trait:
- typeName: The name of the trait
- valueString: String representation (for STRING type traits)
- valueNumber: Numeric representation (for NUMBER type traits)
- traitType: Either NUMBER or STRING
- Can be updated through item usage via mutator contracts
- Affect item functionality and appearance
- Store custom metadata relevant to the item
Rendering
The rendering is handled by a hot swappable renderer contract. All token metadata is onchain. The image rendered is simply the name of the item (and tier), plus the blueprint used to create it.
When creating items, users can provide a “default image uri” for the fungible token and for all tiers of non fungible tokens. This image is not what will appear in wallets and marketplaces, but front ends for the items system (e.g. Assembly), can use these images instead of the onchain svg.
Creating items
Step-by-step guides for creating different items for Assembly
We’ll be using Hardhat in the examples below. Here’s the Otoms repo if you’d prefer to take a look beforehand.
Creating fungible items
- Fill in the following data (use this example data to get started)
const item = {
// name, description, & imageUri: used to generate the NFT metadata.
name: 'Jusonic Ore',
description: 'A ductile metal, known for its strength and lightweight',
imageUri:
'https://arweave.net/cBCEZ6bqtmdkS6Vj8dIAgXhl4O8oDSyAWuqCMlCDToU',
// blueprint: list of Otoms used to craft your Item
// (in our example we chose the Otom Ju2 with the an amount of 2).
blueprint: [
{
componentType: 0, // ComponentType.OTOM
itemIdOrOtomTokenId:
'35159569680903626501449329353512578019171730918789714169601127546706358467814', // Ju2
amount: 3,
criteria: [],
},
],
// traits: these are optional
traits: [
{
typeName: 'Strength',
valueString: '70',
valueNumber: 70,
traitType: 0, // TraitType.NUMBER
},
{
typeName: 'Weight',
valueString: '10',
valueNumber: 10,
traitType: 0, // TraitType.NUMBER
},
{
typeName: 'Build Materials',
valueString: 'Three Ju2',
valueNumber: 0,
traitType: 1, // TraitType.STRING
},
],
// costInWei: what you want to charge for the item
costInWei: 0,
// feeRecipient: who should get the fees generated from by item
feeRecipient: '0x0000000000000000000000000000000000000000',
};
- Call the
createFungibleItem
function on theOtomItemsCore
contract
const core = await ethers.getContractAt(
'OtomItemsCore',
'0xe8af571878D33CfecA4eA11caEf124E5ef105a30' // Shape Mainnet
);
await core.createFungibleItem(
item.name,
item.description,
item.imageUri,
item.blueprint,
item.traits,
item.costInWei,
item.feeRecipient
);
And that’s all you need to create your first fungible Assembly item.
Creating non-fungible items
- Fill in the following data
- Note: this example data is different than the previous example data
const testPick = {
// name, description, & imageUri: used to generate the NFT metadata.
name: "Invisibility Cloak",
description: "A mythical cloak that grants invisibility",
imageUri:
"https://arweave.net/D3uuvnLDY48wmJ_Qic5l3AUrQ1pfOaUYn79t9PJYG18",
// tieredImageUris: used for tiered items; optionally leave empty.
tieredImageUris: ["", "", "", "", "", "", ""],
// blueprint: list of Otoms used to craft your Item
// (in our example we chose the Otom Ju3 with the an amount of 1).
blueprint: [
{
componentType: 0, // ComponentType.OTOM
itemIdOrOtomTokenId: "59556967404187678662646267921069430003902119556859655597906602022599940926148", // T15
amount: 1,
criteria: []
},
{
componentType: 0, // ComponentType.OTOM
itemIdOrOtomTokenId: "103207475906385737307003328874764609497044818069100475897999853498353983732479", // Sr2
amount: 2,
criteria: []
}
],
// traits: these are optional,
// but become useful when paired with a mutator contract (more on that later).
traits: [
{
typeName: "Ability",
valueString: "Invisibility",
valueNumber: 0,
traitType: 1 // TraitType.STRING
},
{
typeName: "Build Materials",
valueString: "T15 and two Sr2",
valueNumber: 0,
traitType: 1 // TraitType.STRING
}
],
// costInWei: what you want to charge for the item
costInWei: 0,
// feeRecipient: who should get the fee
feeRecipient: "0x0000000000000000000000000000000000000000",
};
- Call the
createNonFungibleItem
function on theOtomItemsCore
contract
const core = await ethers.getContractAt(
'OtomItemsCore',
'0xe8af571878D33CfecA4eA11caEf124E5ef105a30' // Shape Mainnet
);
await core.createNonFungibleItem(
item.name,
item.description,
item.imageUri,
item.tieredImageUris,
item.blueprint,
item.traits,
'0x0000000000000000000000000000000000000000',
// ^this is where you mutator address would go
item.costInWei,
item.feeRecipient
);
Now you can create non-fungible items for Assembly.
Creating non-fungible items (with a mutator)
- Create your mutator contract and implement the
IOtomItemMutator
hooks for custom item logic (calculateTier
,onCraft
,onItemUse
,onTransfer
)- Here is the interface you will need, and here is an example of a generic mutator
- Once you’ve deployed your mutator contract, follow step #1 of the previous tutorial
- Note: If you handle tiers inside your mutator contract, try adding
tieredImageUris
- Note: If you handle tiers inside your mutator contract, try adding
- Now we will call the
createNonFungibleItem
function on theOtomItemsCore
contract, this time we pass in our mutator address
const core = await ethers.getContractAt(
'OtomItemsCore',
'0xe8af571878D33CfecA4eA11caEf124E5ef105a30' // Shape Mainnet
);
const mutatorAddress = '0xYourContractAddressHere';
await core.createNonFungibleItem(
item.name,
item.description,
item.imageUri,
item.tieredImageUris,
item.blueprint,
item.traits,
mutatorAddress,
// ^this is the only difference
item.costInWei,
item.feeRecipient
);
Now you’ve created an Assembly item with a custom mutator contract.