Skip to content

Sending Mosaics with a Transfer Transaction⚓︎

INTERMEDIATE

A Transfer transaction can carry other mosaics instead of, or alongside, XEM.

This tutorial shows how to send a mosaic, focusing on the parts that differ from a plain XEM Transfer.

Transfer company:tokenAABBA->B100 company:token

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.

Prerequisites⚓︎

Before you start, make sure to:

Full Code⚓︎

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.NemFacade import NemFacade
from symbolchain.nc import Amount
from symbolchain.nem.FeeCalculator import calculate_transaction_fee
from symbolchain.nem.Network import NetworkTimestamp

NODE_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 time
    time_path = '/time-sync/network-time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        network_time = response_json['receiveTimeStamp'] // 1000
        print(f'  Network time: {network_time} s since the nemesis block')

    # Derived fields from network time
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)

    # Fetch the mosaic's divisibility and supply
    definition_path = f'/mosaic/definition?mosaicId={MOSAIC_ID}'
    print(f'Fetching mosaic definition from {definition_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{definition_path}') as response:
        definition = json.loads(response.read().decode())
    properties = {
        prop['name']: prop['value']
        for prop in definition['properties']
    }
    divisibility = int(properties['divisibility'])

    supply_path = f'/mosaic/supply?mosaicId={MOSAIC_ID}'
    print(f'Fetching mosaic supply from {supply_path}')
    with urllib.request.urlopen(f'{NODE_URL}{supply_path}') as response:
        supply = json.loads(response.read().decode())['supply']
    print(f'  {MOSAIC_ID}: divisibility {divisibility}, supply {supply}')

    # Build the transaction
    atomic_quantity = QUANTITY * (10 ** divisibility)
    multiplier = 1
    scaled_multiplier = multiplier * 1_000_000
    transaction = 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 fee
    fee = 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 payload
    signature = 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 transaction
    announce_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'
    )
    with urllib.request.urlopen(announce_request) as response:
        announce_result = json.loads(response.read().decode())
    print(f'  Result: {announce_result['message']}')

    # Wait for confirmation
    if 'SUCCESS' == announce_result['message']:
        status_path = (
            f'/transaction/get?hash={
                facade.hash_transaction(transaction)}')
        print(f'Waiting for confirmation from {status_path}')
        is_confirmed = False
        for attempt in range(120):
            try:
                with urllib.request.urlopen(
                    f'{NODE_URL}{status_path}'
                ) as response:
                    confirmed = json.loads(response.read().decode())
                    height = confirmed['meta']['height']
                    print(f'Transaction confirmed in block {height}')
                    is_confirmed = True
                    break
            except urllib.error.HTTPError:
                print('  Transaction status: pending')
            time.sleep(1)
        if not is_confirmed:
            print('Confirmation took too long.')
    else:
        print(f'Transaction rejected: {announce_result['message']}')

except urllib.error.URLError as e:
    print(e.reason)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    NemFacade,
    NetworkTimestamp,
    calculateTransactionFee,
    models
} from 'symbol-sdk/nem';

const NODE_URL = process.env.NODE_URL ||
    'http://libertalia.nemtest.net:7890';
console.log('Using node', NODE_URL);

const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new NemFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS ||
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';


const MOSAIC_ID = process.env.MOSAIC_ID || 'company:token';
const [MOSAIC_NAMESPACE, MOSAIC_NAME] = MOSAIC_ID.split(':');
const QUANTITY = parseInt(process.env.QUANTITY || '100', 10);
console.log('Sending mosaic', MOSAIC_ID);
console.log(`  Amount: ${QUANTITY} units`);

const facade = new NemFacade('testnet');

