Working with Assets in MOI
This tutorial demonstrates how to create, retrieve, transfer, mint, burn, approve, revoke, lock, and release assets in MOI, based on the predefined protocol asset standard — MAS0 (MOI Asset Standard 0) and It also showcases how to define and deploy a custom asset standard, and invoke routines within it.
MAS0 defines the foundational behavior for programmable assets in the MOI ecosystem, enabling developers to perform supply modulation, access control, and advanced state-driven operations with native protocol support.
ℹ️ Note: MAS0 serves as the base standard for fungible asset types within the MOI ecosystem.
Documentation for the MAS0 standard (non-fungible and advanced programmable assets) will be added soon.
Create & Manage MAS0 Asset in MOI
Prerequisites
To effectively utilise this guide, we recommend reading our guide on Submitting an Interaction and the documentation regarding, Interactions, Participants and Assets
Getting Started
This is a walkthrough of the different capabilities of MAS0 Assets in MOI. We use the JS-MOI-SDK to submit Interactions and make RPC calls to the network. This can also be done directly with the JSON-RPC API or with MOI Voyage.
This walkthrough assumes that your code is already set up with VoyageProvider and that the wallet signer for sending Interactions is initialized. Refer to the section on Setting up JS-MOI-SDK.
Creating an Asset
We must first create an Asset in order to transfer and manage it. We can do this using the AssetCreate interaction and specify the appropriate parameters and metadata. For the sake of this guide, we will first create an MAS0 Asset (Fungible Token) with the symbol CANDYLAND001.
- Code
- Output
const createAsset = async() => {
// Submit the AssetCreate Interaction and await the response
const response = await MAS0AssetLogic.create(
wallet, "CANDYLAND001", 200000, "0x000000001ec28dabfc3e4ac4dfc2084b45785b5e9cf1287b63a4f46900000000", true,
).send();
// Obtain the Interaction Hash and print it
const ixhash = response.hash;
console.log("Interaction Hash: ", ixhash)
// Poll for the Interaction receipt and print it
const receipt = await response.wait()
console.log("Interaction Receipt: ", receipt)
}
// Console Output
Interaction Hash: 0xec421597a48f1fae649a3817e5668b5f5767d38dc4bbe6820bd3128a6802a4be
Interaction Receipt: {
ix_hash: '0xec421597a48f1fae649a3817e5668b5f5767d38dc4bbe6820bd3128a6802a4be',
status: 0,
fuel_used: '0x8d9',
ix_operations: [ { tx_type: '0x4', status: 0, data: [Object] } ],
from: '0x000000001ec28dabfc3e4ac4dfc2084b45785b5e9cf1287b63a4f46900000000',
ts_hash: '0x4b0a1c6d8732e21f4a8f1fcd867bc7c7e5a79e642a99dd2a1f37fe1c11889400',
participants: [
{
id: '0x000000001ec28dabfc3e4ac4dfc2084b45785b5e9cf1287b63a4f46900000000',
height: '0x5',
transitive_link: '0x36bd385d23eb81702eefdc0cbbff891a0dd5aef358a9d9b6985e3d9aeb37759a',
locked_context: '0x08d829a5911b07af4741fb77cd7d33d0cd6b81f1bf335314de9281ec8d30c4f6',
context_delta: null,
state_hash: '0x9a238e8af5ad545f5de52b1458ca5b0c78b4122d121b4bb41b3f1d1a179f6863'
},
{
id: '0x10030000d9f12d13c0347a737430cdbcca174164f0823ccc39bedbe800000000',
height: '0x0',
transitive_link: '0x0000000000000000000000000000000000000000000000000000000000000000',
locked_context: '0x0000000000000000000000000000000000000000000000000000000000000000',
context_delta: [Object],
state_hash: '0x02ca19c2c6c1a115acab1ffae49ee6a47438362727d9b252ed70fbdef623f25a'
},
{
id: '0x20800000a6ba9853f131679d00da0f033516a2efe9cd53c3d54e1f9a00000000',
height: '0x2',
transitive_link: '0x36bd385d23eb81702eefdc0cbbff891a0dd5aef358a9d9b6985e3d9aeb37759a',
locked_context: '0xe4cd45abf9de20991d1a73e77b9d7313aacbfc49a276cea89abc5ca8300e8479',
context_delta: null,
state_hash: '0xb97ef001e12ddbd14e17cf91b4d1d0ed469793d9174e92dadac6b106115bfbf6'
}
]
}
Bingo! We have now successfully created an TOKEN called CANDYLAND001. We can see from the receipt that it has been created with the following id and Asset ID:
[
{
"asset_id": "0x10030000d9f12d13c0347a737430cdbcca174164f0823ccc39bedbe800000000",
"error": "0x"
}
]
Minting New Tokens
Once the asset is created, we can mint tokens to the creator or any beneficiary account. Here we will mint 100000 tokens to the manager’s account.
- Code
- Output
const mintAsset = async() => {
const assetId = "0x10030000d9f12d13c0347a737430cdbcca174164f0823ccc39bedbe800000000";
const manager = (await wallet.getId()).toHex()
const mas0 = new MAS0AssetLogic(assetId, wallet);
const response = await mas0.mint(manager, 100000).send();
// Poll for the Interaction Receipt
const receipt = await response.wait()
// We get the updated supply of MYTOKEN from the receipt and print it
const new_supply = receipt.ix_operations[0].data.total_supply
console.log("New Supply: ", new_supply)
}
// Console Output
New Supply: 0x186A0 // 100000
Retrieving Asset Information
Now that we have successfully created our Asset, let us retrieve some information about it with an RPC Call. We can also check that CANDYLAND001 is available in the spendable balances of the sender’s account.
Retrieving the Metadata of an Asset
- Code
- Output
const getAssetInfo = async() => {
// We get the Asset ID of CANDYLAND001 from the receipt of the Asset Create Interaction
const asset_id = "0x0000000108ffe861df53c4c0c7d3cddeffd65070b90706d535a651bd64a6842bbded00b9"
// Use the moi.AssetInfoByAssetID RPC to fetch Asset Metadata
const asset_info = await provider.getAssetInfoByAssetID(asset_id)
console.log("Asset Metadata: ", asset_info)
}
// Console Output
Asset Metadata: {
dimension: '0x0',
is_logical: false,
is_stateful: false,
operator: '0x45b9906e65c9bdf4703918aa2c78fe139ba8e32c5e0dcda585dac4c584651f08',
standard: '0x1',
supply: '0x1',
symbol: 'CANDYLAND001'
}
Retrieving the Balance of an Asset for a Participant
const getAssetBalance = async() => {
// We get the id of the participant who created the Asset,
// the initial supply of the Asset would have been credited to them
const id = (await wallet.getId()).toHex();
// We get the Asset ID of CANDYLAND001 from the receipt of the Asset Create Interaction
const assetId = "0x0000000108ffe861df53c4c0c7d3cddeffd65070b90706d535a651bd64a6842bbded00b9"
// Use the moi.Balance RPC to fetch the balance of an Asset for a
// specific Asset ID. The moi.TDU RPC can be used to fetch all balances
const balance = await provider.getBalance(id, assetId)
console.log("Asset Balance: ", balance)
}
// Console Output
Asset Balance: 100000
Transferring an Asset between Participants
Now that we have a functional asset, let's transfer this FUNGIBLE TOKEN to another Participant. MOI allows Participants to transfer multiple assets at once because all Assets are recognized natively in the account’s balances.
- Code
- Output
const transferAssets = async() => {
// Submit the AssetTransfer Interaction and await the response
const assetId = "0x10030000d9f12d13c0347a737430cdbcca174164f0823ccc39bedbe800000000";
const beneficiary = "0x00000000ed434a2ab138e69295e134686d57d80a9aa3325dbbde9bbf00000000";
const mas0 = new MAS0AssetLogic(assetId, wallet);
const response = await mas0.transfer(beneficiary, 100).send()
// Obtain the Interaction Hash and print it
const ixhash = response.hash;
console.log("Interaction Hash: ", ixhash)
// Poll for the Interaction Receipt and print it
const receipt = await response.wait()
console.log("Interaction Receipt: ", receipt)
}
// Console Output
Interaction Hash: 0x8b885068cb62864539177507a2687e5f066d26d2cd17f1c16639fdd2d04a2e13
Interaction Receipt: {
ix_hash: '0x8b885068cb62864539177507a2687e5f066d26d2cd17f1c16639fdd2d04a2e13',
status: 0,
fuel_used: '0x64',
ix_operations: [ { tx_type: '0x1', status: 0, data: null } ],
from: '0x45b9906e65c9bdf4703918aa2c78fe139ba8e32c5e0dcda585dac4c584651f08',
ts_hash: '0x71f4a213a0c8aefb98b52c7cd8d5173973a808a694b11adb67daa5d2e0c28b00',
participants: [
{
id: '0x45b9906e65c9bdf4703918aa2c78fe139ba8e32c5e0dcda585dac4c584651f08',
height: '0x6',
transitive_link: '0xd7d81ffe706dff181b49b45c22c31b685745fbd0c68065f263de72e4c0ac6d2e',
prev_context: '0x637104288d0eb688bc7e8332f06db6e68a27b6427ed4458a6649ac2410f891ec',
latest_context: '0x637104288d0eb688bc7e8332f06db6e68a27b6427ed4458a6649ac2410f891ec',
context_delta: null,
state_hash: '0x474183d0deeccd59f2fcc2ba2a995cda096eced59e6d30c64bcb0473941ae3ff'
},
{
id: '0x6ef7715969a7a99edf06957e94494e40203b4be2b0ffb325196677a82de6fcb6',
height: '0x0',
transitive_link: '0x0000000000000000000000000000000000000000000000000000000000000000',
prev_context: '0x0000000000000000000000000000000000000000000000000000000000000000',
latest_context: '0x5c2f5281df903d082e46349558c8ea26bdddeeca502a14b344e104c163c930e6',
context_delta: {
behavioural_nodes: [
'3Wvdsur7X92s2bkwgWomxxnYZnXbU4xfuv1HS4ELqTwaNqcdKTq9.16Uiu2HAmRKggu3i2N26J7ofAHnfV6Sn3WTzRznkN2HierXvL9AxB'
],
random_nodes: [
'3WyySN6RKQ2iWpzjoBKpzSrkLzuye3Jm7nEEmCzyvwrovZ4C6ekw.16Uiu2HAmQwpuc5d6GYoogPfatowFddPeCzVMmGzwEUuRN94ksnh7'
],
replaced_nodes: null
},
state_hash: '0x723c9975b99dce3c3b833e1d5e87b827f04418039c0c1da8be4c2d3121a3bdcd'
},
{
id: '0xa6ba9853f131679d00da0f033516a2efe9cd53c3d54e1f9a6e60e9077e9f9384',
height: '0x7',
transitive_link: '0xd7d81ffe706dff181b49b45c22c31b685745fbd0c68065f263de72e4c0ac6d2e',
prev_context: '0x348561859bb43cb835672e29f9b0e5d8ec79c1c08fc5b9553f9c9fa73c0ff900',
latest_context: '0x348561859bb43cb835672e29f9b0e5d8ec79c1c08fc5b9553f9c9fa73c0ff900',
context_delta: null,
state_hash: '0x4a3e1d03b73e8f1486767b72963091bd6b5872bdaab0c6adbcbbddec0a5269dc'
}
]
}
SUCCESS! We have successfully transferred the CANDYLAND001 Asset from the sender 0x9c6..30a to the receiver 0x6ef..cb6.
Transferring Operatorship of an Asset
When an Asset is created, the creator is marked as its manager. Only the manager can perform privileged actions such as modulating its supply. Currently, the manager of Assets is static and cannot be transferred, but this will change in the coming months.
Adjusting the Supply of an Asset
Supply Modulation of Assets involves the creation (minting) or destruction (burning) of tokens to manipulate its total supply. This action can only be performed on Assets of the MAS0 standard are also only capable of being performed by the operator of the Asset. Any minted or burned tokens are credited to or debited from the spendable balances of the operator.
The CANDYLAND001 Asset we have used until now does not fit support supply modulation. So we will now create
another Asset of the MAS0 standard called MYTOKEN with a 100 million tokens as the initial supply with
AssetCreate Interaction.
- Code
- Output
const createAsset = async() => {
// Submit the AssetCreate Interaction and wait for the response
const id = "0x000000001ec28dabfc3e4ac4dfc2084b45785b5e9cf1287b63a4f46900000000";
const response = await MAS0AssetLogic.create(
wallet, "MYTOKEN", 100000000, id, true,
).send();
// Poll for the Interaction Receipt
const receipt = await response.wait()
// We get the Asset ID of MYTOKEN from the receipt and print it
const asset_id = receipt.ix_operations[0].data.asset_id
console.log("Asset Created: ", asset_id)
// Use the moi.AssetInfoByAssetID RPC to fetch Asset Metadata
const asset_info = await provider.getAssetInfoByAssetID(asset_id)
console.log("Asset Supply: ", asset_info.supply)
}
// Console Output
Asset Created: 0x00000000a314461000bbc64ad953b468abfd2c39103862334ce6cbc301c906391d1bbb4a
Asset Supply: 0x5f5e100 // 100,000,000
Minting New Tokens (Increasing the Supply)
We will now increase the supply of MYTOKEN by 10,000 tokens using the AssetMint Interaction and
verify that the supply has adjusted accordingly.
- Code
- Output
const mintAsset = async() => {
const mas0 = new MAS0AssetLogic(asset_id, wallet);
const response = await mas0.mint(beneficiary, 10000).send();
// Poll for the Interaction Receipt
const receipt = await response.wait()
// We get the updated supply of MYTOKEN from the receipt and print it
const new_supply = receipt.ix_operations[0].data.total_supply
console.log("New Supply: ", new_supply)
}
// Console Output
New Supply: 0x5f60810 // 100,010,000
Burning Existing Tokens (Decreasing the Supply)
The current supply of MYTOKEN is 100,010,00 tokens. We will now decrease the supply by 100,000 using
the AssetBurn Interaction and verify that the supply adjusted accordingly.
- Code
- Output
const burnAsset = async() => {
const mas0 = new MAS0AssetLogic(asset_id, wallet);
const response = await mas0.burn(beneficiary, 100000).send();
// Poll for the Interaction Receipt
const receipt = await response.wait()
// We get the updated supply of MYTOKEN from the receipt and print it
const new_supply = receipt.ix_operations[0].data.total_supply
console.log("New Supply: ", new_supply)
}
// Console Output
New Supply: 0x5f48170 // 99,910,000
Transferring Tokens (Moving Assets Between Accounts)
This interaction transfers a specific number of MYTOKEN units from the sender (connected wallet) to another account (beneficiary). It uses the AssetTransfer interaction and does not alter the total token supply.
- Code
- Output
const transferAsset = async() => {
const mas0 = new MAS0AssetLogic(assetId, wallet);
const response = await mas0.transfer(beneficiary, 5000).send()
// Poll for the Interaction Receipt
await response.wait()
}
// Console Output
This interaction does not return any result
Approving Allowance (Granting Spending Permission)
This interaction allows the owner of MYTOKEN to grant another account permission to spend a defined number of tokens on their behalf. The AssetApprove interaction also accepts an optional expiry timestamp for when the allowance becomes invalid.
- Code
- Output
const approveAsset = async() => {
const mas0 = new MAS0AssetLogic(assetId, wallet);
const response = await mas0.approve(beneficiary, 100000, 1765650600).send()
// Poll for the Interaction Receipt
await response.wait()
}
// Console Output
This interaction does not return any result
Revoking Allowance (Removing Spending Permission)
This interaction revokes any previously approved spending permissions for a given account, preventing that account from transferring tokens on behalf of the owner.
- Code
- Output
const revokeAsset = async() => {
const mas0 = new MAS0AssetLogic(assetId, wallet);
const response = await mas0.revoke(beneficiary).send();
// Poll for the Interaction Receipt
await response.wait()
}
// Console Output
This interaction does not return any result
Transferring Approved Asset
This interaction allows a third party (spender) to transfer MYTOKEN units from a benefactor’s account to another account, provided they’ve been granted allowance via approve().
- Code
- Output
const transferFrom = async() => {
const mas0 = new MAS0AssetLogic(assetId, wallet);
const response = await mas0.transferFrom(benefactor, beneficiary, 100000).send()
// Poll for the Interaction Receipt
await response.wait()
}
// Console Output
This interaction does not return any result
Locking Asset
This interaction locks a specified amount of tokens under a beneficiary’s account, preventing them from being transferred or spent until they are explicitly released.
- Code
- Output
const lockTokens = async() => {
const mas0 = new MAS0AssetLogic(assetId, wallet);
const response = await mas0.lockup(beneficiary, amount).send();
// Poll for the Interaction Receipt
await response.wait()
}
// Console Output
This interaction does not return any result
Release Asset
This interaction releases previously locked tokens, returning them to the beneficiary’s transferable balance. It effectively reverses a prior lockup operation once the conditions for release are met.
- Code
- Output
const releaseTokens = async() => {
const mas0 = new MAS0AssetLogic(assetId, wallet);
const response = await mas0.release(benefactor, beneficiary, amount).send();
// Poll for the Interaction Receipt
await response.wait()
}
// Console Output
This interaction does not return any result
Custom Asset Standard
Creating a Custom Asset
The AssetFactory create method constructs and submits an AssetCreatePayload, which defines the metadata and behavior of a new on-chain asset.
It automatically builds a payload containing the following fields:
symbol – The ticker symbol or short name of the asset.
max_supply – The maximum supply allowed for the asset.
standard – Set internally to AssetStandard.MAS0.
dimension – Default value is 0.
enable_events – A boolean to enable or disable event emission.
manager – The manager address converted to hexadecimal format.
logic_payload – Encoded logic data from the asset’s manifest (if provided).
If a manifest is provided, the function encodes both the manifest and routine calldata (if applicable). It returns an InteractionContext initialized for ASSET_CREATE.
- Code
- Output
const createAsset = async() => {
// Define basic parameters for the new asset
const symbol = "GOLD";
const max_supply = 1000000n;
const managerAddress = "0x9af834c5ab..."; // Address of asset manager
const enable_events = true;
// Optional: Include a manifest and callsite if asset logic requires deploy arguments
const manifest = "0x23343...3234; // Predefined manifest
const callsite = "deploy"; // Routine to be invoked in the manifest
// Create the AssetCreate interaction
const ctx = AssetFactory.create(
wallet, // Signer wallet
symbol, // Asset symbol
max_supply, // Maximum supply
managerAddress, // Manager address
enable_events, // Event emission flag
manifest, // (optional) Manifest
callsite, // (optional) Deploy routine
1000, // (optional) Routine argument 1
"JOHN" // (optional) Routine argument 2
);
// Submit the interaction
const response = await ctx.send();
// Wait for confirmation
const receipt = await response.wait();
// Retrieve and display the newly created asset ID
const asset_id = receipt.ix_operations[0].data.asset_id;
console.log("Asset Created:", asset_id);
// Use the moi.AssetInfoByAssetID RPC to fetch Asset Metadata
const asset_info = await provider.getAssetInfoByAssetID(asset_id)
console.log("Asset Supply: ", asset_info.supply)
}
// Console Output
Asset Created: 0x00000000b714461000bbc64ad953b468abfd2c39103862334ce6cbc301c906391d1bbb4b
Asset Supply: 0x5f5e100 // 100,000,000
Invoking a custom asset
The AssetDriver class provides a direct interface to interact with a deployed asset logic contract. Its constructor initializes the base LogicDriver with the given parameters, enabling seamless invocation of on-chain routines such as minting, transferring, or burning asset units.
assetId (str) – The on-chain ID of the deployed asset.
manifest (Manifest) – The logic manifest describing available routines and interfaces.
signer (Wallet | Signer) – The wallet or signer used to authorize and send interactions.
Once instantiated, the driver.routines object exposes callable logic routines defined in the manifest.
- Code
- Output
const mintAsset = async() => {
// Asset identifier and configuration
const assetId = "0x00000000b714461000bbc64ad953b468abfd2c39103862334ce6cbc301c906391d1bbb4b";
// Initialize the AssetDriver for interaction
const driver = new AssetDriver(assetId, null, wallet);
// Example: invoke the 'mint' routine to issue new tokens
const beneficiary = "0xabcd5678...";
const amount = 5000;
const response = driver.routines.mint(beneficiary, amount).send();
// Wait for confirmation
const receipt = await response.wait();
console.log("New Supply:", receipt.ix_operations[0].total_supply);
}
// Console Output
New Supply: 0x186A0 // 100000