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

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

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:
    # 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)

    # 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.transaction_factory.create({
        'type': 'mosaic_definition_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'rental_fee_sink': 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',
        'rental_fee': rental_fee,
        'mosaic_definition': {
            '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
        }
    })

    # 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))

    announce_path = '/transaction/announce'
    print(f'Announcing mosaic definition to {announce_path}')
    announce_request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=json_payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    with urllib.request.urlopen(announce_request) as response:
        announce_result = json.loads(response.read().decode())
    print(f'  Result: {announce_result["message"]}')

    if 'SUCCESS' == announce_result['message']:
        transaction_hash = facade.hash_transaction(transaction)
        status_path = f'/transaction/get?hash={transaction_hash}'
        print(f'Waiting for confirmation from {status_path}')
        is_confirmed = 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"]}')

    # 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 urllib.error.URLError as e:
    print(e.reason)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    NemFacade,
    NetworkTimestamp,
    calculateMosaicRentalFee,
    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 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 {
    // 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);

    // Describe the levy
    const LEVY_RECIPIENT = process.env.LEVY_RECIPIENT ||
        'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';

    const levy = {
        transferFeeType: 'absolute',
        recipientAddress: LEVY_RECIPIENT,
        mosaicId: {
            namespaceId: { name: 'nem' },
            name: 'xem'
        },
        fee: 1_000_000
    };
    console.log('Levy:');
    console.log('  Type:', levy.transferFeeType);
    console.log('  Recipient:', levy.recipientAddress);
    console.log('  Mosaic:',
        `${levy.mosaicId.namespaceId.name}:${levy.mosaicId.name}`);
    console.log('  Fee:', levy.fee);

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

    const transaction = facade.transactionFactory.create({
        type: 'mosaic_definition_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        rentalFeeSink: 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',
        rentalFee,
        mosaicDefinition: {
            ownerPublicKey: signerKeyPair.publicKey.toString(),
            id: {
                namespaceId: { name: namespaceName },
                name: mosaicName
            },
            description: 'My tutorial mosaic with a levy',
            properties: [
                { property: { name: 'divisibility', value: '2' } },
                { property: { name: 'initialSupply', value: '1000' } },
                { property: { name: 'supplyMutable', value: 'true' } },
                { property: { name: 'transferable', value: 'true' } }
            ],
            levy
        }
    });

    // 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 announcePath = '/transaction/announce';
    console.log('Announcing mosaic definition 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);

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

    // 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

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);

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.

    # 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);

Network time is fetched from /time-sync/network-time GET, and the transaction's timestamp and deadline fields are derived from it, following the process described in the Transfer XEM tutorial.

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 levy = {
        transferFeeType: 'absolute',
        recipientAddress: LEVY_RECIPIENT,
        mosaicId: {
            namespaceId: { name: 'nem' },
            name: 'xem'
        },
        fee: 1_000_000
    };
    console.log('Levy:');
    console.log('  Type:', levy.transferFeeType);
    console.log('  Recipient:', levy.recipientAddress);
    console.log('  Mosaic:',
        `${levy.mosaicId.namespaceId.name}:${levy.mosaicId.name}`);
    console.log('  Fee:', levy.fee);

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.transaction_factory.create({
        'type': 'mosaic_definition_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'rental_fee_sink': 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',
        'rental_fee': rental_fee,
        'mosaic_definition': {
            '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
        }
    })

    # 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.transactionFactory.create({
        type: 'mosaic_definition_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        rentalFeeSink: 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',
        rentalFee,
        mosaicDefinition: {
            ownerPublicKey: signerKeyPair.publicKey.toString(),
            id: {
                namespaceId: { name: namespaceName },
                name: mosaicName
            },
            description: 'My tutorial mosaic with a levy',
            properties: [
                { property: { name: 'divisibility', value: '2' } },
                { property: { name: 'initialSupply', value: '1000' } },
                { property: { name: 'supplyMutable', value: 'true' } },
                { property: { name: 'transferable', value: 'true' } }
            ],
            levy
        }
    });

    // 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`);

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

    announce_path = '/transaction/announce'
    print(f'Announcing mosaic definition to {announce_path}')
    announce_request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=json_payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    with urllib.request.urlopen(announce_request) as response:
        announce_result = json.loads(response.read().decode())
    print(f'  Result: {announce_result["message"]}')

    if 'SUCCESS' == announce_result['message']:
        transaction_hash = facade.hash_transaction(transaction)
        status_path = f'/transaction/get?hash={transaction_hash}'
        print(f'Waiting for confirmation from {status_path}')
        is_confirmed = 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"]}')
    // 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 announcePath = '/transaction/announce';
    console.log('Announcing mosaic definition 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);

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

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);

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
Fetching current network time from /time-sync/network-time
  Network time: 357038135 s since the nemesis block
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"
}
Announcing mosaic definition to /transaction/announce
  Result: SUCCESS
Waiting for confirmation from /transaction/get?hash=190340818E859F24FD570CD5D54153DD3E5E192F2026F23E079522AC9BA56456
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
Transaction 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 6-10): 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 58-68): 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 82-85): 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: