Skip to content

Creating a Mosaic with a Levy⚓︎

ADVANCED

A mosaic can include an optional levy, a fee charged to the sender on every transfer and credited to a designated account.

A typical use of levies is funding the account behind an asset, for example by charging a commission or a royalty on every transfer.

This tutorial shows how to create a mosaic with a levy.

Prerequisites⚓︎

Before you start, make sure to:

Additionally, review the Creating a Mosaic tutorial to understand how a mosaic definition is built, announced, and confirmed.

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_mosaic_rental_fee,
    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))

facade = NemFacade('testnet')
signer_address = facade.network.public_key_to_address(
    signer_key_pair.public_key)
print(f'Signer address: {signer_address}')

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

try:
    # Describe the levy
    LEVY_RECIPIENT = os.getenv(
        'LEVY_RECIPIENT',
        'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4')

    levy = {
        'transfer_fee_type': 'absolute',
        'recipient_address': LEVY_RECIPIENT,
        'mosaic_id': {
            'namespace_id': {'name': 'nem'},
            'name': 'xem'
        },
        'fee': 1_000_000
    }
    levy_mosaic_id = levy['mosaic_id']
    print('Levy:')
    print(f'  Type: {levy["transfer_fee_type"]}')
    print(f'  Recipient: {levy["recipient_address"]}')
    print(f'  Mosaic: {levy_mosaic_id["namespace_id"]["name"]}:'
        f'{levy_mosaic_id["name"]}')
    print(f'  Fee: {levy["fee"]}')

    # Build the mosaic definition transaction
    rental_fee = calculate_mosaic_rental_fee()
    print(f'  Mosaic creation fee: {rental_fee / 1_000_000} XEM')

    transaction = facade.create_transaction_from_descriptor({
        'type': 'mosaic_definition_transaction_v1',
        'rental_fee_sink': 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',
        'rental_fee': rental_fee,
        'mosaic_definition': {
            'owner_public_key': signer_key_pair.public_key,
            'id': {
                'namespace_id': {'name': namespace_name},
                'name': mosaic_name
            },
            'description': 'My tutorial mosaic with a levy',
            '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'}}
            ],
            'levy': levy
        }
    }, signer_key_pair.public_key, 0, 2 * 60 * 60)

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')

    # Sign, announce and wait for confirmation
    signature = 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))

    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    announce_result = announce_transaction(
        json_payload, 'mosaic definition')
    if 'SUCCESS' == announce_result:
        wait_for_confirmation(transaction_hash, 'mosaic definition')
    else:
        print(f'Transaction rejected: {announce_result}')

    # Retrieve the levy
    definition_path = f'/mosaic/definition?mosaicId={mosaic_id}'
    print(f'Fetching mosaic information from {definition_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{definition_path}'
    ) as response:
        mosaic_info = json.loads(response.read().decode())
        levy_info = mosaic_info['levy']
        levy_mosaic_id = levy_info['mosaicId']
        levy_type = 'absolute' if 1 == levy_info['type'] else 'percentile'
        print('Levy information:')
        print(f'  Type: {levy_type}')
        print(f'  Recipient: {levy_info["recipient"]}')
        print(f'  Mosaic: '
            f'{levy_mosaic_id["namespaceId"]}:{levy_mosaic_id["name"]}')
        print(f'  Fee: {levy_info["fee"]}')

except Exception as error:
    print(error)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    Address,
    NemFacade,
    calculateMosaicRentalFee,
    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 facade = new NemFacade('testnet');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer address:', signerAddress.toString());

const namespaceName = process.env.NAMESPACE || 'my_namespace';
const mosaicName = process.env.MOSAIC ||
    `token_${Math.floor(Date.now() / 1000)}`;
const mosaicId = `${namespaceName}:${mosaicName}`;
console.log('Creating mosaic:', mosaicId);

