Skip to main content

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.

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

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.

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

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

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.

const revokeAsset = async() => {
const mas0 = new MAS0AssetLogic(assetId, wallet);

const response = await mas0.revoke(beneficiary).send();

// Poll for the Interaction Receipt
await response.wait()
}

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

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

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.

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

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.

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

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.

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

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.

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