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⚓︎

The following is the complete code listing for this tutorial. A detailed, step-by-step explanation follows in the next section.

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

NODE_URL = os.getenv('NODE_URL', 'http://libertalia.nemtest.net:7890')
print(f'Using node {NODE_URL}')


# Helper function to announce a transaction
def announce_transaction(payload, label):
    announce_path = '/transaction/announce'
    print(f'Announcing {label} to {announce_path}')
    request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    with urllib.request.urlopen(request) as announce_response:
        result = json.loads(announce_response.read().decode())
    print(f'  Result: {result["message"]}')
    return result['message']


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    status_path = f'/transaction/get?hash={tx_hash}'
    print(f'Waiting for {label} confirmation from {status_path}')
    is_confirmed = False
    for _ in range(120):
        try:
            with urllib.request.urlopen(
                f'{NODE_URL}{status_path}'
            ) as status_response:
                confirmed = json.loads(status_response.read().decode())
                height = confirmed['meta']['height']
                print(f'{label} confirmed in block {height}')
                is_confirmed = True
                break
        except urllib.error.HTTPError:
            print('  Transaction status: pending')
        time.sleep(1)
    if not is_confirmed:
        print(f'{label} confirmation took too long.')



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 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.create_transaction_from_descriptor({
        'type': 'transfer_transaction_v2',
        'recipient_address': RECIPIENT_ADDRESS,
        'amount': scaled_multiplier,
        'mosaics': [{
            'mosaic': {
                'mosaic_id': {
                    'namespace_id': {'name': MOSAIC_NAMESPACE},
                    'name': MOSAIC_NAME
                },
                'amount': atomic_quantity
            }
        }]
    }, signer_key_pair.public_key, 0, 2 * 60 * 60)

    # 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_result = announce_transaction(json_payload, 'transaction')

    # Wait for confirmation
    if 'SUCCESS' == announce_result:
        transaction_hash = facade.hash_transaction(transaction)
        print(f'Transaction hash: {transaction_hash}')
        wait_for_confirmation(transaction_hash, 'transaction')
    else:
        print(f'Transaction rejected: {announce_result}')

except Exception as error:
    print(error)

Download source

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

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

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    const announcePath = '/transaction/announce';
    console.log(`Announcing ${label} to ${announcePath}`);
    const response = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    if (!response.ok)
        throw new Error(`HTTP ${response.status}`);
    const result = await response.json();
    console.log('  Result:', result.message);
    return result.message;
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    const statusPath = `/transaction/get?hash=${transactionHash}`;
    console.log(`Waiting for ${label} confirmation from`, statusPath);
    let isConfirmed = false;
    for (let attempt = 1; 120 >= attempt; ++attempt) {
        const response = await fetch(`${NODE_URL}${statusPath}`);
        if (!response.ok) {
            console.log('  Transaction status: pending');
            await new Promise(resolve => { setTimeout(resolve, 1000); });
        } else {
            const confirmed = await response.json();
            console.log(`${label} confirmed in block`,
                confirmed.meta.height);
            isConfirmed = true;
            break;
        }
    }
    if (!isConfirmed)
        console.warn(`${label} confirmation took too long.`);
}


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 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.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV2Descriptor(
            new Address(RECIPIENT_ADDRESS),
            new models.Amount(BigInt(scaledMultiplier)),
            undefined,
            [new descriptors.SizePrefixedMosaicDescriptor(
                new descriptors.MosaicDescriptor(
                    new descriptors.MosaicIdDescriptor(
                        new descriptors.NamespaceIdDescriptor(
                            MOSAIC_NAMESPACE),
                        MOSAIC_NAME),
                    new models.Amount(BigInt(atomicQuantity))))]),
        signerKeyPair.publicKey,
        0n,
        2 * 60 * 60);

    // 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 announceResult = await announceTransaction(
        jsonPayload, 'transaction');

    // Wait for confirmation
    if ('SUCCESS' === announceResult) {
        const transactionHash = facade.hashTransaction(transaction)
            .toString();
        console.log('Transaction hash:', transactionHash);
        await waitForConfirmation(transactionHash, 'transaction');
    } else {
        console.log('Transaction rejected:', announceResult);
    }

} catch (e) {
    console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown');
}

Download source

//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.3

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.NemFacade;
import org.symbol.sdk.nem.Address;
import org.symbol.sdk.nem.FeeCalculator;
import org.symbol.sdk.nem.FeeCalculator.MosaicInformation;
import org.symbol.sdk.nem.KeyPair;
import org.symbol.sdk.nem.NemTransactionFactory;
import org.symbol.sdk.nem.descriptors.MosaicDescriptor;
import org.symbol.sdk.nem.descriptors.MosaicIdDescriptor;
import org.symbol.sdk.nem.descriptors.NamespaceIdDescriptor;
import org.symbol.sdk.nem.descriptors.SizePrefixedMosaicDescriptor;
import org.symbol.sdk.nem.descriptors.TransferTransactionV2Descriptor;
import org.symbol.sdk.nem.models.Amount;
import org.symbol.sdk.nem.models.Transaction;

public final class TransferMosaics {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