try {
    // Describe the levy
    const LEVY_RECIPIENT = process.env.LEVY_RECIPIENT ||
        'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';
    const levyFee = 1_000_000;
    const levy = new descriptors.MosaicLevyDescriptor(
        models.MosaicTransferFeeType.ABSOLUTE,
        new Address(LEVY_RECIPIENT),
        new descriptors.MosaicIdDescriptor(
            new descriptors.NamespaceIdDescriptor('nem'), 'xem'),
        new models.Amount(BigInt(levyFee)));
    console.log('Levy:');
    console.log('  Type: absolute');
    console.log('  Recipient:', LEVY_RECIPIENT);
    console.log('  Mosaic: nem:xem');
    console.log('  Fee:', levyFee);

    // Build the mosaic definition transaction
    const rentalFee = calculateMosaicRentalFee();
    console.log('  Mosaic creation fee:',
        `${Number(rentalFee) / 1_000_000} XEM`);

    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.MosaicDefinitionTransactionV1Descriptor(
            new descriptors.MosaicDefinitionDescriptor(
                signerKeyPair.publicKey,
                new descriptors.MosaicIdDescriptor(
                    new descriptors.NamespaceIdDescriptor(namespaceName),
                    mosaicName),
                'My tutorial mosaic with a levy',
                [
                    ['divisibility', '2'],
                    ['initialSupply', '1000'],
                    ['supplyMutable', 'true'],
                    ['transferable', 'true']
                ].map(([name, value]) =>
                    new descriptors.SizePrefixedMosaicPropertyDescriptor(
                        new descriptors.MosaicPropertyDescriptor(
                            name, value))),
                levy),
            new Address('TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC'),
            new models.Amount(rentalFee)),
        signerKeyPair.publicKey,
        0n,
        2 * 60 * 60);

    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log('  Transaction fee:', `${Number(fee) / 1_000_000} XEM`);

    // Sign, announce and wait for confirmation
    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built mosaic definition transaction:');
    console.dir(transaction.toJson(), { colors: true });

    const transactionHash = facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    const announceResult = await announceTransaction(
        jsonPayload, 'mosaic definition');
    if ('SUCCESS' === announceResult)
        await waitForConfirmation(transactionHash, 'mosaic definition');
    else
        console.log('Transaction rejected:', announceResult);

    // Retrieve the levy
    const definitionPath = `/mosaic/definition?mosaicId=${mosaicId}`;
    console.log('Fetching mosaic information from', definitionPath);
    const definitionResponse = await fetch(
        `${NODE_URL}${definitionPath}`);
    const mosaicInfo = await definitionResponse.json();
    const levyInfo = mosaicInfo.levy;
    const levyMosaicId = levyInfo.mosaicId;
    const levyType = 1 === levyInfo.type ? 'absolute' : 'percentile';
    console.log('Levy information:');
    console.log('  Type:', levyType);
    console.log('  Recipient:', levyInfo.recipient);
    console.log('  Mosaic:',
        `${levyMosaicId.namespaceId}:${levyMosaicId.name}`);
    console.log('  Fee:', levyInfo.fee);

} 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.KeyPair;
import org.symbol.sdk.nem.NemTransactionFactory;
import org.symbol.sdk.nem.descriptors.*;
import org.symbol.sdk.nem.models.Amount;
import org.symbol.sdk.nem.models.MosaicTransferFeeType;
import org.symbol.sdk.nem.models.Transaction;

