Mosaics represent assets on the NEM blockchain, such as currencies, collectibles, or access rights.
Unlike tokens on other platforms, NEM mosaics are supported directly at the protocol level
and require no additional coding to use.
Their properties are configurable to support various use cases, from simple currencies to tokens with custom supply
and transfer rules.
Every mosaic belongs to a registered namespace, which provides the first half of its
fully qualified name, such as my_namespace:token.
A namespace must therefore be registered before a mosaic can be created.
This tutorial shows how to create a mosaic under an existing namespace and configure its initial properties.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.NemFacadeimportNemFacadefromsymbolchain.ncimportAmountfromsymbolchain.nem.FeeCalculatorimport(calculate_mosaic_rental_fee,calculate_transaction_fee)fromsymbolchain.nem.NetworkimportNetworkTimestampNODE_URL=os.getenv('NODE_URL','http://libertalia.nemtest.net:7890')print(f'Using node {NODE_URL}')SIGNER_PRIVATE_KEY=os.getenv('SIGNER_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000000')signer_key_pair=NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))facade=NemFacade('testnet')signer_address=facade.network.public_key_to_address(signer_key_pair.public_key)print(f'Signer address: {signer_address}')try:# Fetch current network timetime_path='/time-sync/network-time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())network_time=response_json['receiveTimeStamp']//1000print(f' Network time: {network_time} s since the nemesis block')# Derived fields from network timetimestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)# Build the mosaic IDnamespace_name=os.getenv('NAMESPACE','my_namespace')mosaic_name=os.getenv('MOSAIC',f'token_{int(time.time())}')mosaic_id=f'{namespace_name}:{mosaic_name}'print(f'Creating mosaic: {mosaic_id}')# Define the mosaicmosaic_definition={'owner_public_key':signer_key_pair.public_key,'id':{'namespace_id':{'name':namespace_name},'name':mosaic_name},'description':'My tutorial mosaic','properties':[{'property_':{'name':b'divisibility','value':b'2'}},{'property_':{'name':b'initialSupply','value':b'1000'}},{'property_':{'name':b'supplyMutable','value':b'true'}},{'property_':{'name':b'transferable','value':b'true'}}]}# Build the mosaic definition transactionrental_fee=calculate_mosaic_rental_fee()print(f' Mosaic creation fee: {rental_fee/1_000_000} XEM')transaction=facade.transaction_factory.create({'type':'mosaic_definition_transaction_v1','signer_public_key':signer_key_pair.public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,'rental_fee_sink':'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC','rental_fee':rental_fee,'mosaic_definition':mosaic_definition})# Calculate and attach the transaction feefee=calculate_transaction_fee(transaction)transaction.fee=Amount(fee)print(f' Transaction fee: {fee/1_000_000} XEM')# Sign and generate final payloadsignature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)print('Built mosaic definition transaction:')print(json.dumps(transaction.to_json(),indent=2))# Announce the transactionannounce_path='/transaction/announce'print(f'Announcing mosaic definition to {announce_path}')announce_request=urllib.request.Request(f'{NODE_URL}{announce_path}',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(announce_request)asresponse:announce_result=json.loads(response.read().decode())print(f' Result: {announce_result["message"]}')# Wait for confirmationif'SUCCESS'==announce_result['message']:transaction_hash=facade.hash_transaction(transaction)status_path=f'/transaction/get?hash={transaction_hash}'print(f'Waiting for confirmation from {status_path}')is_confirmed=Falseforattemptinrange(120):try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asresponse:confirmed=json.loads(response.read().decode())height=confirmed['meta']['height']print(f'Transaction confirmed in block {height}')is_confirmed=Truebreakexcepturllib.error.HTTPError:print(' Transaction status: pending')time.sleep(1)ifnotis_confirmed:print('Confirmation took too long.')else:print(f'Transaction rejected: {announce_result["message"]}')# Retrieve the mosaicdefinition_path=f'/mosaic/definition?mosaicId={mosaic_id}'print(f'Fetching mosaic information from {definition_path}')withurllib.request.urlopen(f'{NODE_URL}{definition_path}')asresponse:mosaic_info=json.loads(response.read().decode())properties={prop['name']:prop['value']forpropinmosaic_info['properties']}print('Mosaic information:')print(f' Creator: {mosaic_info["creator"]}')print(f' Divisibility: {properties["divisibility"]}')print(f' Initial supply: {properties["initialSupply"]}')print(f' Supply mutable: {properties["supplyMutable"]}')print(f' Transferable: {properties["transferable"]}')excepturllib.error.URLErrorase:print(e.reason)
import{PrivateKey}from'symbol-sdk';import{NemFacade,NetworkTimestamp,calculateMosaicRentalFee,calculateTransactionFee,models}from'symbol-sdk/nem';constNODE_URL=process.env.NODE_URL||'http://libertalia.nemtest.net:7890';console.log('Using node',NODE_URL);constSIGNER_PRIVATE_KEY=process.env.SIGNER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constsignerKeyPair=newNemFacade.KeyPair(newPrivateKey(SIGNER_PRIVATE_KEY));constfacade=newNemFacade('testnet');constsignerAddress=facade.network.publicKeyToAddress(signerKeyPair.publicKey);console.log('Signer address:',signerAddress.toString());try{// Fetch current network timeconsttimePath='/time-sync/network-time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();constnetworkTime=Math.floor(timeJSON.receiveTimeStamp/1000);console.log(' Network time:',networkTime,'s since the nemesis block');// Derived fields from network timeconsttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);// Build the mosaic IDconstnamespaceName=process.env.NAMESPACE||'my_namespace';constmosaicName=process.env.MOSAIC||`token_${Math.floor(Date.now()/1000)}`;constmosaicId=`${namespaceName}:${mosaicName}`;console.log('Creating mosaic:',mosaicId);// Define the mosaicconstmosaicDefinition={ownerPublicKey:signerKeyPair.publicKey.toString(),id:{namespaceId:{name:namespaceName},name:mosaicName},description:'My tutorial mosaic',properties:[{property:{name:'divisibility',value:'2'}},{property:{name:'initialSupply',value:'1000'}},{property:{name:'supplyMutable',value:'true'}},{property:{name:'transferable',value:'true'}}]};// Build the mosaic definition transactionconstrentalFee=calculateMosaicRentalFee();console.log(' Mosaic creation fee:',`${Number(rentalFee)/1_000_000} XEM`);consttransaction=facade.transactionFactory.create({type:'mosaic_definition_transaction_v1',signerPublicKey:signerKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,rentalFeeSink:'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',rentalFee,mosaicDefinition});// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.Amount(fee);console.log(' Transaction fee:',`${Number(fee)/1_000_000} XEM`);// Sign and generate final payloadconstsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);console.log('Built mosaic definition transaction:');console.dir(transaction.toJson(),{colors:true});// Announce the transactionconstannouncePath='/transaction/announce';console.log('Announcing mosaic definition to',announcePath);constannounceResponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:jsonPayload});constannounceResult=awaitannounceResponse.json();console.log(' Result:',announceResult.message);// Wait for confirmationif('SUCCESS'===announceResult.message){consttransactionHash=facade.hashTransaction(transaction).toString();conststatusPath=`/transaction/get?hash=${transactionHash}`;console.log('Waiting for confirmation from',statusPath);letisConfirmed=false;for(letattempt=1;120>=attempt;++attempt){constresponse=awaitfetch(`${NODE_URL}${statusPath}`);if(response.ok){constconfirmed=awaitresponse.json();console.log('Transaction confirmed in block',confirmed.meta.height);isConfirmed=true;break;}console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}if(!isConfirmed)console.warn('Confirmation took too long.');}else{console.log('Transaction rejected:',announceResult.message);}// Retrieve the mosaicconstdefinitionPath=`/mosaic/definition?mosaicId=${mosaicId}`;console.log('Fetching mosaic information from',definitionPath);constdefinitionResponse=awaitfetch(`${NODE_URL}${definitionPath}`);constmosaicInfo=awaitdefinitionResponse.json();constproperties=Object.fromEntries(mosaicInfo.properties.map(prop=>[prop.name,prop.value]));console.log('Mosaic information:');console.log(' Creator:',mosaicInfo.creator);console.log(' Divisibility:',properties.divisibility);console.log(' Initial supply:',properties.initialSupply);console.log(' Supply mutable:',properties.supplyMutable);console.log(' Transferable:',properties.transferable);}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
The snippet reads the signer's private key from the SIGNER_PRIVATE_KEY environment variable, which defaults to a
test key if not set.
The signer's address is derived from the public key.
This account will own the created mosaic and must also own the namespace that will hold it.
# Fetch current network timetime_path='/time-sync/network-time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())network_time=response_json['receiveTimeStamp']//1000print(f' Network time: {network_time} s since the nemesis block')# Derived fields from network timetimestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)
// Fetch current network timeconsttimePath='/time-sync/network-time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();constnetworkTime=Math.floor(timeJSON.receiveTimeStamp/1000);console.log(' Network time:',networkTime,'s since the nemesis block');// Derived fields from network timeconsttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);
Network time is fetched from /time-sync/network-timeGET, and the transaction's timestamp and deadline fields
are derived from it, following the process described in the Transfer XEM tutorial.
// Build the mosaic IDconstnamespaceName=process.env.NAMESPACE||'my_namespace';constmosaicName=process.env.MOSAIC||`token_${Math.floor(Date.now()/1000)}`;constmosaicId=`${namespaceName}:${mosaicName}`;console.log('Creating mosaic:',mosaicId);
The mosaic ID is assembled from an existing namespace and a mosaic name.
See Name in the Textbook for the naming rules.
To avoid collisions across multiple runs of the tutorial, a timestamp is added to the mosaic name.
In practice, however, programs would use a fixed name for their mosaics.
You can force the tutorial to use fixed names through the NAMESPACE and MOSAIC environment variables.
Use a namespace owned by the signer
By default, the code uses the test account referenced by SIGNER_PRIVATE_KEY and a namespace named
my_namespace.
If you come from the Registering a Root Namespace tutorial, set the
SIGNER_PRIVATE_KEY and NAMESPACE environment variables to match the account and namespace you created there,
or any other namespace that the signer owns.
: A set of key-value pairs that configure the mosaic behavior:
: The number of decimal places the mosaic supports.
For example, a value of 2 means each whole unit can be divided into 100 (102) atomic units.
See Divisibility in the Textbook.
: The number of whole units minted to the creator when the mosaic is
defined.
See Initial Supply in the Textbook.
: Whether the total supply can be changed after creation.
See Supply Mutability in the Textbook.
: Whether the mosaic can be sent between any two accounts other than the
creator.
See Transferability in the Textbook.
In this example, the mosaic is divisible to two decimal places and starts with a supply of 1000.00 whole
units.
Its supply can be changed after creation, and its units can be freely transferred between accounts.
: The account that signs the transaction and pays the fees, which must be the
owner of the namespace that will hold the mosaic.
It becomes the owner of the created mosaic.
and : The values computed in the network time step.
: The special account that collects mosaic
creation fees.
Each network has a fixed sink address:
// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.Amount(fee);console.log(' Transaction fee:',`${Number(fee)/1_000_000} XEM`);
Finally, the transaction fee is calculated with and attached to the
transaction.
Unlike the creation fee, the transaction fee is paid to the harvester account.
Mosaic definition transactions pay a fixed transaction fee of 0.15 XEM, as shown in the
fee schedule.
# Sign and generate final payloadsignature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)print('Built mosaic definition transaction:')print(json.dumps(transaction.to_json(),indent=2))# Announce the transactionannounce_path='/transaction/announce'print(f'Announcing mosaic definition to {announce_path}')announce_request=urllib.request.Request(f'{NODE_URL}{announce_path}',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(announce_request)asresponse:announce_result=json.loads(response.read().decode())print(f' Result: {announce_result["message"]}')
# Wait for confirmationif'SUCCESS'==announce_result['message']:transaction_hash=facade.hash_transaction(transaction)status_path=f'/transaction/get?hash={transaction_hash}'print(f'Waiting for confirmation from {status_path}')is_confirmed=Falseforattemptinrange(120):try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asresponse:confirmed=json.loads(response.read().decode())height=confirmed['meta']['height']print(f'Transaction confirmed in block {height}')is_confirmed=Truebreakexcepturllib.error.HTTPError:print(' Transaction status: pending')time.sleep(1)ifnotis_confirmed:print('Confirmation took too long.')else:print(f'Transaction rejected: {announce_result["message"]}')
// Wait for confirmationif('SUCCESS'===announceResult.message){consttransactionHash=facade.hashTransaction(transaction).toString();conststatusPath=`/transaction/get?hash=${transactionHash}`;console.log('Waiting for confirmation from',statusPath);letisConfirmed=false;for(letattempt=1;120>=attempt;++attempt){constresponse=awaitfetch(`${NODE_URL}${statusPath}`);if(response.ok){constconfirmed=awaitresponse.json();console.log('Transaction confirmed in block',confirmed.meta.height);isConfirmed=true;break;}console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}if(!isConfirmed)console.warn('Confirmation took too long.');}else{console.log('Transaction rejected:',announceResult.message);}
The code then waits for the transaction to be confirmed by polling the /transaction/getGET endpoint until the
transaction is included in a block.
// Retrieve the mosaicconstdefinitionPath=`/mosaic/definition?mosaicId=${mosaicId}`;console.log('Fetching mosaic information from',definitionPath);constdefinitionResponse=awaitfetch(`${NODE_URL}${definitionPath}`);constmosaicInfo=awaitdefinitionResponse.json();constproperties=Object.fromEntries(mosaicInfo.properties.map(prop=>[prop.name,prop.value]));console.log('Mosaic information:');console.log(' Creator:',mosaicInfo.creator);console.log(' Divisibility:',properties.divisibility);console.log(' Initial supply:',properties.initialSupply);console.log(' Supply mutable:',properties.supplyMutable);console.log(' Transferable:',properties.transferable);
To verify the mosaic was created successfully, the code retrieves its definition from the /mosaic/definitionGET
endpoint and displays its properties.
A successful response confirms the mosaic exists on the network with the expected properties.
Mosaic lifetime
A mosaic has no duration of its own and becomes inactive when its parent namespace expires.
Extending the root namespace keeps its mosaics usable.
See Lifetime in the Textbook.
Mosaic ID (line 5): The mosaic is identified by its fully qualified name, combining the namespace
my_namespace and a timestamped mosaic name.
Search for this name in the NEM testnet explorer to view the mosaic details.
Creation fee and transaction fee (lines 6-7): The creation fee is 10 XEM, while the transaction fee is
0.15 XEM.
Verified properties (lines 67-70): The mosaic is retrieved from the network, confirming the expected
divisibility, the initial supply of 1000, and that the mosaic is both supply mutable and transferable.