    private static final HttpClient HTTP_CLIENT =
        HttpClient.newHttpClient();

    private final String nodeUrl = System.getenv().getOrDefault(
        "NODE_URL", "http://libertalia.nemtest.net:7890");

    private final NemFacade facade = new NemFacade("testnet");

    private String announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        final String announcePath = "/transaction/announce";
        System.out.printf("Announcing %s to %s%n", label, announcePath);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + announcePath))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        if (2 != response.statusCode() / 100)
            throw new IOException("HTTP " + response.statusCode());
        final String result = JSON_MAPPER.readTree(response.body())
            .get("message").asText();
        System.out.printf("  Result: %s%n", result);
        return result;
    }

    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        final String statusPath = String.format(
            "/transaction/get?hash=%s", transactionHash);
        System.out.printf("Waiting for %s confirmation from %s%n",
            label, statusPath);
        boolean isConfirmed = false;
        for (int attempt = 1; 120 >= attempt; ++attempt) {
            final HttpRequest request = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> response = HTTP_CLIENT.send(
                request, BodyHandlers.ofString());
            if (2 != response.statusCode() / 100) {
                System.out.println("  Transaction status: pending");
                Thread.sleep(1000);
            } else {
                final JsonNode confirmed = JSON_MAPPER.readTree(
                    response.body());
                System.out.printf("%s confirmed in block %s%n", label,
                    confirmed.get("meta").get("height").asText());
                isConfirmed = true;
                break;
            }
        }
        if (!isConfirmed)
            System.out.printf("%s confirmation took too long.%n", label);
    }

    public static void main(final String[] args) {
        try {
            new TransferMosaics().run();
        } catch (final Exception ex) {
            System.out.println(null == ex.getMessage()
                ? ex.toString()
                : ex.getMessage());
        }
    }

    private void run() throws IOException, InterruptedException {
        System.out.printf("Using node %s%n", nodeUrl);


        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));
        final String recipientAddress = System.getenv().getOrDefault(
            "RECIPIENT_ADDRESS",
            "TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4");


        final String mosaicId = System.getenv().getOrDefault(
            "MOSAIC_ID", "company:token");
        final String[] mosaicIdParts = mosaicId.split(":");
        final String mosaicNamespace = mosaicIdParts[0];
        final String mosaicName = mosaicIdParts[1];
        final int quantity = Integer.parseInt(
            System.getenv().getOrDefault("QUANTITY", "100"));
        System.out.printf("Sending mosaic %s%n", mosaicId);
        System.out.printf("  Amount: %d units%n", quantity);


        // Fetch the mosaic's divisibility and supply
        final String definitionPath = String.format(
            "/mosaic/definition?mosaicId=%s", mosaicId);
        System.out.printf("Fetching mosaic definition from %s%n",
            definitionPath);
        final HttpRequest definitionRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + definitionPath)).GET().build();
        final HttpResponse<String> definitionResponse = HTTP_CLIENT.send(
            definitionRequest, BodyHandlers.ofString());
        final JsonNode definition = JSON_MAPPER.readTree(
            definitionResponse.body());
        int divisibility = 0;
        for (final JsonNode property : definition.get("properties")) {
            if ("divisibility".equals(property.get("name").asText()))
                divisibility = property.get("value").asInt();
        }
        final String supplyPath = String.format(
            "/mosaic/supply?mosaicId=%s", mosaicId);
        System.out.printf("Fetching mosaic supply from %s%n", supplyPath);
        final HttpRequest supplyRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + supplyPath)).GET().build();
        final HttpResponse<String> supplyResponse = HTTP_CLIENT.send(
            supplyRequest, BodyHandlers.ofString());
        final long supply = JSON_MAPPER.readTree(supplyResponse.body())
            .get("supply").asLong();
        System.out.printf("  %s: divisibility %d, supply %d%n",
            mosaicId, divisibility, supply);

        // Build the transaction
        final long atomicQuantity = Math.round(
            quantity * Math.pow(10, divisibility));
        final long scaledMultiplier = 1_000_000;
        final MosaicIdDescriptor mosaicIdDescriptor =
            new MosaicIdDescriptor(
                new NamespaceIdDescriptor(mosaicNamespace.getBytes(UTF_8)),
                mosaicName.getBytes(UTF_8));
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV2Descriptor(
                    new Address(recipientAddress),
                    new Amount(scaledMultiplier),
                    null,
                    List.of(new SizePrefixedMosaicDescriptor(
                        new MosaicDescriptor(
                            mosaicIdDescriptor,
                            new Amount(atomicQuantity))))),
                signerKeyPair.getPublicKey(),
                0,
                2 * 60 * 60);

        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction, Map.of(mosaicId,
                new MosaicInformation(supply, divisibility)));
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) fee / 1_000_000);

        // Sign transaction and generate final payload
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = NemTransactionFactory.attachSignature(
            transaction, signature);
        System.out.println("Built transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        // Announce the transaction
        final String announceResult = announceTransaction(
            jsonPayload, "transaction");

        // Wait for confirmation
        if ("SUCCESS".equals(announceResult)) {
            final String transactionHash = facade.hashTransaction(
                transaction).toString();
            System.out.printf("Transaction hash: %s%n", transactionHash);
            waitForConfirmation(transactionHash, "transaction");
        } else {
            System.out.printf(
                "Transaction rejected: %s%n", announceResult);
        }

    }
}

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';
        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));
        final String recipientAddress = System.getenv().getOrDefault(
            "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`);
        final String mosaicId = System.getenv().getOrDefault(
            "MOSAIC_ID", "company:token");
        final String[] mosaicIdParts = mosaicId.split(":");
        final String mosaicNamespace = mosaicIdParts[0];
        final String mosaicName = mosaicIdParts[1];
        final int quantity = Integer.parseInt(
            System.getenv().getOrDefault("QUANTITY", "100"));
        System.out.printf("Sending mosaic %s%n", mosaicId);
        System.out.printf("  Amount: %d units%n", quantity);

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 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}`);
        // Fetch the mosaic's divisibility and supply
        final String definitionPath = String.format(
            "/mosaic/definition?mosaicId=%s", mosaicId);
        System.out.printf("Fetching mosaic definition from %s%n",
            definitionPath);
        final HttpRequest definitionRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + definitionPath)).GET().build();
        final HttpResponse<String> definitionResponse = HTTP_CLIENT.send(
            definitionRequest, BodyHandlers.ofString());
        final JsonNode definition = JSON_MAPPER.readTree(
            definitionResponse.body());
        int divisibility = 0;
        for (final JsonNode property : definition.get("properties")) {
            if ("divisibility".equals(property.get("name").asText()))
                divisibility = property.get("value").asInt();
        }
        final String supplyPath = String.format(
            "/mosaic/supply?mosaicId=%s", mosaicId);
        System.out.printf("Fetching mosaic supply from %s%n", supplyPath);
        final HttpRequest supplyRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + supplyPath)).GET().build();
        final HttpResponse<String> supplyResponse = HTTP_CLIENT.send(
            supplyRequest, BodyHandlers.ofString());
        final long supply = JSON_MAPPER.readTree(supplyResponse.body())
            .get("supply").asLong();
        System.out.printf("  %s: divisibility %d, supply %d%n",
            mosaicId, divisibility, 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.

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.create_transaction_from_descriptor({
        'type': 'transfer_transaction_v2',
        'recipient_address': RECIPIENT_ADDRESS,
        'amount': scaled_multiplier,
        'mosaics': [{
            'mosaic': {
                'mosaic_id': {
                    'namespace_id': {'name': MOSAIC_NAMESPACE},
                    'name': MOSAIC_NAME
                },
                'amount': atomic_quantity
            }
        }]
    }, signer_key_pair.public_key, 0, 2 * 60 * 60)
    // Build the transaction
    const atomicQuantity = QUANTITY * (10 ** divisibility);
    const multiplier = 1;
    const scaledMultiplier = multiplier * 1_000_000;
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV2Descriptor(
            new Address(RECIPIENT_ADDRESS),
            new models.Amount(BigInt(scaledMultiplier)),
            undefined,
            [new descriptors.SizePrefixedMosaicDescriptor(
                new descriptors.MosaicDescriptor(
                    new descriptors.MosaicIdDescriptor(
                        new descriptors.NamespaceIdDescriptor(
                            MOSAIC_NAMESPACE),
                        MOSAIC_NAME),
                    new models.Amount(BigInt(atomicQuantity))))]),
        signerKeyPair.publicKey,
        0n,
        2 * 60 * 60);
        // Build the transaction
        final long atomicQuantity = Math.round(
            quantity * Math.pow(10, divisibility));
        final long scaledMultiplier = 1_000_000;
        final MosaicIdDescriptor mosaicIdDescriptor =
            new MosaicIdDescriptor(
                new NamespaceIdDescriptor(mosaicNamespace.getBytes(UTF_8)),
                mosaicName.getBytes(UTF_8));
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV2Descriptor(
                    new Address(recipientAddress),
                    new Amount(scaledMultiplier),
                    null,
                    List.of(new SizePrefixedMosaicDescriptor(
                        new MosaicDescriptor(
                            mosaicIdDescriptor,
                            new Amount(atomicQuantity))))),
                signerKeyPair.getPublicKey(),
                0,
                2 * 60 * 60);

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 creates a TransferTransactionV2 from the transaction's descriptor, including a mosaics field that 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. The facade adds the signer and derives the timestamp and deadline from the two-hour deadline duration.

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`);
        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction, Map.of(mosaicId,
                new MosaicInformation(supply, divisibility)));
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) fee / 1_000_000);

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 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
Transaction hash: FAA9346884DF9AB9CEBD21D18242FEB24559F781F94148FD069165AECB751F65
Waiting for transaction 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 6): 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 16): 350000 atomic units (0.35 XEM), derived from the mosaic's supply, divisibility, and the quantity sent.

  • Amount as multiplier (line 19): With mosaics attached, amount is 1000000 (a factor of one) rather than an amount of XEM.

  • Mosaics array (lines 20-27): 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 starts with Transaction 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