public final class MosaicLevy {
    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 MosaicLevy().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 privateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(privateKey));
        System.out.printf("Signer address: %s%n",
            facade.network.publicKeyToAddress(
                signerKeyPair.getPublicKey()));
        final String namespaceName = System.getenv().getOrDefault(
            "NAMESPACE", "my_namespace");
        final String mosaicName = System.getenv().getOrDefault(
            "MOSAIC", "token_" + System.currentTimeMillis() / 1000);
        final String mosaicId = namespaceName + ":" + mosaicName;
        System.out.printf("Creating mosaic: %s%n", mosaicId);

        // Describe the levy
        final String levyRecipient = System.getenv().getOrDefault(
            "LEVY_RECIPIENT",
            "TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4");
        final long levyFee = 1_000_000;
        final MosaicLevyDescriptor levy = new MosaicLevyDescriptor(
            MosaicTransferFeeType.ABSOLUTE,
            new Address(levyRecipient),
            new MosaicIdDescriptor(
                new NamespaceIdDescriptor("nem".getBytes(UTF_8)),
                "xem".getBytes(UTF_8)),
            new Amount(levyFee));
        System.out.println("Levy:");
        System.out.println("  Type: absolute");
        System.out.printf("  Recipient: %s%n", levyRecipient);
        System.out.println("  Mosaic: nem:xem");
        System.out.printf("  Fee: %d%n", levyFee);

        // Build the mosaic definition transaction
        final MosaicDefinitionDescriptor mosaicDefinition =
            new MosaicDefinitionDescriptor(
                signerKeyPair.getPublicKey(),
                new MosaicIdDescriptor(
                    new NamespaceIdDescriptor(
                        namespaceName.getBytes(UTF_8)),
                    mosaicName.getBytes(UTF_8)),
                "My tutorial mosaic with a levy".getBytes(UTF_8),
                List.of(
                    Map.entry("divisibility", "2"),
                    Map.entry("initialSupply", "1000"),
                    Map.entry("supplyMutable", "true"),
                    Map.entry("transferable", "true"))
                    .stream()
                    .map(property ->
                        new SizePrefixedMosaicPropertyDescriptor(
                            new MosaicPropertyDescriptor(
                                property.getKey().getBytes(UTF_8),
                                property.getValue().getBytes(UTF_8))))
                    .toList(),
                levy);
        final long rentalFee = FeeCalculator.calculateMosaicRentalFee();
        System.out.printf("  Mosaic creation fee: %s XEM%n",
            (double) rentalFee / 1_000_000);
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new MosaicDefinitionTransactionV1Descriptor(
                    mosaicDefinition,
                    new Address(
                        "TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC"),
                    new Amount(rentalFee)),
                signerKeyPair.getPublicKey(), 0, 2 * 60 * 60);

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

        // Sign, announce and wait for confirmation
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String payload = NemTransactionFactory.attachSignature(
            transaction, signature);
        System.out.println("Built mosaic definition transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));
        final String transactionHash = facade.hashTransaction(transaction)
            .toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);
        final String result = announceTransaction(
            payload, "mosaic definition");
        if ("SUCCESS".equals(result)) {
            waitForConfirmation(
                transactionHash, "mosaic definition");
        } else {
            System.out.printf("Transaction rejected: %s%n", result);
        }

        // Retrieve the levy
        final String path = "/mosaic/definition?mosaicId=" + mosaicId;
        System.out.printf("Fetching mosaic information from %s%n", path);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + path)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        final JsonNode info = JSON_MAPPER.readTree(response.body());
        final JsonNode levyInfo = info.get("levy");
        final JsonNode levyMosaicId = levyInfo.get("mosaicId");
        System.out.println("Levy information:");
        System.out.printf("  Type: %s%n",
            1 == levyInfo.get("type").asInt()
                ? "absolute" : "percentile");
        System.out.printf("  Recipient: %s%n",
            levyInfo.get("recipient").asText());
        System.out.printf("  Mosaic: %s:%s%n",
            levyMosaicId.get("namespaceId").asText(),
            levyMosaicId.get("name").asText());
        System.out.printf("  Fee: %s%n", levyInfo.get("fee").asText());

    }
}

Download source

Code Explanation⚓︎

Setting Up the Account and the Mosaic⚓︎

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

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

const facade = new NemFacade('testnet');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer address:', signerAddress.toString());

const namespaceName = process.env.NAMESPACE || 'my_namespace';
const mosaicName = process.env.MOSAIC ||
    `token_${Math.floor(Date.now() / 1000)}`;
const mosaicId = `${namespaceName}:${mosaicName}`;
console.log('Creating mosaic:', mosaicId);
        final String privateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(privateKey));
        System.out.printf("Signer address: %s%n",
            facade.network.publicKeyToAddress(
                signerKeyPair.getPublicKey()));
        final String namespaceName = System.getenv().getOrDefault(
            "NAMESPACE", "my_namespace");
        final String mosaicName = System.getenv().getOrDefault(
            "MOSAIC", "token_" + System.currentTimeMillis() / 1000);
        final String mosaicId = namespaceName + ":" + mosaicName;
        System.out.printf("Creating mosaic: %s%n", mosaicId);

