This tutorial shows how to send a mosaic, focusing on the parts that differ from a plain
XEM Transfer.
The example sends 100 units of a company:token mosaic that exists on testnet, using the default test account that
already owns some.
The same flow works for any other mosaic, as long as the signing account owns enough of it.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.NemFacadeimportNemFacadefromsymbolchain.ncimportAmountfromsymbolchain.nem.FeeCalculatorimportcalculate_transaction_feefromsymbolchain.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))RECIPIENT_ADDRESS=os.getenv('RECIPIENT_ADDRESS','TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4')MOSAIC_ID=os.getenv('MOSAIC_ID','company:token')MOSAIC_NAMESPACE,MOSAIC_NAME=MOSAIC_ID.split(':')QUANTITY=int(os.getenv('QUANTITY','100'))print(f'Sending mosaic {MOSAIC_ID}')print(f' Amount: {QUANTITY} units')facade=NemFacade('testnet')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)# Fetch the mosaic's divisibility and supplydefinition_path=f'/mosaic/definition?mosaicId={MOSAIC_ID}'print(f'Fetching mosaic definition from {definition_path}')withurllib.request.urlopen(f'{NODE_URL}{definition_path}')asresponse:definition=json.loads(response.read().decode())properties={prop['name']:prop['value']forpropindefinition['properties']}divisibility=int(properties['divisibility'])supply_path=f'/mosaic/supply?mosaicId={MOSAIC_ID}'print(f'Fetching mosaic supply from {supply_path}')withurllib.request.urlopen(f'{NODE_URL}{supply_path}')asresponse:supply=json.loads(response.read().decode())['supply']print(f' {MOSAIC_ID}: divisibility {divisibility}, supply {supply}')# Build the transactionatomic_quantity=QUANTITY*(10**divisibility)multiplier=1scaled_multiplier=multiplier*1_000_000transaction=facade.transaction_factory.create({'type':'transfer_transaction_v2','signer_public_key':signer_key_pair.public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,'recipient_address':RECIPIENT_ADDRESS,'amount':scaled_multiplier,'mosaics':[{'mosaic':{'mosaic_id':{'namespace_id':{'name':MOSAIC_NAMESPACE},'name':MOSAIC_NAME},'amount':atomic_quantity}}]})# Calculate and attach the transaction feefee=calculate_transaction_fee(transaction,{MOSAIC_ID:{'supply':supply,'divisibility':divisibility}})transaction.fee=Amount(fee)print(f' Transaction fee: {fee/1_000_000} XEM')# Sign transaction and generate final payloadsignature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)print('Built transaction:')print(json.dumps(transaction.to_json(),indent=2))# Announce the transactionannounce_path='/transaction/announce'print(f'Announcing transaction 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']:status_path=(f'/transaction/get?hash={facade.hash_transaction(transaction)}')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']}')excepturllib.error.URLErrorase:print(e.reason)
import{PrivateKey}from'symbol-sdk';import{NemFacade,NetworkTimestamp,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));constRECIPIENT_ADDRESS=process.env.RECIPIENT_ADDRESS||'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';constMOSAIC_ID=process.env.MOSAIC_ID||'company:token';const[MOSAIC_NAMESPACE,MOSAIC_NAME]=MOSAIC_ID.split(':');constQUANTITY=parseInt(process.env.QUANTITY||'100',10);console.log('Sending mosaic',MOSAIC_ID);console.log(` Amount: ${QUANTITY} units`);constfacade=newNemFacade('testnet');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);// Fetch the mosaic's divisibility and supplyconstdefinitionPath=`/mosaic/definition?mosaicId=${MOSAIC_ID}`;console.log('Fetching mosaic definition from',definitionPath);constdefinitionResponse=awaitfetch(`${NODE_URL}${definitionPath}`);constdefinition=awaitdefinitionResponse.json();constproperties=Object.fromEntries(definition.properties.map(property=>[property.name,property.value]));constdivisibility=parseInt(properties.divisibility,10);constsupplyPath=`/mosaic/supply?mosaicId=${MOSAIC_ID}`;console.log('Fetching mosaic supply from',supplyPath);constsupplyResponse=awaitfetch(`${NODE_URL}${supplyPath}`);const{supply}=awaitsupplyResponse.json();console.log(` ${MOSAIC_ID}: divisibility ${divisibility},`,`supply ${supply}`);// Build the transactionconstatomicQuantity=QUANTITY*(10**divisibility);constmultiplier=1;constscaledMultiplier=multiplier*1_000_000;consttransaction=facade.transactionFactory.create({type:'transfer_transaction_v2',signerPublicKey:signerKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,recipientAddress:RECIPIENT_ADDRESS,amount:BigInt(scaledMultiplier),mosaics:[{mosaic:{mosaicId:{namespaceId:{name:MOSAIC_NAMESPACE},name:MOSAIC_NAME},amount:BigInt(atomicQuantity)}}]});// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction,{[MOSAIC_ID]:{supply:BigInt(supply),divisibility}});transaction.fee=newmodels.Amount(fee);console.log(` Transaction fee: ${Number(fee)/1_000_000} XEM`);// Sign transaction and generate final payloadconstsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);console.log('Built transaction:');console.dir(transaction.toJson(),{colors:true,depth:null});// Announce the transactionconstannouncePath='/transaction/announce';console.log('Announcing transaction 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);}}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
Signing, announcing, and waiting for confirmation work the same as in the Transfer XEM tutorial and
are not repeated here.
Only the steps that differ are explained below.
Every mosaic transfer involves two accounts: a sender and a recipient.
The sender is the account that signs the transaction, pays the fee, and holds the mosaic being sent.
Its private key is loaded from the SIGNER_PRIVATE_KEY environment variable.
If not provided, a test key is used as default.
The recipient is the account that receives the mosaic.
Its address is loaded from the RECIPIENT_ADDRESS environment variable.
If not provided, a test address is used as default.
The default test account is pre-funded with company:token, though its balance is shared across tutorial runs and can
be exhausted.
If your transfer fails with insufficient balance, set SIGNER_PRIVATE_KEY to an account that holds the mosaic.
The mosaic to send is identified by MOSAIC_ID, its
fully qualified name in <namespace>:<mosaic_name> form,
defaulting to company:token.
Setting it lets you point the tutorial at any other mosaic the signing account owns.
QUANTITY is how much of the mosaic to transfer, in whole units, defaulting
to 100.
# 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);
Every NEM transaction needs a timestamp (when it was created) and a deadline (how long the network keeps trying to
confirm it), both in network time.
The snippet fetches the current network time from /time-sync/network-timeGET, sets timestamp to it, and sets
deadline two hours later.
The endpoint returns the time in milliseconds, so the code divides by 1000 to obtain the seconds that transactions
expect.
See Fetching Network Time in Transfer XEM for more detail, including caching
strategies.
# Fetch the mosaic's divisibility and supplydefinition_path=f'/mosaic/definition?mosaicId={MOSAIC_ID}'print(f'Fetching mosaic definition from {definition_path}')withurllib.request.urlopen(f'{NODE_URL}{definition_path}')asresponse:definition=json.loads(response.read().decode())properties={prop['name']:prop['value']forpropindefinition['properties']}divisibility=int(properties['divisibility'])supply_path=f'/mosaic/supply?mosaicId={MOSAIC_ID}'print(f'Fetching mosaic supply from {supply_path}')withurllib.request.urlopen(f'{NODE_URL}{supply_path}')asresponse:supply=json.loads(response.read().decode())['supply']print(f' {MOSAIC_ID}: divisibility {divisibility}, supply {supply}')
// Fetch the mosaic's divisibility and supplyconstdefinitionPath=`/mosaic/definition?mosaicId=${MOSAIC_ID}`;console.log('Fetching mosaic definition from',definitionPath);constdefinitionResponse=awaitfetch(`${NODE_URL}${definitionPath}`);constdefinition=awaitdefinitionResponse.json();constproperties=Object.fromEntries(definition.properties.map(property=>[property.name,property.value]));constdivisibility=parseInt(properties.divisibility,10);constsupplyPath=`/mosaic/supply?mosaicId=${MOSAIC_ID}`;console.log('Fetching mosaic supply from',supplyPath);constsupplyResponse=awaitfetch(`${NODE_URL}${supplyPath}`);const{supply}=awaitsupplyResponse.json();console.log(` ${MOSAIC_ID}: divisibility ${divisibility},`,`supply ${supply}`);
The mosaic's divisibility is used to convert the transfer quantity to
atomic units, and together with the
current supply it determines the fee.
Both are fetched from the node before building the transaction.
The /mosaic/definitionGET endpoint returns the mosaic's definition, including its properties such as
divisibility.
As with the network time, these values do not need to be fetched before every transfer.
A mosaic's divisibility is fixed at creation, and its supply changes only if the mosaic was created with a mutable
supply, so applications can fetch both values once and cache them, refreshing the supply when needed.
# Build the transactionatomic_quantity=QUANTITY*(10**divisibility)multiplier=1scaled_multiplier=multiplier*1_000_000transaction=facade.transaction_factory.create({'type':'transfer_transaction_v2','signer_public_key':signer_key_pair.public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,'recipient_address':RECIPIENT_ADDRESS,'amount':scaled_multiplier,'mosaics':[{'mosaic':{'mosaic_id':{'namespace_id':{'name':MOSAIC_NAMESPACE},'name':MOSAIC_NAME},'amount':atomic_quantity}}]})
// Build the transactionconstatomicQuantity=QUANTITY*(10**divisibility);constmultiplier=1;constscaledMultiplier=multiplier*1_000_000;consttransaction=facade.transactionFactory.create({type:'transfer_transaction_v2',signerPublicKey:signerKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,recipientAddress:RECIPIENT_ADDRESS,amount:BigInt(scaledMultiplier),mosaics:[{mosaic:{mosaicId:{namespaceId:{name:MOSAIC_NAMESPACE},name:MOSAIC_NAME},amount:BigInt(atomicQuantity)}}]});
The snippet first converts QUANTITY from whole units to atomic units using the divisibility fetched in the
previous step.
Whole to atomic units
A mosaic's divisibility, set at creation and ranging from 0 to 6, defines the conversion:
1 whole unit equals 10divisibilityatomic units.
The company:token mosaic used here has divisibility 0, so 100 = 1 and a QUANTITY of 100 is encoded as 100
atomic units.
A mosaic with divisibility 2, by contrast, would have 102 = 100 atomic units per whole unit, so the same
QUANTITY of 100 would be encoded as 10'000 atomic units.
The snippet then calls with a TransferTransactionV2 descriptor that has an extra
mosaics field, which accepts up to 10 entries.
Each entry identifies a mosaic and how much of it to send:
The quantity, given in the mosaic's atomic units (calculated above).
When mosaics are attached, the top-level amount is no longer the
XEM amount.
It becomes a multiplier applied to every listed mosaic, where 1_000_000 represents a factor of one.
Setting amount to 1_000_000 sends each mosaic at the quantity given in its entry.
Sending XEM alongside other mosaics
The top-level amount now acts as a multiplier rather than sending XEM.
To send XEM at the same time as another mosaic, add a nem:xem entry to the mosaics array.
Its quantity is the amount of XEM to send, in atomic units.
// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction,{[MOSAIC_ID]:{supply:BigInt(supply),divisibility}});transaction.fee=newmodels.Amount(fee);console.log(` Transaction fee: ${Number(fee)/1_000_000} XEM`);
The fee for a mosaic transfer depends on each mosaic's supply, divisibility, and the quantity transferred.
Rather than implement NEM's fixed fee schedule by hand, the snippet calls the SDK's
helper.
The helper reads the transferred quantities directly from the transaction built in the previous step, and takes
each mosaic's supply (in whole units) and divisibility as a second
argument, since those values are not stored in the transaction.
The returned fee is assigned to transaction.fee before signing.
See the Fees section for the full rules behind the calculation.
Signing, Announcing, and Waiting for Confirmation⚓︎
These steps work the same as in Transfer XEM and are not detailed here.
Mosaics with a levy
Some mosaics carry a levy: an additional fee paid to a third-party account on every transfer of that mosaic, on
top of the transaction fee.
The sender's account must hold enough of the levy mosaic (which may differ from the mosaic being transferred)
to cover it, or the transaction is rejected.
Using node http://libertalia.nemtest.net:7890
Sending mosaic company:token
Amount: 100 units
Fetching current network time from /time-sync/network-time
Network time: 353346165 s since the nemesis block
Fetching mosaic definition from /mosaic/definition?mosaicId=company:token
Fetching mosaic supply from /mosaic/supply?mosaicId=company:token
company:token: divisibility 0, supply 1000000
Transaction fee: 0.35 XEM
Built transaction:
{
type: 257,
version: 2,
network: 152,
timestamp: 353346165,
signerPublicKey: '462EE976890916E54FA825D26BDD0235F5EB5B6A143C199AB0AE5EE9328E08CE',
signature: 'C5BAAB71037317719398BA8643A7A3561AF0D69CEAC67757080E2625C4EAB3E0D7B960C850209CAD8E9C4EEA492FB75084AD9090DAE422624120D9C129642104',
fee: '350000',
deadline: 353353365,
recipientAddress: '5442554C4541554732435A51495355523434324857413655414B47574958484441424A5649505334',
amount: '1000000',
mosaics: [
{
mosaic: {
mosaicId: { namespaceId: { name: '636F6D70616E79' }, name: '746F6B656E' },
amount: '100'
}
}
]
}
Announcing transaction to /transaction/announce
Result: SUCCESS
Waiting for confirmation from /transaction/get?hash=FAA9346884DF9AB9CEBD21D18242FEB24559F781F94148FD069165AECB751F65
Transaction status: pending
Transaction status: pending
Transaction status: pending
Transaction status: pending
Transaction status: pending
Transaction status: pending
Transaction status: pending
Transaction status: pending
Transaction status: pending
Transaction status: pending
Transaction confirmed in block 649700
Some highlights from the output, focusing on the parts that differ from a plain XEM transfer:
Definition and supply (line 8): The mosaic company:token, with its divisibility (0) and current supply
(1000000), fetched to convert the quantity to atomic units and to calculate the fee.
Transaction fee (line 18): 350000 atomic units (0.35 XEM), derived from the mosaic's supply, divisibility, and
the quantity sent.
Amount as multiplier (line 21): With mosaics attached, amount is 1000000 (a factor of one) rather than an
amount of XEM.
Mosaics array (lines 22-29): The mosaics included in the transfer, here a single entry with a quantity of 100.
The namespace and name are hex-encoded like the address, so 636F6D70616E79 and 746F6B656E decode back to
company and token.
To see the transaction from the network's perspective, you can search for the transaction hash on the
NEM testnet explorer.
The hash is printed in the line that says Waiting for confirmation from /transaction/get?hash=....