try {
    // Fetch current network time
    const timePath = '/time-sync/network-time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
    console.log('  Network time:', networkTime,
        's since the nemesis block');

    // Derived fields from network time
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);

    // Fetch the mosaic's divisibility and supply
    const definitionPath = `/mosaic/definition?mosaicId=${MOSAIC_ID}`;
    console.log('Fetching mosaic definition from', definitionPath);
    const definitionResponse =
        await fetch(`${NODE_URL}${definitionPath}`);
    const definition = await definitionResponse.json();
    const properties = Object.fromEntries(
        definition.properties.map(
            property => [property.name, property.value]));
    const divisibility = parseInt(properties.divisibility, 10);

    const supplyPath = `/mosaic/supply?mosaicId=${MOSAIC_ID}`;
    console.log('Fetching mosaic supply from', supplyPath);
    const supplyResponse = await fetch(`${NODE_URL}${supplyPath}`);
    const { supply } = await supplyResponse.json();
    console.log(`  ${MOSAIC_ID}: divisibility ${divisibility},`,
        `supply ${supply}`);

    // Build the transaction
    const atomicQuantity = QUANTITY * (10 ** divisibility);
    const multiplier = 1;
    const scaledMultiplier = multiplier * 1_000_000;
    const transaction = 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 fee
    const fee = calculateTransactionFee(transaction, {
        [MOSAIC_ID]: { supply: BigInt(supply), divisibility }
    });
    transaction.fee = new models.Amount(fee);
    console.log(`  Transaction fee: ${Number(fee) / 1_000_000} XEM`);

    // Sign transaction and generate final payload
    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built transaction:');
    console.dir(transaction.toJson(), { colors: true, depth: null });

    // Announce the transaction
    const announcePath = '/transaction/announce';
    console.log('Announcing transaction to', announcePath);
    const announceResponse = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    const announceResult = await announceResponse.json();
    console.log('  Result:', announceResult.message);

    // Wait for confirmation
    if ('SUCCESS' === announceResult.message) {
        const transactionHash = facade.hashTransaction(transaction)
            .toString();
        const statusPath = `/transaction/get?hash=${transactionHash}`;
        console.log('Waiting for confirmation from', statusPath);

        let isConfirmed = false;
        for (let attempt = 1; 120 >= attempt; ++attempt) {
            const response = await fetch(`${NODE_URL}${statusPath}`);

            if (response.ok) {
                const confirmed = await response.json();
                console.log('Transaction confirmed in block',
                    confirmed.meta.height);
                isConfirmed = true;
                break;
            }
            console.log('  Transaction status: pending');
            await new Promise(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');
}

Download source

Code Explanation⚓︎

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.

Setting Up the Accounts⚓︎

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')
const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new NemFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS ||
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';

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.

Setting Up the Mosaic⚓︎

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')
const MOSAIC_ID = process.env.MOSAIC_ID || 'company:token';
const [MOSAIC_NAMESPACE, MOSAIC_NAME] = MOSAIC_ID.split(':');
const QUANTITY = parseInt(process.env.QUANTITY || '100', 10);
console.log('Sending mosaic', MOSAIC_ID);
console.log(`  Amount: ${QUANTITY} units`);

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.

Fetching Network Time⚓︎

    # Fetch current network time
    time_path = '/time-sync/network-time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        network_time = response_json['receiveTimeStamp'] // 1000
        print(f'  Network time: {network_time} s since the nemesis block')

    # Derived fields from network time
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)
    // Fetch current network time
    const timePath = '/time-sync/network-time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
    console.log('  Network time:', networkTime,
        's since the nemesis block');

    // Derived fields from network time
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = 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-time GET, 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.

Fetching the Mosaic Definition and Supply⚓︎

    # Fetch the mosaic's divisibility and supply
    definition_path = f'/mosaic/definition?mosaicId={MOSAIC_ID}'
    print(f'Fetching mosaic definition from {definition_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{definition_path}') as response:
        definition = json.loads(response.read().decode())
    properties = {
        prop['name']: prop['value']
        for prop in definition['properties']
    }
    divisibility = int(properties['divisibility'])

    supply_path = f'/mosaic/supply?mosaicId={MOSAIC_ID}'
    print(f'Fetching mosaic supply from {supply_path}')
    with urllib.request.urlopen(f'{NODE_URL}{supply_path}') as response:
        supply = json.loads(response.read().decode())['supply']
    print(f'  {MOSAIC_ID}: divisibility {divisibility}, supply {supply}')
    // Fetch the mosaic's divisibility and supply
    const definitionPath = `/mosaic/definition?mosaicId=${MOSAIC_ID}`;
    console.log('Fetching mosaic definition from', definitionPath);
    const definitionResponse =
        await fetch(`${NODE_URL}${definitionPath}`);
    const definition = await definitionResponse.json();
    const properties = Object.fromEntries(
        definition.properties.map(
            property => [property.name, property.value]));
    const divisibility = parseInt(properties.divisibility, 10);

    const supplyPath = `/mosaic/supply?mosaicId=${MOSAIC_ID}`;
    console.log('Fetching mosaic supply from', supplyPath);
    const supplyResponse = await fetch(`${NODE_URL}${supplyPath}`);
    const { supply } = await supplyResponse.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.

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.

Building the Transaction⚓︎

    # Build the transaction
    atomic_quantity = QUANTITY * (10 ** divisibility)
    multiplier = 1
    scaled_multiplier = multiplier * 1_000_000
    transaction = 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 transaction
    const atomicQuantity = QUANTITY * (10 ** divisibility);
    const multiplier = 1;
    const scaledMultiplier = multiplier * 1_000_000;
    const transaction = 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 10divisibility atomic 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 namespace that owns the mosaic.
  • The mosaic's name.
  • 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.

Calculating the Transaction Fee⚓︎

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(
        transaction,
        {MOSAIC_ID: {'supply': supply, 'divisibility': divisibility}})
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')
    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction, {
        [MOSAIC_ID]: { supply: BigInt(supply), divisibility }
    });
    transaction.fee = new models.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.

Output⚓︎

The output shown below corresponds to a typical run of the program.

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=....

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Fetch a mosaic's divisibility /mosaic/definition GET
Fetch a mosaic's supply /mosaic/supply GET
Build a mosaic transfer , TransferTransactionV2
Calculate the transaction fee