The snippet reads the signer's private key from the SIGNER_PRIVATE_KEY environment variable, which defaults to a test key if not set. This account signs the transaction and becomes the owner of the mosaic, so it must also own the namespace that will hold it. The mosaic identifier is assembled from that 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.

Describing the Levy⚓︎

    # Describe the levy
    LEVY_RECIPIENT = os.getenv(
        'LEVY_RECIPIENT',
        'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4')

    levy = {
        'transfer_fee_type': 'absolute',
        'recipient_address': LEVY_RECIPIENT,
        'mosaic_id': {
            'namespace_id': {'name': 'nem'},
            'name': 'xem'
        },
        'fee': 1_000_000
    }
    levy_mosaic_id = levy['mosaic_id']
    print('Levy:')
    print(f'  Type: {levy["transfer_fee_type"]}')
    print(f'  Recipient: {levy["recipient_address"]}')
    print(f'  Mosaic: {levy_mosaic_id["namespace_id"]["name"]}:'
        f'{levy_mosaic_id["name"]}')
    print(f'  Fee: {levy["fee"]}')
    // Describe the levy
    const LEVY_RECIPIENT = process.env.LEVY_RECIPIENT ||
        'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';
    const levyFee = 1_000_000;
    const levy = new descriptors.MosaicLevyDescriptor(
        models.MosaicTransferFeeType.ABSOLUTE,
        new Address(LEVY_RECIPIENT),
        new descriptors.MosaicIdDescriptor(
            new descriptors.NamespaceIdDescriptor('nem'), 'xem'),
        new models.Amount(BigInt(levyFee)));
    console.log('Levy:');
    console.log('  Type: absolute');
    console.log('  Recipient:', LEVY_RECIPIENT);
    console.log('  Mosaic: nem:xem');
    console.log('  Fee:', levyFee);
        // Describe the levy
        final String levyRecipient = System.getenv().getOrDefault(
            "LEVY_RECIPIENT",
            "TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4");
        final long levyFee = 1_000_000;
        final MosaicLevyDescriptor levy = new MosaicLevyDescriptor(
            MosaicTransferFeeType.ABSOLUTE,
            new Address(levyRecipient),
            new MosaicIdDescriptor(
                new NamespaceIdDescriptor("nem".getBytes(UTF_8)),
                "xem".getBytes(UTF_8)),
            new Amount(levyFee));
        System.out.println("Levy:");
        System.out.println("  Type: absolute");
        System.out.printf("  Recipient: %s%n", levyRecipient);
        System.out.println("  Mosaic: nem:xem");
        System.out.printf("  Fee: %d%n", levyFee);

The levy is a MosaicLevy structure with four fields:

  • : How the levy amount is calculated:

    • absolute: A fixed quantity charged on every transfer, regardless of the amount transferred.
    • percentile: A quantity proportional to the amount transferred.

    This tutorial uses an absolute levy, so every transfer is charged the same amount.

  • : The account credited with the levy on every transfer. It can be the mosaic creator or any other account.

  • : The mosaic in which the levy is paid. This tutorial charges the levy in nem:xem, so senders pay in the network currency.

    The levy can also be paid in the mosaic being defined itself. Any other levy mosaic must already exist on the network and be transferable.

  • : The levy amount. For an absolute levy, it is expressed in the atomic units of the levy mosaic. Because nem:xem has a divisibility of 6, a value of 1'000'000 charges 1 XEM per transfer.

    For a percentile levy, the fee is interpreted in basis points instead: a fee of 100 charges 1% of the amount transferred. See the percentile levy calculation in the Textbook for the full rules.

Attaching the Levy to the Mosaic Definition⚓︎

    # Build the mosaic definition transaction
    rental_fee = calculate_mosaic_rental_fee()
    print(f'  Mosaic creation fee: {rental_fee / 1_000_000} XEM')

    transaction = facade.create_transaction_from_descriptor({
        'type': 'mosaic_definition_transaction_v1',
        'rental_fee_sink': 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',
        'rental_fee': rental_fee,
        'mosaic_definition': {
            'owner_public_key': signer_key_pair.public_key,
            'id': {
                'namespace_id': {'name': namespace_name},
                'name': mosaic_name
            },
            'description': 'My tutorial mosaic with a levy',
            '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'}}
            ],
            'levy': levy
        }
    }, signer_key_pair.public_key, 0, 2 * 60 * 60)

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')
    // Build the mosaic definition transaction
    const rentalFee = calculateMosaicRentalFee();
    console.log('  Mosaic creation fee:',
        `${Number(rentalFee) / 1_000_000} XEM`);

    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.MosaicDefinitionTransactionV1Descriptor(
            new descriptors.MosaicDefinitionDescriptor(
                signerKeyPair.publicKey,
                new descriptors.MosaicIdDescriptor(
                    new descriptors.NamespaceIdDescriptor(namespaceName),
                    mosaicName),
                'My tutorial mosaic with a levy',
                [
                    ['divisibility', '2'],
                    ['initialSupply', '1000'],
                    ['supplyMutable', 'true'],
                    ['transferable', 'true']
                ].map(([name, value]) =>
                    new descriptors.SizePrefixedMosaicPropertyDescriptor(
                        new descriptors.MosaicPropertyDescriptor(
                            name, value))),
                levy),
            new Address('TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC'),
            new models.Amount(rentalFee)),
        signerKeyPair.publicKey,
        0n,
        2 * 60 * 60);

    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log('  Transaction fee:', `${Number(fee) / 1_000_000} XEM`);
        // Build the mosaic definition transaction
        final MosaicDefinitionDescriptor mosaicDefinition =
            new MosaicDefinitionDescriptor(
                signerKeyPair.getPublicKey(),
                new MosaicIdDescriptor(
                    new NamespaceIdDescriptor(
                        namespaceName.getBytes(UTF_8)),
                    mosaicName.getBytes(UTF_8)),
                "My tutorial mosaic with a levy".getBytes(UTF_8),
                List.of(
                    Map.entry("divisibility", "2"),
                    Map.entry("initialSupply", "1000"),
                    Map.entry("supplyMutable", "true"),
                    Map.entry("transferable", "true"))
                    .stream()
                    .map(property ->
                        new SizePrefixedMosaicPropertyDescriptor(
                            new MosaicPropertyDescriptor(
                                property.getKey().getBytes(UTF_8),
                                property.getValue().getBytes(UTF_8))))
                    .toList(),
                levy);
        final long rentalFee = FeeCalculator.calculateMosaicRentalFee();
        System.out.printf("  Mosaic creation fee: %s XEM%n",
            (double) rentalFee / 1_000_000);
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new MosaicDefinitionTransactionV1Descriptor(
                    mosaicDefinition,
                    new Address(
                        "TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC"),
                    new Amount(rentalFee)),
                signerKeyPair.getPublicKey(), 0, 2 * 60 * 60);

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

A levy is part of the mosaic definition, so it is set with the same MosaicDefinitionTransactionV1 used in Creating a Mosaic. This tutorial reuses the same transaction, with the levy added to the field. The facade adds the signer and derives the timestamp and deadline from the two-hour deadline duration.

The creation fee is 10 XEM, and the transaction fee is a fixed 0.15 XEM, as shown in the fee schedule.

