Skip to content

Adding Restrictions to a Mosaic⚓︎

The owner of a mosaic can restrict which accounts are allowed to transact with it. The conditions are called mosaic restrictions and are defined in two parts:

An account can transact with the mosaic only if its assigned values satisfy all the mosaic's global conditions.

This tutorial requires a preexisting mosaic created with the restrictable flag. If the mosaic does not yet define any global restriction, the tutorial creates one with the configuration:

Key Value Relation
security_level 1 greater-or-equal

This configuration means that the mosaic can only be used by accounts whose security_level restriction value is greater than or equal to 1.

The tutorial then assigns this key to a test account, or toggles its value between 1 and 0 if it already exists, and attempts to transfer the mosaic from its owner account to the test account.

As a result, every other run of the program fails with a restriction violation error.

Because configuring restrictions requires several transactions, the tutorial bundles them into a single complete aggregate transaction. This avoids waiting for each transaction to be confirmed individually.

Difference with Account Restrictions

Symbol also supports account restrictions, which are defined at the account level rather than at the mosaic level as shown in this tutorial.

These are distinct mechanisms. They are configured using different transaction types and operate under different rules.

However, account restrictions can limit which mosaics an account may interact with, and mosaic restrictions can limit which accounts may interact with a mosaic. The conceptual overlap is therefore a common source of confusion.

Prerequisites⚓︎

Before you start, make sure to:

Additionally, review the Transfer transaction and Creating a Complete Aggregate Transaction tutorials to understand how transactions are announced and confirmed, and how to bundle them.

Full Code⚓︎

import json
import os
import time
import urllib.request
import hashlib

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.sc import Amount
from symbolchain.symbol.Network import NetworkTimestamp

NODE_URL = os.environ.get(
    'NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using node {NODE_URL}')

