Skip to content

Adding Restrictions to a Mosaic⚓︎

ADVANCED

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

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.SymbolFacade import SymbolFacade
from symbolchain.symbol.Restriction import mosaic_restriction_generate_key

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


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', '6A5ACF2376E50D4A'), 16)
print(f'Mosaic ID: 0x{mosaic_id:016X}')
restriction_name = os.getenv('RESTRICTION_NAME', 'security_level')
restriction_key = mosaic_restriction_generate_key(restriction_name)
print(f'Restriction name: "{restriction_name}"'
    f' (key: 0x{restriction_key:016X})')


# 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 announce_response:
        print(f'  Response: {announce_response.read().decode()}')


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{tx_hash}'
            with urllib.request.urlopen(url) as confirm_response:
                status = json.loads(confirm_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 RuntimeError(
                        f"{label} failed: {status['code']}")
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise TimeoutError(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 = []
    url = f'{NODE_URL}{restrictions_path}'
    with urllib.request.urlopen(url) as restr_response:
        status = json.loads(restr_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]
    print(f'  Response: {res}')
    return res


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


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


# Returns a transaction enabling a mosaic's global restriction
def set_global_restriction_transaction():
    restr_transaction = facade.create_embedded_transaction_from_descriptor(
        {
            'type': 'mosaic_global_restriction_transaction_v1',
            '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
        },
        owner_key_pair.public_key)
    print(json.dumps(restr_transaction.to_json(), indent=2))

    return restr_transaction


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

    return restr_transaction


try:
    # 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_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')

    # 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(set_global_restriction_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')
    aggregate_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                transactions),
            'transactions': transactions
        },
        owner_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)

    # Sign, announce and wait for confirmation
    aggregate_payload = facade.transaction_factory.attach_signature(
        aggregate_transaction,
        facade.sign_transaction(owner_key_pair, aggregate_transaction))
    aggregate_hash = facade.hash_transaction(aggregate_transaction)
    announce_transaction(aggregate_payload, 'aggregate')
    wait_for_confirmation(aggregate_hash, 'aggregate')

    # Try to transfer the mosaic to the target address
    test_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address': target_address,
            'mosaics': [{
                'mosaic_id': mosaic_id,
                'amount': 1
            }]
        },
        owner_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    test_payload = facade.transaction_factory.attach_signature(
        test_transaction,
        facade.sign_transaction(owner_key_pair, test_transaction))
    test_hash = facade.hash_transaction(test_transaction)
    print('\nAttempting transfer to the target account')
    announce_transaction(test_payload, 'test transfer')
    wait_for_confirmation(test_hash, 'test transfer')

except Exception as e:
    print(e)

Download source

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

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

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 ||
    '6A5ACF2376E50D4A'}`);
console.log(`Mosaic ID: 0x${mosaicId.toString(16)
    .toUpperCase().padStart(16, '0')}`);

const restrictionName = process.env.RESTRICTION_NAME || 'security_level';
const restrictionKey = mosaicRestrictionGenerateKey(restrictionName);
console.log(`Restriction name: "${restrictionName}" (key: 0x${
    restrictionKey.toString(16).toUpperCase().padStart(16, '0')})`);


// 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; 60 > attempt; attempt++) {
        await new Promise(resolve => { setTimeout(resolve, 1000); });
        const response = await fetch(
            `${NODE_URL}/transactionStatus/${transactionHash}`);
        if (!response.ok) {
            if (404 === response.status) {
                console.log('  Transaction status: unknown');
                continue;
            }
            throw new Error(`HTTP ${response.status}`);
        }
        const status = await response.json();
        console.log('  Transaction status:', status.group);
        if ('confirmed' === status.group) {
            console.log(`${label} confirmed in`, attempt, 'seconds');
            return;
        }
        if ('failed' === status.group)
            throw new Error(`${label} failed: ${status.code}`);
    }
    throw new Error(`${label} not confirmed after 60 seconds`);
}

// Returns 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 = [];
    const response = await fetch(`${NODE_URL}${restrictionsPath}`);
    const status = await response.json();
    const data = status.data;
    if (0 < data.length) {
        // Look at the first returned restriction
        const rlist = data[0].mosaicRestrictionEntry.restrictions;
        // Filter by key
        res = rlist.filter(r => BigInt(r.key) === key);
    }
    console.log('  Response:', res);
    return res;
}

function getMosaicGlobalRestrictions(queriedMosaicId, key) {
    return getMosaicRestrictions(
        `mosaicId=${queriedMosaicId.toString(16)
            .toUpperCase().padStart(16, '0')}` +
        '&entryType=1', key);
}

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

// Returns a transaction enabling a mosaic's global restriction
function globalRestrictionEnableTransaction() {
    const transaction =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.MosaicGlobalRestrictionTransactionV1Descriptor(
                new models.UnresolvedMosaicId(mosaicId),
                new models.UnresolvedMosaicId(0n),
                restrictionKey,
                0n,
                1n,
                models.MosaicRestrictionType.NONE,
                models.MosaicRestrictionType.GE),
            ownerKeyPair.publicKey);
    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.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.MosaicAddressRestrictionTransactionV1Descriptor(
                new models.UnresolvedMosaicId(mosaicId),
                restrictionKey,
                prevValue,
                newValue,
                new SymbolFacade.Address(address)),
            ownerKeyPair.publicKey);
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}

try {
    // 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 medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);

    // Enable global restriction if required
    const transactions = [];
    console.log('Checking if the global restriction is enabled:');
    const globalRestrictions = await getMosaicGlobalRestrictions(
        mosaicId, restrictionKey);
    if (0 === globalRestrictions.length) {
        // 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 (0 < addressRestrictions.length)
        prevValue = BigInt(addressRestrictions[0].value);
    if (1n !== prevValue) {
        // 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 aggregateTransaction =
        facade.createTransactionFromTypedDescriptor(
            new descriptors.AggregateCompleteTransactionV3Descriptor(
                facade.static.hashEmbeddedTransactions(transactions),
                transactions,
                undefined),
            ownerKeyPair.publicKey,
            feeMultiplier,
            2 * 60 * 60);

    // Sign, announce and wait for confirmation
    const aggregatePayload = SymbolTransactionFactory.attachSignature(
        aggregateTransaction,
        facade.signTransaction(ownerKeyPair, aggregateTransaction));
    const aggregateHash =
        facade.hashTransaction(aggregateTransaction).toString();
    await announceTransaction(aggregatePayload, 'aggregate');
    await waitForConfirmation(aggregateHash, 'aggregate');

    // Try to transfer the mosaic to the target address
    const testTransaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV1Descriptor(
            new SymbolFacade.Address(targetAddress),
            [
                new descriptors.UnresolvedMosaicDescriptor(
                    new models.UnresolvedMosaicId(mosaicId),
                    new models.Amount(1n))
            ],
            undefined),
        ownerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

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

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

Download source

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

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.ArrayList;
import java.util.List;

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

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
import org.symbol.sdk.symbol.Address;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.Restriction;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.descriptors.*;
import org.symbol.sdk.symbol.models.*;

public final class MosaicRestrictions {
    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", "https://reference.symboltest.net:3001");

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

    private KeyPair ownerKeyPair;

    private Address ownerAddress;

    private Address targetAddress;

    private long mosaicId;

    private long restrictionKey;

    // Helper function to announce a transaction
    private void announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to /transactions%n", label);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + "/transactions"))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        System.out.printf("  Response: %s%n", response.body());
    }

    // Helper function to wait for transaction confirmation
    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        for (int attempt = 0; 60 > attempt; ++attempt) {
            Thread.sleep(1000);
            final String statusPath =
                "/transactionStatus/" + transactionHash;
            final HttpRequest statusRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> statusResponse = HTTP_CLIENT
                .send(statusRequest, BodyHandlers.ofString());
            if (404 == statusResponse.statusCode()) {
                System.out.println("  Transaction status: unknown");
                continue;
            }
            if (2 != statusResponse.statusCode() / 100)
                throw new IOException(
                    "HTTP " + statusResponse.statusCode());

            final JsonNode status =
                JSON_MAPPER.readTree(statusResponse.body());
            final String group = status.get("group").asText();
            System.out.printf("  Transaction status: %s%n", group);
            if ("confirmed".equals(group)) {
                System.out.printf("%s confirmed in %d seconds%n",
                    label, attempt);
                return;
            }
            if ("failed".equals(group))
                throw new IOException(String.format("%s failed: %s",
                    label, status.get("code").asText()));
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", label));
    }

    // Returns restrictions currently applied to the mosaic
    // matching the given restriction key
    private List<JsonNode> getMosaicRestrictions(
        final String query,
        final long key
    ) throws IOException, InterruptedException {
        final String restrictionsPath = "/restrictions/mosaic?" + query;
        System.out.printf("  Getting restrictions from %s%n",
            restrictionsPath);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + restrictionsPath)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        final List<JsonNode> result = new ArrayList<>();
        final JsonNode data = JSON_MAPPER.readTree(response.body())
            .get("data");
        if (!data.isEmpty()) {
            // Look at the first returned restriction
            final JsonNode restrictions = data.get(0)
                .get("mosaicRestrictionEntry").get("restrictions");
            // Filter by key
            for (final JsonNode restriction : restrictions) {
                final long restrictionValue = Long.parseUnsignedLong(
                    restriction.get("key").asText());
                if (restrictionValue == key)
                    result.add(restriction);
            }
        }
        System.out.printf("  Response: %s%n", result);
        return result;
    }

    private List<JsonNode> getMosaicGlobalRestrictions(
        final long queriedMosaicId,
        final long key
    ) throws IOException, InterruptedException {
        return getMosaicRestrictions(String.format(
            "mosaicId=%016X&entryType=1", queriedMosaicId), key);
    }

    private List<JsonNode> getMosaicAddressRestrictions(
        final long queriedMosaicId,
        final Address address,
        final long key
    ) throws IOException, InterruptedException {
        return getMosaicRestrictions(String.format(
            "mosaicId=%016X&entryType=0&targetAddress=%s",
            queriedMosaicId, address), key);
    }

    // Returns a transaction enabling a mosaic's global restriction
    private EmbeddedTransaction setGlobalRestrictionTransaction()
        throws IOException {
        final EmbeddedTransaction transaction =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new MosaicGlobalRestrictionTransactionV1Descriptor(
                    new UnresolvedMosaicId(mosaicId),
                    new UnresolvedMosaicId(0),
                    restrictionKey,
                    0,
                    1,
                    MosaicRestrictionType.NONE,
                    MosaicRestrictionType.GE),
                ownerKeyPair.getPublicKey());
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        return transaction;
    }

    // Returns a transaction setting an address restriction's value
    private EmbeddedTransaction addressRestrictionSetValue(
        final long previousValue,
        final long newValue,
        final Address address
    ) throws IOException {
        final EmbeddedTransaction transaction =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new MosaicAddressRestrictionTransactionV1Descriptor(
                    new UnresolvedMosaicId(mosaicId),
                    restrictionKey,
                    previousValue,
                    newValue,
                    address),
                ownerKeyPair.getPublicKey());
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        return transaction;
    }

    public static void main(final String[] args) {
        try {
            new MosaicRestrictions().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 privateKeyString = System.getenv().getOrDefault(
            "OWNER_PRIVATE_KEY", "0".repeat(64));
        ownerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(privateKeyString));
        ownerAddress = facade.network.publicKeyToAddress(
            ownerKeyPair.getPublicKey());
        System.out.printf("Owner address: %s%n", ownerAddress);

        targetAddress = new Address(System.getenv().getOrDefault(
            "TARGET_ADDRESS", "TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA"));
        System.out.printf("Target address: %s%n", targetAddress);

        mosaicId = Long.parseUnsignedLong(System.getenv().getOrDefault(
            "MOSAIC_ID", "6A5ACF2376E50D4A"), 16);
        System.out.printf("Mosaic ID: 0x%016X%n", mosaicId);

        final String restrictionName = System.getenv().getOrDefault(
            "RESTRICTION_NAME", "security_level");
        restrictionKey = Restriction.mosaicRestrictionGenerateKey(
            restrictionName);
        System.out.printf("Restriction name: \"%s\" (key: 0x%016X)%n",
            restrictionName, restrictionKey);

        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJson = JSON_MAPPER.readTree(feeResponse.body());
        final long feeMultiplier = Math.max(
            feeJson.get("medianFeeMultiplier").asLong(),
            feeJson.get("minFeeMultiplier").asLong());
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

        // Enable global restriction if required
        final List<EmbeddedTransaction> transactions = new ArrayList<>();
        System.out.println(
            "Checking if the global restriction is enabled:");
        final List<JsonNode> globalRestrictions =
            getMosaicGlobalRestrictions(mosaicId, restrictionKey);
        if (globalRestrictions.isEmpty()) {
            // Enable the global restriction
            System.out.println("+ Enabling global restriction");
            transactions.add(setGlobalRestrictionTransaction());

            // Enable the address restriction
            System.out.println("+ Authorizing owner account");
            transactions.add(addressRestrictionSetValue(
                0xFFFFFFFFFFFFFFFFL, 1, ownerAddress));
        }

        // Toggle target address restriction
        System.out.println(
            "Checking if target account is authorized:");
        final List<JsonNode> addressRestrictions =
            getMosaicAddressRestrictions(
                mosaicId, targetAddress, restrictionKey);
        long previousValue = 0xFFFFFFFFFFFFFFFFL;
        if (!addressRestrictions.isEmpty())
            previousValue = Long.parseUnsignedLong(
                addressRestrictions.get(0).get("value").asText());
        if (1 != previousValue) {
            // Enable the address restriction
            System.out.println("+ Authorizing target account");
            transactions.add(addressRestrictionSetValue(
                previousValue, 1, targetAddress));
        } else {
            // Disable the address restriction
            System.out.println("+ Deauthorizing target account");
            transactions.add(addressRestrictionSetValue(
                previousValue, 0, targetAddress));
        }

        // Build an aggregate transaction
        System.out.printf(
            "Bundling %d transaction(s) in an aggregate%n",
            transactions.size());
        final Transaction aggregateTransaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(transactions),
                    transactions,
                    null),
                ownerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign, announce and wait for confirmation
        final String aggregatePayload =
            SymbolTransactionFactory.attachSignature(
                aggregateTransaction,
                facade.signTransaction(ownerKeyPair,
                    aggregateTransaction));
        final String aggregateHash =
            facade.hashTransaction(aggregateTransaction).toString();
        announceTransaction(aggregatePayload, "aggregate");
        waitForConfirmation(aggregateHash, "aggregate");

        // Try to transfer the mosaic to the target address
        final Transaction testTransaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    targetAddress,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(mosaicId),
                        new Amount(1))),
                    null),
                ownerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);
        final String testPayload = SymbolTransactionFactory
            .attachSignature(
                testTransaction,
                facade.signTransaction(ownerKeyPair, testTransaction));
        final String testHash =
            facade.hashTransaction(testTransaction).toString();
        System.out.println("\nAttempting transfer to the target account");
        announceTransaction(testPayload, "test transfer");
        waitForConfirmation(testHash, "test transfer");

    }
}

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', '6A5ACF2376E50D4A'), 16)
print(f'Mosaic ID: 0x{mosaic_id:016X}')
restriction_name = os.getenv('RESTRICTION_NAME', 'security_level')
restriction_key = mosaic_restriction_generate_key(restriction_name)
print(f'Restriction name: "{restriction_name}"'
    f' (key: 0x{restriction_key:016X})')
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 ||
    '6A5ACF2376E50D4A'}`);
console.log(`Mosaic ID: 0x${mosaicId.toString(16)
    .toUpperCase().padStart(16, '0')}`);

const restrictionName = process.env.RESTRICTION_NAME || 'security_level';
const restrictionKey = mosaicRestrictionGenerateKey(restrictionName);
console.log(`Restriction name: "${restrictionName}" (key: 0x${
    restrictionKey.toString(16).toUpperCase().padStart(16, '0')})`);
        final String privateKeyString = System.getenv().getOrDefault(
            "OWNER_PRIVATE_KEY", "0".repeat(64));
        ownerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(privateKeyString));
        ownerAddress = facade.network.publicKeyToAddress(
            ownerKeyPair.getPublicKey());
        System.out.printf("Owner address: %s%n", ownerAddress);

        targetAddress = new Address(System.getenv().getOrDefault(
            "TARGET_ADDRESS", "TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA"));
        System.out.printf("Target address: %s%n", targetAddress);

        mosaicId = Long.parseUnsignedLong(System.getenv().getOrDefault(
            "MOSAIC_ID", "6A5ACF2376E50D4A"), 16);
        System.out.printf("Mosaic ID: 0x%016X%n", mosaicId);

        final String restrictionName = System.getenv().getOrDefault(
            "RESTRICTION_NAME", "security_level");
        restrictionKey = Restriction.mosaicRestrictionGenerateKey(
            restrictionName);
        System.out.printf("Restriction name: \"%s\" (key: 0x%016X)%n",
            restrictionName, restrictionKey);

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 using the SDK's function, which hashes the name with SHA3-256 and takes the first eight bytes of the hash. This approach allows applications to use human-readable names while producing deterministic keys. Any 64-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.

    # 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_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')
    // 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 medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);
        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJson = JSON_MAPPER.readTree(feeResponse.body());
        final long feeMultiplier = Math.max(
            feeJson.get("medianFeeMultiplier").asLong(),
            feeJson.get("minFeeMultiplier").asLong());
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

Recommended fees are fetched from /network/fees/transaction GET, 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(set_global_restriction_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 (0 === globalRestrictions.length) {
        // 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()));
    }
        // Enable global restriction if required
        final List<EmbeddedTransaction> transactions = new ArrayList<>();
        System.out.println(
            "Checking if the global restriction is enabled:");
        final List<JsonNode> globalRestrictions =
            getMosaicGlobalRestrictions(mosaicId, restrictionKey);
        if (globalRestrictions.isEmpty()) {
            // Enable the global restriction
            System.out.println("+ Enabling global restriction");
            transactions.add(setGlobalRestrictionTransaction());

            // Enable the address restriction
            System.out.println("+ Authorizing owner account");
            transactions.add(addressRestrictionSetValue(
                0xFFFFFFFFFFFFFFFFL, 1, ownerAddress));
        }

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 = []
    url = f'{NODE_URL}{restrictions_path}'
    with urllib.request.urlopen(url) as restr_response:
        status = json.loads(restr_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]
    print(f'  Response: {res}')
    return res


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

function getMosaicGlobalRestrictions(queriedMosaicId, key) {
    return getMosaicRestrictions(
        `mosaicId=${queriedMosaicId.toString(16)
            .toUpperCase().padStart(16, '0')}` +
        '&entryType=1', key);
}
    private List<JsonNode> getMosaicRestrictions(
        final String query,
        final long key
    ) throws IOException, InterruptedException {
        final String restrictionsPath = "/restrictions/mosaic?" + query;
        System.out.printf("  Getting restrictions from %s%n",
            restrictionsPath);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + restrictionsPath)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        final List<JsonNode> result = new ArrayList<>();
        final JsonNode data = JSON_MAPPER.readTree(response.body())
            .get("data");
        if (!data.isEmpty()) {
            // Look at the first returned restriction
            final JsonNode restrictions = data.get(0)
                .get("mosaicRestrictionEntry").get("restrictions");
            // Filter by key
            for (final JsonNode restriction : restrictions) {
                final long restrictionValue = Long.parseUnsignedLong(
                    restriction.get("key").asText());
                if (restrictionValue == key)
                    result.add(restriction);
            }
        }
        System.out.printf("  Response: %s%n", result);
        return result;
    }

    private List<JsonNode> getMosaicGlobalRestrictions(
        final long queriedMosaicId,
        final long key
    ) throws IOException, InterruptedException {
        return getMosaicRestrictions(String.format(
            "mosaicId=%016X&entryType=1", queriedMosaicId), 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 .

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_address_restrictions(
    queried_mosaic_id, address, key):
    return get_mosaic_restrictions(
        f'mosaicId={queried_mosaic_id:016X}&'
        f'entryType=0&targetAddress={address}',
        key)
function getMosaicAddressRestrictions(queriedMosaicId, address, key) {
    return getMosaicRestrictions(
        `mosaicId=${queriedMosaicId.toString(16)
            .toUpperCase().padStart(16, '0')}` +
        `&entryType=0&targetAddress=${address}`, key);
}
    private List<JsonNode> getMosaicAddressRestrictions(
        final long queriedMosaicId,
        final Address address,
        final long key
    ) throws IOException, InterruptedException {
        return getMosaicRestrictions(String.format(
            "mosaicId=%016X&entryType=0&targetAddress=%s",
            queriedMosaicId, 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 .

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 (0 < addressRestrictions.length)
        prevValue = BigInt(addressRestrictions[0].value);
    if (1n !== prevValue) {
        // 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));
    }
        System.out.println(
            "Checking if target account is authorized:");
        final List<JsonNode> addressRestrictions =
            getMosaicAddressRestrictions(
                mosaicId, targetAddress, restrictionKey);
        long previousValue = 0xFFFFFFFFFFFFFFFFL;
        if (!addressRestrictions.isEmpty())
            previousValue = Long.parseUnsignedLong(
                addressRestrictions.get(0).get("value").asText());
        if (1 != previousValue) {
            // Enable the address restriction
            System.out.println("+ Authorizing target account");
            transactions.add(addressRestrictionSetValue(
                previousValue, 1, targetAddress));
        } else {
            // Disable the address restriction
            System.out.println("+ Deauthorizing target account");
            transactions.add(addressRestrictionSetValue(
                previousValue, 0, 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 , 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')
    aggregate_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                transactions),
            'transactions': transactions
        },
        owner_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    console.log('Bundling', transactions.length,
        'transaction(s) in an aggregate');
    const aggregateTransaction =
        facade.createTransactionFromTypedDescriptor(
            new descriptors.AggregateCompleteTransactionV3Descriptor(
                facade.static.hashEmbeddedTransactions(transactions),
                transactions,
                undefined),
            ownerKeyPair.publicKey,
            feeMultiplier,
            2 * 60 * 60);
        System.out.printf(
            "Bundling %d transaction(s) in an aggregate%n",
            transactions.size());
        final Transaction aggregateTransaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(transactions),
                    transactions,
                    null),
                ownerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

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.

    aggregate_payload = facade.transaction_factory.attach_signature(
        aggregate_transaction,
        facade.sign_transaction(owner_key_pair, aggregate_transaction))
    aggregate_hash = facade.hash_transaction(aggregate_transaction)
    announce_transaction(aggregate_payload, 'aggregate')
    wait_for_confirmation(aggregate_hash, 'aggregate')
    const aggregatePayload = SymbolTransactionFactory.attachSignature(
        aggregateTransaction,
        facade.signTransaction(ownerKeyPair, aggregateTransaction));
    const aggregateHash =
        facade.hashTransaction(aggregateTransaction).toString();
    await announceTransaction(aggregatePayload, 'aggregate');
    await waitForConfirmation(aggregateHash, 'aggregate');
        final String aggregatePayload =
            SymbolTransactionFactory.attachSignature(
                aggregateTransaction,
                facade.signTransaction(ownerKeyPair,
                    aggregateTransaction));
        final String aggregateHash =
            facade.hashTransaction(aggregateTransaction).toString();
        announceTransaction(aggregatePayload, "aggregate");
        waitForConfirmation(aggregateHash, "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.

    test_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address': target_address,
            'mosaics': [{
                'mosaic_id': mosaic_id,
                'amount': 1
            }]
        },
        owner_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    test_payload = facade.transaction_factory.attach_signature(
        test_transaction,
        facade.sign_transaction(owner_key_pair, test_transaction))
    test_hash = facade.hash_transaction(test_transaction)
    print('\nAttempting transfer to the target account')
    announce_transaction(test_payload, 'test transfer')
    wait_for_confirmation(test_hash, 'test transfer')
    const testTransaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV1Descriptor(
            new SymbolFacade.Address(targetAddress),
            [
                new descriptors.UnresolvedMosaicDescriptor(
                    new models.UnresolvedMosaicId(mosaicId),
                    new models.Amount(1n))
            ],
            undefined),
        ownerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

    const testPayload = SymbolTransactionFactory.attachSignature(
        testTransaction,
        facade.signTransaction(ownerKeyPair, testTransaction));
    const testHash = facade.hashTransaction(testTransaction).toString();
    console.log('\nAttempting transfer to the target account');
    await announceTransaction(testPayload, 'test transfer');
    await waitForConfirmation(testHash, 'test transfer');
        final Transaction testTransaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    targetAddress,
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(mosaicId),
                        new Amount(1))),
                    null),
                ownerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);
        final String testPayload = SymbolTransactionFactory
            .attachSignature(
                testTransaction,
                facade.signTransaction(ownerKeyPair, testTransaction));
        final String testHash =
            facade.hashTransaction(testTransaction).toString();
        System.out.println("\nAttempting transfer to the target account");
        announceTransaction(testPayload, "test transfer");
        waitForConfirmation(testHash, "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: 0x6A5ACF2376E50D4A
Restriction name: "security_level" (key: 0xE08F1643881FD0C1)
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Checking if the global restriction is enabled:
  Getting restrictions from /restrictions/mosaic?mosaicId=6A5ACF2376E50D4A&entryType=1
  Response: []
+ Enabling global restriction
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16721,
  "mosaic_id": "7663665467149847882",
  "reference_mosaic_id": "0",
  "restriction_key": "16181176465467887809",
  "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": "7663665467149847882",
  "restriction_key": "16181176465467887809",
  "previous_restriction_value": "18446744073709551615",
  "new_restriction_value": "1",
  "target_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9"
}
Checking if target account is authorized:
  Getting restrictions from /restrictions/mosaic?mosaicId=6A5ACF2376E50D4A&entryType=0&targetAddress=TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
  Response: []
+ Authorizing target account
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16977,
  "mosaic_id": "7663665467149847882",
  "restriction_key": "16181176465467887809",
  "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 8 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 25 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 10 (Response: []): The mosaic currently has no global restrictions.
  • Line 11: 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 25: 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 39 (Response: []): The target account is currently unauthorized because it has no value associated with the restriction key.
  • Line 40: 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 70 (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: 0x6A5ACF2376E50D4A
Restriction name: "security_level" (key: 0xE08F1643881FD0C1)
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Checking if the global restriction is enabled:
  Getting restrictions from /restrictions/mosaic?mosaicId=6A5ACF2376E50D4A&entryType=1
  Response: [{'key': '16181176465467887809', 'restriction': {'referenceMosaicId': '0000000000000000', 'restrictionValue': '1', 'restrictionType': 6}}]
Checking if target account is authorized:
  Getting restrictions from /restrictions/mosaic?mosaicId=6A5ACF2376E50D4A&entryType=0&targetAddress=TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
  Response: [{'key': '16181176465467887809', 'value': '1'}]
+ Deauthorizing target account
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16977,
  "mosaic_id": "7663665467149847882",
  "restriction_key": "16181176465467887809",
  "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 21 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 10 (Response: [ ... ]): Existing restrictions are detected.
  • Line 13 (Response: [ ... ]): The target account has a restriction value of 1, meaning it is authorized.
  • Line 14: 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 41 (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