Submitting the Mosaic Definition⚓︎

    # Sign, announce and wait for confirmation
    signature = 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))

    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    announce_result = announce_transaction(
        json_payload, 'mosaic definition')
    if 'SUCCESS' == announce_result:
        wait_for_confirmation(transaction_hash, 'mosaic definition')
    else:
        print(f'Transaction rejected: {announce_result}')
    // Sign, announce and wait for confirmation
    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built mosaic definition transaction:');
    console.dir(transaction.toJson(), { colors: true });

    const transactionHash = facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    const announceResult = await announceTransaction(
        jsonPayload, 'mosaic definition');
    if ('SUCCESS' === announceResult)
        await waitForConfirmation(transactionHash, 'mosaic definition');
    else
        console.log('Transaction rejected:', announceResult);
        // Sign, announce and wait for confirmation
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String payload = NemTransactionFactory.attachSignature(
            transaction, signature);
        System.out.println("Built mosaic definition transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));
        final String transactionHash = facade.hashTransaction(transaction)
            .toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);
        final String result = announceTransaction(
            payload, "mosaic definition");
        if ("SUCCESS".equals(result)) {
            waitForConfirmation(
                transactionHash, "mosaic definition");
        } else {
            System.out.printf("Transaction rejected: %s%n", result);
        }

The transaction is then signed, announced, and confirmed following the same process as in the Transfer XEM tutorial.

Verifying the Levy⚓︎

    # Retrieve the levy
    definition_path = f'/mosaic/definition?mosaicId={mosaic_id}'
    print(f'Fetching mosaic information from {definition_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{definition_path}'
    ) as response:
        mosaic_info = json.loads(response.read().decode())
        levy_info = mosaic_info['levy']
        levy_mosaic_id = levy_info['mosaicId']
        levy_type = 'absolute' if 1 == levy_info['type'] else 'percentile'
        print('Levy information:')
        print(f'  Type: {levy_type}')
        print(f'  Recipient: {levy_info["recipient"]}')
        print(f'  Mosaic: '
            f'{levy_mosaic_id["namespaceId"]}:{levy_mosaic_id["name"]}')
        print(f'  Fee: {levy_info["fee"]}')
    // Retrieve the levy
    const definitionPath = `/mosaic/definition?mosaicId=${mosaicId}`;
    console.log('Fetching mosaic information from', definitionPath);
    const definitionResponse = await fetch(
        `${NODE_URL}${definitionPath}`);
    const mosaicInfo = await definitionResponse.json();
    const levyInfo = mosaicInfo.levy;
    const levyMosaicId = levyInfo.mosaicId;
    const levyType = 1 === levyInfo.type ? 'absolute' : 'percentile';
    console.log('Levy information:');
    console.log('  Type:', levyType);
    console.log('  Recipient:', levyInfo.recipient);
    console.log('  Mosaic:',
        `${levyMosaicId.namespaceId}:${levyMosaicId.name}`);
    console.log('  Fee:', levyInfo.fee);
        // Retrieve the levy
        final String path = "/mosaic/definition?mosaicId=" + mosaicId;
        System.out.printf("Fetching mosaic information from %s%n", path);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + path)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        final JsonNode info = JSON_MAPPER.readTree(response.body());
        final JsonNode levyInfo = info.get("levy");
        final JsonNode levyMosaicId = levyInfo.get("mosaicId");
        System.out.println("Levy information:");
        System.out.printf("  Type: %s%n",
            1 == levyInfo.get("type").asInt()
                ? "absolute" : "percentile");
        System.out.printf("  Recipient: %s%n",
            levyInfo.get("recipient").asText());
        System.out.printf("  Mosaic: %s:%s%n",
            levyMosaicId.get("namespaceId").asText(),
            levyMosaicId.get("name").asText());
        System.out.printf("  Fee: %s%n", levyInfo.get("fee").asText());

To verify the mosaic with the levy was created, the code retrieves the mosaic definition from the /mosaic/definition GET endpoint, which returns the levy alongside the mosaic properties.

A levy in the response confirms that future transfers of the mosaic will be charged the levy.

How the Levy Is Charged⚓︎

After the mosaic is created, the levy applies to every transfer of the mosaic, and no extra field is needed in the transfer transaction.

A levy is not a guaranteed charge

Levies are not recursive, therefore they can be sidestepped. See the example in the Textbook.

The levy is charged on top of the transferred amount, so a sender who transfers 50 units of the mosaic created in this tutorial is debited:

  • 50 units of the mosaic, credited to the recipient of the transfer.
  • 1 XEM, credited to the levy recipient.
  • The transaction fee, credited to the harvester account.