# Helper function to announce a transaction
def announce_transaction(payload, label):
    print(f'Announcing {label} to /transactions')
    request = urllib.request.Request(
        f'{NODE_URL}/transactions',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(request) as response:
        print(f'  Response: {response.read().decode()}')

# Helper function to wait for transaction confirmation
def wait_for_confirmation(transaction_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{transaction_hash}'
            with urllib.request.urlopen(url) as response:
                status = json.loads(response.read().decode())
                print(f'  Transaction status: {status["group"]}')
                if status['group'] == 'confirmed':
                    print(f'{label} confirmed in {attempt} seconds')
                    return
                if status['group'] == 'failed':
                    raise Exception(f'{label} failed: {status["code"]}')
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise Exception(f'{label} not confirmed after 60 seconds')

# Returns a filtered list of restrictions currently applied to the mosaic
# matching the given restriction key
def get_mosaic_restrictions(query, key):
    restrictions_path = f'/restrictions/mosaic?{query}'
    print(f'  Getting restrictions from {restrictions_path}')
    res = []
    try:
        url = f'{NODE_URL}{restrictions_path}'
        with urllib.request.urlopen(url) as response:
            status = json.loads(response.read().decode())
            data = status['data']
            if len(data) > 0:
                # Look at the first returned restriction
                rlist = data[0]['mosaicRestrictionEntry']['restrictions']
                # Filter by key
                res = [r for r in rlist if int(r['key']) == key]
    except urllib.error.HTTPError:
        # The mosaic has no restrictions applied to this key
        pass
    print(f'  Response: {res}')
    return res

def get_mosaic_global_restrictions(mosaic_id, key):
    return get_mosaic_restrictions(
        f'mosaicId={mosaic_id:X}&entryType=1', key)

def get_mosaic_address_restrictions(mosaic_id, address, key):
    return get_mosaic_restrictions(
        f'mosaicId={mosaic_id:X}&entryType=0&targetAddress={address}',
        key)

# Returns a transaction enabling a mosaic's global restriction
def global_restriction_enable_transaction():
    transaction = facade.transaction_factory.create_embedded({
        'type': 'mosaic_global_restriction_transaction_v1',
        'signer_public_key': owner_key_pair.public_key,
        'mosaic_id': mosaic_id,
        'reference_mosaic_id': 0,
        'restriction_key': restriction_key,
        'previous_restriction_type': 0,
        'previous_restriction_value': 0,
        'new_restriction_type': 'ge',
        'new_restriction_value': 1
    })
    print(json.dumps(transaction.to_json(), indent=2))

    return transaction

# Returns a transaction setting an address restriction's value
def address_restriction_set_value(prev_value, new_value, address):
    transaction = facade.transaction_factory.create_embedded({
        'type': 'mosaic_address_restriction_transaction_v1',
        'signer_public_key': owner_key_pair.public_key,
        'mosaic_id': mosaic_id,
        'restriction_key': restriction_key,
        'previous_restriction_value': prev_value,
        'new_restriction_value': new_value,
        'target_address': address
    })
    print(json.dumps(transaction.to_json(), indent=2))

    return transaction

facade = SymbolFacade('testnet')

OWNER_PRIVATE_KEY = os.getenv('OWNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
owner_key_pair = SymbolFacade.KeyPair(PrivateKey(OWNER_PRIVATE_KEY))
owner_address = facade.network.public_key_to_address(
    owner_key_pair.public_key)
print(f'Owner address: {owner_address}')

target_address = os.getenv('TARGET_ADDRESS',
    'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA')
print(f'Target address: {target_address}')

mosaic_id = int(os.getenv('MOSAIC_ID', '6A383620F5C7A5B2'), 16)
print(f'Mosaic ID: 0x{mosaic_id:08X}')
restriction_name = os.getenv('RESTRICTION_NAME', 'security_level')
hasher = hashlib.sha3_256()
hasher.update(restriction_name.encode('utf8'))
restriction_key = int.from_bytes(hasher.digest()[:4])
print(f'Restriction name: "{restriction_name}"'
    f' (key: 0x{restriction_key:08X})')

try:
    # Fetch current network time
    time_path = '/node/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())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')

    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_mult = response_json['medianFeeMultiplier']
        minimum_mult = response_json['minFeeMultiplier']
        fee_mult = max(median_mult, minimum_mult)
        print(f'  Fee multiplier: {fee_mult}')

    # Enable global restriction if required
    transactions = []
    print("Checking if the global restriction is enabled:")
    global_restrictions = get_mosaic_global_restrictions(
        mosaic_id, restriction_key)
    if len(global_restrictions) == 0:
        # Enable the global restriction
        print('+ Enabling global restriction')
        transactions.append(global_restriction_enable_transaction())

        # Enable the address restriction
        print('+ Authorizing owner account')
        transactions.append(address_restriction_set_value(
            0xFFFFFFFF_FFFFFFFF, 1, owner_address))

    # Toggle target address restriction
    print("Checking if target account is authorized:")
    address_restrictions = get_mosaic_address_restrictions(
        mosaic_id, target_address, restriction_key)
    prev_value = 0xFFFFFFFF_FFFFFFFF
    if len(address_restrictions) > 0:
        prev_value = int(address_restrictions[0]['value'])
    if prev_value != 1:
        # Enable the address restriction
        print('+ Authorizing target account')
        transactions.append(address_restriction_set_value(
            prev_value, 1, target_address))
    else:
        # Disable the address restriction
        print('+ Deauthorizing target account')
        transactions.append(address_restriction_set_value(
            prev_value, 0, target_address))

    # Build an aggregate transaction
    print('Bundling', len(transactions),'transaction(s) in an aggregate')
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': owner_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            transactions),
        'transactions': transactions
    })
    transaction.fee = Amount(fee_mult * transaction.size)

    # Sign, announce and wait for confirmation
    payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(owner_key_pair, transaction))
    transaction_hash = facade.hash_transaction(transaction)
    announce_transaction(payload, 'aggregate')
    wait_for_confirmation(transaction_hash, 'aggregate')

    # Try to transfer the mosaic to the target address
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v1',
        'signer_public_key': owner_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'recipient_address': target_address,
        'mosaics': [{
            'mosaic_id': mosaic_id,
            'amount': 1
        }]
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(owner_key_pair, transaction))
    transaction_hash = facade.hash_transaction(transaction)
    print('\nAttempting transfer to the target account')
    announce_transaction(payload, 'test transfer')
    wait_for_confirmation(transaction_hash, 'test transfer')

except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    KeyPair,
    SymbolTransactionFactory,
    models,
    NetworkTimestamp,
    SymbolFacade
} from 'symbol-sdk/symbol';
import crypto from 'crypto';

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log('Using node', NODE_URL);

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    console.log(`Announcing ${label} to /transactions`);
    const response = await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log('  Response:', await response.text());
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    console.log(`Waiting for ${label} confirmation...`);
    for (let attempt = 0; attempt < 60; attempt++) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        try {
            const response = await fetch(
                `${NODE_URL}/transactionStatus/${transactionHash}`);
            const status = await response.json();
            console.log('  Transaction status:', status.group);
            if (status.group === 'confirmed') {
                console.log(`${label} confirmed in`, attempt, 'seconds');
                return;
            }
            if (status.group === 'failed') {
                throw new Error(`${label} failed: ${status.code}`);
            }
        } catch (e) {
            if (e.message.includes('failed'))
                throw e;
            console.log('  Transaction status: unknown');
        }
    }
    throw new Error(`${label} not confirmed after 60 seconds`);
}

// Returns a filtered list of restrictions currently applied to the mosaic
// matching the given restriction key
async function getMosaicRestrictions(query, key) {
    const restrictionsPath = `/restrictions/mosaic?${query}`;
    console.log(`  Getting restrictions from ${restrictionsPath}`);
    let res = [];
    try {
        const response = await fetch(`${NODE_URL}${restrictionsPath}`);
        const status = await response.json();
        const data = status.data;
        if (data.length > 0) {
            // Look at the first returned restriction
            const rlist = data[0].mosaicRestrictionEntry.restrictions;
            // Filter by key
            res = rlist.filter(r => BigInt(r.key) === key);
        }
    } catch {
        // The mosaic has no restrictions applied to this key
    }
    console.log('  Response:', res);
    return res;
}

function getMosaicGlobalRestrictions(mosaicId, key) {
    return getMosaicRestrictions(
        `mosaicId=${mosaicId.toString(16)}&entryType=1`,
        key);
}

function getMosaicAddressRestrictions(mosaicId, address, key) {
    return getMosaicRestrictions(
        `mosaicId=${mosaicId.toString(16)}&entryType=0` +
        `&targetAddress=${address}`, key);
}

// Returns a transaction enabling a mosaic's global restriction
function globalRestrictionEnableTransaction() {
    const transaction = facade.transactionFactory.createEmbedded({
        type: 'mosaic_global_restriction_transaction_v1',
        signerPublicKey: ownerKeyPair.publicKey,
        mosaicId,
        referenceMosaicId: 0n,
        restrictionKey,
        previousRestrictionType: 0,
        previousRestrictionValue: 0n,
        newRestrictionType: 'ge',
        newRestrictionValue: 1n
    });
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}

// Returns a transaction setting an address restriction's value
function addressRestrictionSetValue(prevValue, newValue, address) {
    const transaction = facade.transactionFactory.createEmbedded({
        type: 'mosaic_address_restriction_transaction_v1',
        signerPublicKey: ownerKeyPair.publicKey,
        mosaicId,
        restrictionKey,
        previousRestrictionValue: prevValue,
        newRestrictionValue: newValue,
        targetAddress: address
    });
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}

const facade = new SymbolFacade('testnet');

const OWNER_PRIVATE_KEY = process.env.OWNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const ownerKeyPair = new KeyPair(new PrivateKey(OWNER_PRIVATE_KEY));
const ownerAddress = facade.network.publicKeyToAddress(
    ownerKeyPair.publicKey);
console.log(`Owner address: ${ownerAddress}`);

const targetAddress = process.env.TARGET_ADDRESS ||
    'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA';
console.log(`Target address: ${targetAddress}`);

const mosaicId = BigInt('0x' + (process.env.MOSAIC_ID ||
    '6A383620F5C7A5B2'));
console.log(`Mosaic ID: 0x${mosaicId.toString(16).toUpperCase()}`);

const restrictionName = process.env.RESTRICTION_NAME || 'security_level';
const digest = crypto.createHash('sha3-256')
    .update(Buffer.from(restrictionName, 'utf8'))
    .digest();
const restrictionKey = BigInt(digest.readUInt32BE(0));
console.log(`Restriction name: "${restrictionName}" (key: 0x${
    restrictionKey.toString(16).toUpperCase().padStart(8, '0')
    })`);

try {
    // Fetch current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');

    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMult = feeJSON.medianFeeMultiplier;
    const minimumMult = feeJSON.minFeeMultiplier;
    const feeMult = Math.max(medianMult, minimumMult);
    console.log('  Fee multiplier:', feeMult);

    // Enable global restriction if required
    const transactions = [];
    console.log('Checking if the global restriction is enabled:');
    const globalRestrictions = await getMosaicGlobalRestrictions(
        mosaicId, restrictionKey);
    if (globalRestrictions.length === 0) {
        // Enable the global restriction
        console.log('+ Enabling global restriction');
        transactions.push(globalRestrictionEnableTransaction());

        // Enable the address restriction
        console.log('+ Authorizing owner account');
        transactions.push(addressRestrictionSetValue(
            0xFFFFFFFFFFFFFFFFn, 1n, ownerAddress.toString()));
    }

    // Toggle target address restriction
    console.log('Checking if target account is authorized:');
    const addressRestrictions = await getMosaicAddressRestrictions(
        mosaicId, targetAddress, restrictionKey);
    let prevValue = 0xFFFFFFFFFFFFFFFFn;
    if (addressRestrictions.length > 0)
        prevValue = BigInt(addressRestrictions[0].value);
    if (prevValue !== 1n) {
        // Enable the address restriction
        console.log('+ Authorizing target account');
        transactions.push(addressRestrictionSetValue(
            prevValue, 1n, targetAddress));
    } else {
        // Disable the address restriction
        console.log('+ Deauthorizing target account');
        transactions.push(addressRestrictionSetValue(
            prevValue, 0n, targetAddress));
    }

    // Build an aggregate transaction
    console.log('Bundling', transactions.length,
        'transaction(s) in an aggregate');
    const aggregate = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: ownerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            transactions),
        transactions
    });
    aggregate.fee = new models.Amount(feeMult * aggregate.size);

    // Sign, announce and wait for confirmation
    let payload = SymbolTransactionFactory.attachSignature(
        aggregate,
        facade.signTransaction(ownerKeyPair, aggregate));
    let hash = facade.hashTransaction(aggregate).toString();
    await announceTransaction(payload, 'aggregate');
    await waitForConfirmation(hash, 'aggregate');

    // Try to transfer the mosaic to the target address
    const transfer = facade.transactionFactory.create({
        type: 'transfer_transaction_v1',
        signerPublicKey: ownerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        recipientAddress: targetAddress,
        mosaics: [{
            mosaicId,
            amount: 1n
        }]
    });
    transfer.fee = new models.Amount(feeMult * transfer.size);

    payload = SymbolTransactionFactory.attachSignature(
        transfer,
        facade.signTransaction(ownerKeyPair, transfer));
    hash = facade.hashTransaction(transfer).toString();
    console.log('\nAttempting transfer to the target account');
    await announceTransaction(payload, 'test transfer');
    await waitForConfirmation(hash, 'test transfer');

} catch (e) {
    console.error(e.message);
}

Download source

Code Explanation⚓︎

The code begins by defining several helper functions. For details on how transactions are announced and how their confirmation is tracked, refer to the Transfer transaction tutorial. The remaining helper functions are described in the sections below.

The tutorial then proceeds to:

Setting Up the Accounts⚓︎

The tutorial starts by configuring the accounts involved in the example.

OWNER_PRIVATE_KEY = os.getenv('OWNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
owner_key_pair = SymbolFacade.KeyPair(PrivateKey(OWNER_PRIVATE_KEY))
owner_address = facade.network.public_key_to_address(
    owner_key_pair.public_key)
print(f'Owner address: {owner_address}')

target_address = os.getenv('TARGET_ADDRESS',
    'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA')
print(f'Target address: {target_address}')

mosaic_id = int(os.getenv('MOSAIC_ID', '6A383620F5C7A5B2'), 16)
print(f'Mosaic ID: 0x{mosaic_id:08X}')
restriction_name = os.getenv('RESTRICTION_NAME', 'security_level')
hasher = hashlib.sha3_256()
hasher.update(restriction_name.encode('utf8'))
restriction_key = int.from_bytes(hasher.digest()[:4])
print(f'Restriction name: "{restriction_name}"'
    f' (key: 0x{restriction_key:08X})')
const OWNER_PRIVATE_KEY = process.env.OWNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const ownerKeyPair = new KeyPair(new PrivateKey(OWNER_PRIVATE_KEY));
const ownerAddress = facade.network.publicKeyToAddress(
    ownerKeyPair.publicKey);
console.log(`Owner address: ${ownerAddress}`);

const targetAddress = process.env.TARGET_ADDRESS ||
    'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA';
console.log(`Target address: ${targetAddress}`);

const mosaicId = BigInt('0x' + (process.env.MOSAIC_ID ||
    '6A383620F5C7A5B2'));
console.log(`Mosaic ID: 0x${mosaicId.toString(16).toUpperCase()}`);

const restrictionName = process.env.RESTRICTION_NAME || 'security_level';
const digest = crypto.createHash('sha3-256')
    .update(Buffer.from(restrictionName, 'utf8'))
    .digest();
const restrictionKey = BigInt(digest.readUInt32BE(0));
console.log(`Restriction name: "${restrictionName}" (key: 0x${
    restrictionKey.toString(16).toUpperCase().padStart(8, '0')
    })`);

The code defines:

  • the owner account, which controls the mosaic and is responsible for configuring its restrictions. Its private key can be provided through the OWNER_PRIVATE_KEY environment variable as a 64-character hexadecimal string.
  • the target account, which will later receive authorization to transact with the mosaic. Its address can be provided through the TARGET_ADDRESS environment variable as a Symbol testnet address.
  • the mosaic identifier, read from MOSAIC_ID as 16 hexadecimal characters.
  • the restriction name, read from RESTRICTION_NAME as a string.
  • the corresponding restriction key, derived from the restriction name by hashing it with SHA3-256 and taking the first four bytes of the hash. This approach allows applications to use human-readable names while producing deterministic keys. Any 32-bit number can also be used directly as a restriction key.

If any of these values is not provided through an environment variable, a default value is used.

The owner account must hold sufficient funds to announce transactions. If the default one is used, it may already be funded.

Fetching Network Time and Fees⚓︎

    # Fetch current network time
    time_path = '/node/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())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')

    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_mult = response_json['medianFeeMultiplier']
        minimum_mult = response_json['minFeeMultiplier']
        fee_mult = max(median_mult, minimum_mult)
        print(f'  Fee multiplier: {fee_mult}')
    // Fetch current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');

    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMult = feeJSON.medianFeeMultiplier;
    const minimumMult = feeJSON.minFeeMultiplier;
    const feeMult = Math.max(medianMult, minimumMult);
    console.log('  Fee multiplier:', feeMult);

Network time and recommended fees are fetched from /node/time GET and /network/fees/transaction GET respectively, following the process described in the Transfer Transaction tutorial.

Enabling the Global Restriction⚓︎

    # Enable global restriction if required
    transactions = []
    print("Checking if the global restriction is enabled:")
    global_restrictions = get_mosaic_global_restrictions(
        mosaic_id, restriction_key)
    if len(global_restrictions) == 0:
        # Enable the global restriction
        print('+ Enabling global restriction')
        transactions.append(global_restriction_enable_transaction())

        # Enable the address restriction
        print('+ Authorizing owner account')
        transactions.append(address_restriction_set_value(
            0xFFFFFFFF_FFFFFFFF, 1, owner_address))
    // Enable global restriction if required
    const transactions = [];
    console.log('Checking if the global restriction is enabled:');
    const globalRestrictions = await getMosaicGlobalRestrictions(
        mosaicId, restrictionKey);
    if (globalRestrictions.length === 0) {
        // Enable the global restriction
        console.log('+ Enabling global restriction');
        transactions.push(globalRestrictionEnableTransaction());

        // Enable the address restriction
        console.log('+ Authorizing owner account');
        transactions.push(addressRestrictionSetValue(
            0xFFFFFFFFFFFFFFFFn, 1n, ownerAddress.toString()));
    }

The code first checks whether the mosaic already defines a global restriction for the configured key:

def get_mosaic_restrictions(query, key):
    restrictions_path = f'/restrictions/mosaic?{query}'
    print(f'  Getting restrictions from {restrictions_path}')
    res = []
    try:
        url = f'{NODE_URL}{restrictions_path}'
        with urllib.request.urlopen(url) as response:
            status = json.loads(response.read().decode())
            data = status['data']
            if len(data) > 0:
                # Look at the first returned restriction
                rlist = data[0]['mosaicRestrictionEntry']['restrictions']
                # Filter by key
                res = [r for r in rlist if int(r['key']) == key]
    except urllib.error.HTTPError:
        # The mosaic has no restrictions applied to this key
        pass
    print(f'  Response: {res}')
    return res

def get_mosaic_global_restrictions(mosaic_id, key):
    return get_mosaic_restrictions(
        f'mosaicId={mosaic_id:X}&entryType=1', key)
async function getMosaicRestrictions(query, key) {
    const restrictionsPath = `/restrictions/mosaic?${query}`;
    console.log(`  Getting restrictions from ${restrictionsPath}`);
    let res = [];
    try {
        const response = await fetch(`${NODE_URL}${restrictionsPath}`);
        const status = await response.json();
        const data = status.data;
        if (data.length > 0) {
            // Look at the first returned restriction
            const rlist = data[0].mosaicRestrictionEntry.restrictions;
            // Filter by key
            res = rlist.filter(r => BigInt(r.key) === key);
        }
    } catch {
        // The mosaic has no restrictions applied to this key
    }
    console.log('  Response:', res);
    return res;
}

function getMosaicGlobalRestrictions(mosaicId, key) {
    return getMosaicRestrictions(
        `mosaicId=${mosaicId.toString(16)}&entryType=1`,
        key);
}

This is done by querying /restrictions/mosaic GET and filtering by mosaicId and entryType=1, which selects global restrictions. The returned entries are then filtered to keep only those involving the selected restriction_key.

If no restriction is found, one is created by adding two transactions to the list of transactions to announce:

  • a mosaic global restriction transaction defining the restriction condition. See the MosaicGlobalRestrictionTransactionV1 serialization table for details about each of its fields.

    The restriction created in this tutorial requires the value associated with the key security_level to be greater than or equal to 1.

  • a mosaic address restriction transaction authorizing the owner account. See the MosaicAddressRestrictionTransactionV1 serialization table for details about each of its fields.

    The code assigns the value 1 to the owner's security_level so the owner account can continue transacting with its own mosaic.

    Note

    For simplicity, the tutorial assumes that if no global restriction exists, the owner account also has no address restriction.

    For this reason 0xFFFFFFFF_FFFFFFFF is used as the previous value, indicating that no value was previously set.

    A more robust implementation should first query the owner's restriction state and use the appropriate previous value, as demonstrated below for the target account.

Toggling the Address Restriction⚓︎

With the global restriction in effect, the next step checks whether the target account already has a restriction value defined for the key.

def get_mosaic_restrictions(query, key):
    restrictions_path = f'/restrictions/mosaic?{query}'
    print(f'  Getting restrictions from {restrictions_path}')
    res = []
    try:
        url = f'{NODE_URL}{restrictions_path}'
        with urllib.request.urlopen(url) as response:
            status = json.loads(response.read().decode())
            data = status['data']
            if len(data) > 0:
                # Look at the first returned restriction
                rlist = data[0]['mosaicRestrictionEntry']['restrictions']
                # Filter by key
                res = [r for r in rlist if int(r['key']) == key]
    except urllib.error.HTTPError:
        # The mosaic has no restrictions applied to this key
        pass
    print(f'  Response: {res}')
    return res

def get_mosaic_global_restrictions(mosaic_id, key):
    return get_mosaic_restrictions(
        f'mosaicId={mosaic_id:X}&entryType=1', key)

def get_mosaic_address_restrictions(mosaic_id, address, key):
    return get_mosaic_restrictions(
        f'mosaicId={mosaic_id:X}&entryType=0&targetAddress={address}',
        key)
async function getMosaicRestrictions(query, key) {
    const restrictionsPath = `/restrictions/mosaic?${query}`;
    console.log(`  Getting restrictions from ${restrictionsPath}`);
    let res = [];
    try {
        const response = await fetch(`${NODE_URL}${restrictionsPath}`);
        const status = await response.json();
        const data = status.data;
        if (data.length > 0) {
            // Look at the first returned restriction
            const rlist = data[0].mosaicRestrictionEntry.restrictions;
            // Filter by key
            res = rlist.filter(r => BigInt(r.key) === key);
        }
    } catch {
        // The mosaic has no restrictions applied to this key
    }
    console.log('  Response:', res);
    return res;
}

function getMosaicGlobalRestrictions(mosaicId, key) {
    return getMosaicRestrictions(
        `mosaicId=${mosaicId.toString(16)}&entryType=1`,
        key);
}

function getMosaicAddressRestrictions(mosaicId, address, key) {
    return getMosaicRestrictions(
        `mosaicId=${mosaicId.toString(16)}&entryType=0` +
        `&targetAddress=${address}`, key);
}

As in the global restriction case, the current value is obtained by querying /restrictions/mosaic GET and filtering by mosaicId, targetAddress, and entryType=0, which selects address restrictions. The returned entries are then filtered to keep only those involving the selected restriction_key.

Depending on the current value of the restriction for the target account, a transaction is created that authorizes or deauthorizes the account. This transaction is added to the list of transactions to announce.

    print("Checking if target account is authorized:")
    address_restrictions = get_mosaic_address_restrictions(
        mosaic_id, target_address, restriction_key)
    prev_value = 0xFFFFFFFF_FFFFFFFF
    if len(address_restrictions) > 0:
        prev_value = int(address_restrictions[0]['value'])
    if prev_value != 1:
        # Enable the address restriction
        print('+ Authorizing target account')
        transactions.append(address_restriction_set_value(
            prev_value, 1, target_address))
    else:
        # Disable the address restriction
        print('+ Deauthorizing target account')
        transactions.append(address_restriction_set_value(
            prev_value, 0, target_address))
    console.log('Checking if target account is authorized:');
    const addressRestrictions = await getMosaicAddressRestrictions(
        mosaicId, targetAddress, restrictionKey);
    let prevValue = 0xFFFFFFFFFFFFFFFFn;
    if (addressRestrictions.length > 0)
        prevValue = BigInt(addressRestrictions[0].value);
    if (prevValue !== 1n) {
        // Enable the address restriction
        console.log('+ Authorizing target account');
        transactions.push(addressRestrictionSetValue(
            prevValue, 1n, targetAddress));
    } else {
        // Disable the address restriction
        console.log('+ Deauthorizing target account');
        transactions.push(addressRestrictionSetValue(
            prevValue, 0n, targetAddress));
    }
  • If the account does not yet have a restriction value, or the value is not 1, the code assigns the value 1, authorizing it to use the mosaic.

  • If the account already has the value 1, the code replaces it with 0, revoking the authorization.

Running the tutorial repeatedly therefore alternates between authorizing and deauthorizing the target account.

Only the first restriction in the returned list is examined, because, after filtering by restriction_key, the list is either empty or contains a single entry.

The same MosaicAddressRestrictionTransactionV1 is used in both cases, changing only the value assigned to the restriction.

When no previous restriction exists, the special value 0xFFFFFFFF_FFFFFFFF must be used as the previous value.

Building the Aggregate Transaction⚓︎

All configuration transactions created above are bundled into a single complete aggregate transaction, so the user does not need to wait for them to be confirmed individually.

    print('Bundling', len(transactions),'transaction(s) in an aggregate')
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': owner_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            transactions),
        'transactions': transactions
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    console.log('Bundling', transactions.length,
        'transaction(s) in an aggregate');
    const aggregate = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: ownerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            transactions),
        transactions
    });
    aggregate.fee = new models.Amount(feeMult * aggregate.size);

Only the aggregate transaction pays fees, so embedded transactions do not use the fee field.

Submitting the Transaction⚓︎

The constructed aggregate transaction is signed, announced, and confirmed as described in the Transfer transaction tutorial.

    payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(owner_key_pair, transaction))
    transaction_hash = facade.hash_transaction(transaction)
    announce_transaction(payload, 'aggregate')
    wait_for_confirmation(transaction_hash, 'aggregate')
    let payload = SymbolTransactionFactory.attachSignature(
        aggregate,
        facade.signTransaction(ownerKeyPair, aggregate));
    let hash = facade.hashTransaction(aggregate).toString();
    await announceTransaction(payload, 'aggregate');
    await waitForConfirmation(hash, 'aggregate');

Sending a Test Transfer⚓︎

Finally, the tutorial attempts to send one unit of the mosaic from the owner account to the target account using a standard transfer transaction.

    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v1',
        'signer_public_key': owner_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'recipient_address': target_address,
        'mosaics': [{
            'mosaic_id': mosaic_id,
            'amount': 1
        }]
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(owner_key_pair, transaction))
    transaction_hash = facade.hash_transaction(transaction)
    print('\nAttempting transfer to the target account')
    announce_transaction(payload, 'test transfer')
    wait_for_confirmation(transaction_hash, 'test transfer')
    const transfer = facade.transactionFactory.create({
        type: 'transfer_transaction_v1',
        signerPublicKey: ownerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        recipientAddress: targetAddress,
        mosaics: [{
            mosaicId,
            amount: 1n
        }]
    });
    transfer.fee = new models.Amount(feeMult * transfer.size);

    payload = SymbolTransactionFactory.attachSignature(
        transfer,
        facade.signTransaction(ownerKeyPair, transfer));
    hash = facade.hashTransaction(transfer).toString();
    console.log('\nAttempting transfer to the target account');
    await announceTransaction(payload, 'test transfer');
    await waitForConfirmation(hash, 'test transfer');

If the target account currently satisfies the restriction (security_level ≥ 1), the transfer is confirmed successfully.

If the restriction value was toggled to 0, the transaction fails with an Account_Unauthorized error.

Running the tutorial multiple times therefore alternates between successful and failing transfers, demonstrating how mosaic restrictions control which accounts are allowed to transact with the mosaic.

Output⚓︎

The output shown below corresponds to two typical runs of the program.

Using node https://reference.symboltest.net:3001
Owner address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Target address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Mosaic ID: 0x147893385B36E34D
Restriction name: "security_level" (key: 0xC1D01F88)
Fetching current network time from /node/time
  Network time: 105560304329 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Checking if the global restriction is enabled:
  Getting restrictions from /restrictions/mosaic?mosaicId=147893385B36E34D&entryType=1
  Response: []
+ Enabling global restriction
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16721,
  "mosaic_id": "1475090748221612877",
  "reference_mosaic_id": "0",
  "restriction_key": "3251642248",
  "previous_restriction_value": "0",
  "new_restriction_value": "1",
  "previous_restriction_type": 0,
  "new_restriction_type": 6
}
+ Authorizing owner account
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16977,
  "mosaic_id": "1475090748221612877",
  "restriction_key": "3251642248",
  "previous_restriction_value": "18446744073709551615",
  "new_restriction_value": "1",
  "target_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9"
}
Checking if target account is authorized:
  Getting restrictions from /restrictions/mosaic?mosaicId=147893385B36E34D&entryType=0&targetAddress=TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
  Response: []
+ Authorizing target account
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16977,
  "mosaic_id": "1475090748221612877",
  "restriction_key": "3251642248",
  "previous_restriction_value": "18446744073709551615",
  "new_restriction_value": "1",
  "target_address": "987D075454716222F609929E883174AD8C996D5828C938BC"
}
Bundling 3 transaction(s) in an aggregate
Announcing aggregate to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for aggregate confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  ...
  Transaction status: confirmed
aggregate confirmed in 14 seconds

Attempting transfer to the target account
Announcing test transfer to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for test transfer confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  ...
  Transaction status: confirmed
test transfer confirmed in 21 seconds

Key points in the output:

  • Lines 2-3: Addresses of the involved accounts.
  • Line 4: The mosaic being restricted.
  • Line 5: The restriction name and its corresponding key.
  • Line 12 (Response: []): The mosaic currently has no global restrictions.
  • Line 13: The transaction configuring the mosaic restriction. It includes the mosaic ID (in decimal), the restriction key (in decimal), the restriction value (1), and the restriction condition (6, which corresponds to the greater-or-equal MosaicRestrictionType)
  • Line 27: The transaction authorizing the owner account. It includes the mosaic ID (in decimal), the restriction key (in decimal), and the necessary restriction value (1).
  • Line 41 (Response: []): The target account is currently unauthorized because it has no value associated with the restriction key.
  • Line 42: The transaction authorizing the target account. It includes the mosaic ID (in decimal), the restriction key (in decimal), and the necessary restriction value (1).
  • Line 72 (test transfer confirmed): The test transaction succeeded because both accounts satisfy the restriction and are therefore authorized.
Using node https://reference.symboltest.net:3001
Owner address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Target address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Mosaic ID: 0x147893385B36E34D
Restriction name: "security_level" (key: 0xC1D01F88)
Fetching current network time from /node/time
  Network time: 105560429335 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Checking if the global restriction is enabled:
  Getting restrictions from /restrictions/mosaic?mosaicId=147893385B36E34D&entryType=1
  Response: [{'key': '3251642248', 'restriction': {'referenceMosaicId': '0000000000000000', 'restrictionValue': '1', 'restrictionType': 6}}]
Checking if target account is authorized:
  Getting restrictions from /restrictions/mosaic?mosaicId=147893385B36E34D&entryType=0&targetAddress=TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
  Response: [{'key': '3251642248', 'value': '1'}]
+ Deauthorizing target account
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16977,
  "mosaic_id": "1475090748221612877",
  "restriction_key": "3251642248",
  "previous_restriction_value": "1",
  "new_restriction_value": "0",
  "target_address": "987D075454716222F609929E883174AD8C996D5828C938BC"
}
Bundling 1 transaction(s) in an aggregate
Announcing aggregate to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for aggregate confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  ...
  Transaction status: confirmed
aggregate confirmed in 19 seconds

Attempting transfer to the target account
Announcing test transfer to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for test transfer confirmation...
  Transaction status: failed
test transfer failed: Failure_RestrictionMosaic_Account_Unauthorized

Key points in the output:

  • Lines 2-3: Addresses of the involved accounts.
  • Line 4: The mosaic being restricted.
  • Line 5: The restriction name and its corresponding key.
  • Line 12 (Response: [ ... ]): Existing restrictions are detected.
  • Line 15 (Response: [ ... ]): The target account has a restriction value of 1, meaning it is authorized.
  • Line 16: The transaction deauthorizing the target account. It includes the mosaic ID (in decimal), the restriction key (in decimal), and the necessary restriction value (0).
  • Line 43 (test transfer failed): The test transaction failed because the target account no longer satisfies the restriction, as expected.

The transaction hashes shown in the output can be used to look up the transactions in the Symbol Testnet Explorer.

Troubleshooting⚓︎

Transactions are rejected if they violate protocol constraints. The following table summarizes the most common error sources:

Error message Probable cause
Mosaic_Expired The mosaic does not exist, or it has expired.
Mosaic_Owner_Conflict The account attempting to restrict the mosaic is not its owner.
Required_Property_Flag_Unset The mosaic was not created with the restrictable flag.
Account_Unauthorized Either the owner or the target account is not authorized to transact with the mosaic.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Retrieve the current mosaic restriction configuration /restrictions/mosaic GET
Configure a mosaic global restriction MosaicGlobalRestrictionTransactionV1
Retrieve an account's mosaic restriction configuration /restrictions/mosaic GET
Configure a mosaic address restriction MosaicAddressRestrictionTransactionV1