The network rejects the transfer if the sender cannot cover both the transferred amount and the levy. Because this levy is paid in nem:xem, the sender must also have enough XEM to cover both the levy and the transaction fee. If the levy is paid in another mosaic, the sender must also hold a sufficient balance of that mosaic.

Output⚓︎

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

Using node http://libertalia.nemtest.net:7890
Signer address: TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP
Creating mosaic: my_namespace:token_1784625719
Levy:
  Type: absolute
  Recipient: TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4
  Mosaic: nem:xem
  Fee: 1000000
  Mosaic creation fee: 10.0 XEM
  Transaction fee: 0.15 XEM
Built mosaic definition transaction:
{
  "type": 16385,
  "version": 1,
  "network": 152,
  "timestamp": 357038135,
  "signer_public_key": "462EE976890916E54FA825D26BDD0235F5EB5B6A143C199AB0AE5EE9328E08CE",
  "signature": "3B240EE7E5B6EDEEDA676ECA2D54039E45089CEE7DE2958350F9A63E168F6DFF0B8380B2B0917C4BDE662E6531C3151EFB999DA3E0330B5C0A6895CD04A40808",
  "fee": "150000",
  "deadline": 357045335,
  "mosaic_definition": {
    "owner_public_key": "462EE976890916E54FA825D26BDD0235F5EB5B6A143C199AB0AE5EE9328E08CE",
    "id": {
      "namespace_id": {
        "name": "6d795f6e616d657370616365"
      },
      "name": "746f6b656e5f31373834363235373139"
    },
    "description": "4d79207475746f7269616c206d6f7361696320776974682061206c657679",
    "properties": [
      {
        "property": {
          "name": "64697669736962696c697479",
          "value": "32"
        }
      },
      {
        "property": {
          "name": "696e697469616c537570706c79",
          "value": "31303030"
        }
      },
      {
        "property": {
          "name": "737570706c794d757461626c65",
          "value": "74727565"
        }
      },
      {
        "property": {
          "name": "7472616e7366657261626c65",
          "value": "74727565"
        }
      }
    ],
    "levy": {
      "transfer_fee_type": 1,
      "recipient_address": "5442554C4541554732435A51495355523434324857413655414B47574958484441424A5649505334",
      "mosaic_id": {
        "namespace_id": {
          "name": "6e656d"
        },
        "name": "78656d"
      },
      "fee": "1000000"
    }
  },
  "rental_fee_sink": "54424D4F534149434F443446353445453543444D523233434342474F414D3258534A4252354F4C43",
  "rental_fee": "10000000"
}
Transaction hash: 190340818E859F24FD570CD5D54153DD3E5E192F2026F23E079522AC9BA56456
Announcing mosaic definition to /transaction/announce
  Result: SUCCESS
Waiting for mosaic definition confirmation from /transaction/get?hash=190340818E859F24FD570CD5D54153DD3E5E192F2026F23E079522AC9BA56456
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
mosaic definition confirmed in block 710705
Fetching mosaic information from /mosaic/definition?mosaicId=my_namespace:token_1784625719
Levy information:
  Type: absolute
  Recipient: TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4
  Mosaic: nem:xem
  Fee: 1000000

Some highlights from the output:

  • Mosaic ID (line 3): 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.

  • Levy fields (lines 4-8): The levy to create, an absolute fee of 1'000'000 atomic units of nem:xem (1 XEM), paid to the levy recipient on every transfer.

  • Levy in the transaction (lines 56-66): The levy is defined inside the mosaic definition. The recipient address, the levy mosaic name, and the mosaic name are hex-encoded in the payload, while the fee of this absolute levy is expressed in atomic units.

  • Verified levy (lines 81-84): The mosaic is retrieved from the network, confirming the levy type, its recipient, the mosaic in which it is paid, and its amount.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Describe the levy MosaicLevy
Attach the levy to a mosaic , MosaicDefinitionTransactionV1
Verify the levy /mosaic/definition GET

Next Steps⚓︎

Now that you have created a mosaic with a levy, you can: