Skip to content

Adding Restrictions to an Account⚓︎

Accounts can enforce limits on:

  • which other Accounts they can interact with
  • which mosaics they can transact with
  • which types of operations they can perform

These limits are configured using account restrictions.

This tutorial demonstrates how to restrict an account's outgoing transactions so that it can only send transactions to a single authorized address.

If the restriction is already enabled, the tutorial instead demonstrates how to remove it.

After enabling or disabling the restriction, a test transfer transaction to an unauthorized address is announced, showing how the network rejects it.

Difference with Mosaic Restrictions

Symbol also supports mosaic restrictions, which are defined at the mosaic level rather than at the account 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 tutorial to understand how transactions are announced and confirmed.

Full Code⚓︎

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade, Address
from symbolchain.sc import Amount, AccountRestrictionFlags
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 the list of restrictions currently applied to the account
def get_account_restrictions(address):
    restrictions_path = f'/restrictions/account/{address}'
    print(f'Getting restrictions from {restrictions_path}')
    try:
        url = f'{NODE_URL}{restrictions_path}'
        with urllib.request.urlopen(url) as response:
            status = json.loads(response.read().decode())
            restrictions = status['accountRestrictions']['restrictions']
            print(f'  Response: {restrictions}')
            return restrictions
    except urllib.error.HTTPError:
        # The address has never been used
        print('  Response: No restrictions found')
    return []

# Returns a transaction that restricts an account
def restriction_enable_transaction():
    transaction = facade.transaction_factory.create({
        'type': 'account_address_restriction_transaction_v1',
        # This is the account that will be restricted
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        # Allow only OUTGOING transactions to the authorized ADDRESS
        'restriction_flags':
            AccountRestrictionFlags.ADDRESS |
            AccountRestrictionFlags.OUTGOING,
        # This is the only authorized outgoing address
        'restriction_additions': [auth_address]
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    print('Enabling the restriction with transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    return transaction

# Returns a transaction that removes a restriction from an account
def restriction_disable_transaction(restriction):
    transaction = facade.transaction_factory.create({
        'type': 'account_address_restriction_transaction_v1',
        # This is the account whose restriction will be lifted
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        # Reverse flags
        'restriction_flags': restriction['restrictionFlags'],
        # Remove all addresses currently restricted
        'restriction_deletions': [
            Address.from_decoded_address_hex_string(addr) for addr in
                restriction['values']
        ]
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    print('Disabling the restriction with transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    return transaction

facade = SymbolFacade('testnet')

SIGNER_PRIVATE_KEY = os.getenv('SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))
signer_address = facade.network.public_key_to_address(
    signer_key_pair.public_key)
print(f'Signer address: {signer_address}')

auth_address = 'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA'
print(f'Authorized address: {auth_address}')

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

    # Get current state of the restriction and decide which
    # operation to perform
    restrictions = get_account_restrictions(signer_address)
    if len(restrictions) == 0:
        # Enable the restriction
        print('\n--- Enabling restriction ---')
        transaction = restriction_enable_transaction()
    else:
        # Disable the restriction
        print('\n--- Disabling restriction ---')
        transaction = restriction_disable_transaction(restrictions[0])

    # Sign, announce and wait for confirmation
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(signer_key_pair, transaction))
    transaction_hash = facade.hash_transaction(transaction)
    announce_transaction(json_payload, 'restriction transaction')
    wait_for_confirmation(transaction_hash, 'restriction transaction')

    # Try a dummy transfer to a random address with no mosaics
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'recipient_address': 'TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y'
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(signer_key_pair, transaction))
    transaction_hash = facade.hash_transaction(transaction)
    print('\n--- Attempting transfer to unauthorized address ---')
    announce_transaction(json_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,
    Address,
    NetworkTimestamp,
    SymbolFacade
} from 'symbol-sdk/symbol';

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 the list of restrictions currently applied to the account
async function getAccountRestrictions(address) {
    const restrictionsPath = `/restrictions/account/${address}`;
    console.log(`Getting restrictions from ${restrictionsPath}`);
    try {
        const response = await fetch(`${NODE_URL}${restrictionsPath}`);
        const json = await response.json();
        const restrictions = json.accountRestrictions.restrictions;
        console.log('  Response:', restrictions);
        return restrictions;
    } catch {
        console.log('  Response: No restrictions found');
        return [];
    }
}

// Returns a transaction that restricts an account
function restrictionEnableTransaction(timestamp, feeMult) {
    const transaction = facade.transactionFactory.create({
        type: 'account_address_restriction_transaction_v1',
        // This is the account that will be restricted
        signerPublicKey: signerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        // Allow only OUTGOING transactions to the authorized ADDRESS
        restrictionFlags:
            models.AccountRestrictionFlags.ADDRESS.value |
            models.AccountRestrictionFlags.OUTGOING.value,
        // This is the only authorized outgoing address
        restrictionAdditions: [authAddress]
    });
    transaction.fee = new models.Amount(feeMult * transaction.size);
    console.log('Enabling the restriction with transaction:');
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}

// Returns a transaction that removes a restriction from an account
function restrictionDisableTransaction(timestamp, feeMult, restriction) {
    const transaction = facade.transactionFactory.create({
        type: 'account_address_restriction_transaction_v1',
        // This is the account whose restriction will be lifted
        signerPublicKey: signerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        // Reverse flags
        restrictionFlags: restriction.restrictionFlags,
        // Remove all addresses currently restricted
        restrictionDeletions: restriction.values.map(hex =>
            Address.fromDecodedAddressHexString(hex))
    });
    transaction.fee = new models.Amount(feeMult * transaction.size);
    console.log('Disabling the restriction with transaction:');
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}

const facade = new SymbolFacade('testnet');

const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new KeyPair(new PrivateKey(SIGNER_PRIVATE_KEY));
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log(`Signer address: ${signerAddress}`);

const authAddress = 'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA';
console.log(`Authorized address: ${authAddress}`);

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

    // Get current state of the restriction and decide which
    // operation to perform
    const restrictions = await getAccountRestrictions(signerAddress);
    let transaction;
    if (restrictions.length === 0) {
        // Enable the restriction
        console.log('\n--- Enabling restriction ---');
        transaction = restrictionEnableTransaction(timestamp, feeMult);
    } else {
        // Disable the restriction
        console.log('\n--- Disabling restriction ---');
        transaction = restrictionDisableTransaction(timestamp, feeMult,
            restrictions[0]);
    }

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

    // Try a dummy transfer to a random address with no mosaics
    transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        recipientAddress: 'TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y'
    });
    transaction.fee = new models.Amount(feeMult * transaction.size);
    payload = SymbolTransactionFactory.attachSignature(
        transaction,
        facade.signTransaction(signerKeyPair, transaction));
    hash = facade.hashTransaction(transaction).toString();
    console.log('\n--- Attempting transfer to unauthorized address ---');
    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 two helper functions. For details on how transactions are announced and how their confirmation is tracked, refer to Transfer transaction tutorial. The remaining helper functions are described in the sections below.

The tutorial then proceeds to:

Depending on whether the account is already restricted, a transaction is created to either:

The transaction is then announced and confirmed, and finally, a test transfer is submitted.

Setting Up the Accounts⚓︎

SIGNER_PRIVATE_KEY = os.getenv('SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))
signer_address = facade.network.public_key_to_address(
    signer_key_pair.public_key)
print(f'Signer address: {signer_address}')

auth_address = 'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA'
print(f'Authorized address: {auth_address}')
const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new KeyPair(new PrivateKey(SIGNER_PRIVATE_KEY));
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log(`Signer address: ${signerAddress}`);

const authAddress = 'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA';
console.log(`Authorized address: ${authAddress}`);

An account can only configure restrictions on itself, so this tutorial requires a single private key. The private key can be provided through the SIGNER_PRIVATE_KEY environment variable (as a 64-character hexadecimal string). If it is not provided, a default value is used.

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

At this stage, the authorized address is also configured. The restriction will later limit outgoing transactions to this address only.

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.

Detecting the Restriction State⚓︎

The following function retrieves the current account restrictions applied to a given address using the /restrictions/account/{address} GET endpoint. If no restrictions are configured, the function returns an empty list.

def get_account_restrictions(address):
    restrictions_path = f'/restrictions/account/{address}'
    print(f'Getting restrictions from {restrictions_path}')
    try:
        url = f'{NODE_URL}{restrictions_path}'
        with urllib.request.urlopen(url) as response:
            status = json.loads(response.read().decode())
            restrictions = status['accountRestrictions']['restrictions']
            print(f'  Response: {restrictions}')
            return restrictions
    except urllib.error.HTTPError:
        # The address has never been used
        print('  Response: No restrictions found')
    return []
async function getAccountRestrictions(address) {
    const restrictionsPath = `/restrictions/account/${address}`;
    console.log(`Getting restrictions from ${restrictionsPath}`);
    try {
        const response = await fetch(`${NODE_URL}${restrictionsPath}`);
        const json = await response.json();
        const restrictions = json.accountRestrictions.restrictions;
        console.log('  Response:', restrictions);
        return restrictions;
    } catch {
        console.log('  Response: No restrictions found');
        return [];
    }
}

The returned list is then evaluated to determine the tutorial's execution path. Based on its contents, the appropriate configuration transaction is constructed, either to enable or to remove the restriction.

    restrictions = get_account_restrictions(signer_address)
    if len(restrictions) == 0:
        # Enable the restriction
        print('\n--- Enabling restriction ---')
        transaction = restriction_enable_transaction()
    else:
        # Disable the restriction
        print('\n--- Disabling restriction ---')
        transaction = restriction_disable_transaction(restrictions[0])
    const restrictions = await getAccountRestrictions(signerAddress);
    let transaction;
    if (restrictions.length === 0) {
        // Enable the restriction
        console.log('\n--- Enabling restriction ---');
        transaction = restrictionEnableTransaction(timestamp, feeMult);
    } else {
        // Disable the restriction
        console.log('\n--- Disabling restriction ---');
        transaction = restrictionDisableTransaction(timestamp, feeMult,
            restrictions[0]);
    }

If multiple restrictions are configured on the account, only the first one returned by the endpoint is removed. This situation should not occur in this tutorial.

Enabling the Restriction⚓︎

To restrict the list of addresses the account can interact with, an AccountAddressRestrictionTransaction is used.

The other two account restriction types, not covered in this tutorial, are:

  • AccountMosaicRestrictionTransaction
  • AccountOperationRestrictionTransaction
def restriction_enable_transaction():
    transaction = facade.transaction_factory.create({
        'type': 'account_address_restriction_transaction_v1',
        # This is the account that will be restricted
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        # Allow only OUTGOING transactions to the authorized ADDRESS
        'restriction_flags':
            AccountRestrictionFlags.ADDRESS |
            AccountRestrictionFlags.OUTGOING,
        # This is the only authorized outgoing address
        'restriction_additions': [auth_address]
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    print('Enabling the restriction with transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    return transaction
function restrictionEnableTransaction(timestamp, feeMult) {
    const transaction = facade.transactionFactory.create({
        type: 'account_address_restriction_transaction_v1',
        // This is the account that will be restricted
        signerPublicKey: signerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        // Allow only OUTGOING transactions to the authorized ADDRESS
        restrictionFlags:
            models.AccountRestrictionFlags.ADDRESS.value |
            models.AccountRestrictionFlags.OUTGOING.value,
        // This is the only authorized outgoing address
        restrictionAdditions: [authAddress]
    });
    transaction.fee = new models.Amount(feeMult * transaction.size);
    console.log('Enabling the restriction with transaction:');
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}

The transaction includes the following fields:

  • signer_public_key: public key of the account whose restriction configuration will be modified.

  • restriction_flags:

    • AccountRestrictionFlags.ADDRESS specifies that the restriction applies to addresses. Other possible scopes are MOSAIC_ID and TRANSACTION_TYPE.
    • AccountRestrictionFlags.OUTGOING specifies that only outgoing transactions are affected. Incoming transaction restrictions can be configured independently by omitting this flag.

    By default, the listed values form an allowlist. Only the specified addresses are allowed.

    To configure the restriction in blocklist mode, where the listed addresses are forbidden, include the AccountRestrictionFlags.BLOCK flag.

    The network XOR's these flags with the current value, which at this point is 0 because the tutorial makes sure no restriction is present before enabling it.

  • restriction_additions: list of addresses (or mosaic IDs, or transaction types) to be added to the restriction.

    In this case, the list contains only the authorized address.

Removing the Restriction⚓︎

Disabling the restriction requires clearing both the configured flags and the listed addresses.

def restriction_disable_transaction(restriction):
    transaction = facade.transaction_factory.create({
        'type': 'account_address_restriction_transaction_v1',
        # This is the account whose restriction will be lifted
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        # Reverse flags
        'restriction_flags': restriction['restrictionFlags'],
        # Remove all addresses currently restricted
        'restriction_deletions': [
            Address.from_decoded_address_hex_string(addr) for addr in
                restriction['values']
        ]
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    print('Disabling the restriction with transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    return transaction
function restrictionDisableTransaction(timestamp, feeMult, restriction) {
    const transaction = facade.transactionFactory.create({
        type: 'account_address_restriction_transaction_v1',
        // This is the account whose restriction will be lifted
        signerPublicKey: signerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        // Reverse flags
        restrictionFlags: restriction.restrictionFlags,
        // Remove all addresses currently restricted
        restrictionDeletions: restriction.values.map(hex =>
            Address.fromDecodedAddressHexString(hex))
    });
    transaction.fee = new models.Amount(feeMult * transaction.size);
    console.log('Disabling the restriction with transaction:');
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}

The same restriction_flags values used when enabling the restriction are provided again. Because the flags are XOR'ed by the network, supplying the same values toggles them off, effectively clearing the restriction.

The addresses currently configured in the restriction are supplied in the restriction_deletions field so they can be removed from the configuration.

The method converts the hexadecimal string format returned by the REST API into the address representation expected when constructing a transaction.

Submitting the Transaction⚓︎

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

    # Sign, announce and wait for confirmation
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(signer_key_pair, transaction))
    transaction_hash = facade.hash_transaction(transaction)
    announce_transaction(json_payload, 'restriction transaction')
    wait_for_confirmation(transaction_hash, 'restriction transaction')
    // Sign, announce and wait for confirmation
    let payload = SymbolTransactionFactory.attachSignature(
        transaction,
        facade.signTransaction(signerKeyPair, transaction));
    let hash =
        facade.hashTransaction(transaction).toString();
    await announceTransaction(payload, 'restriction transaction');
    await waitForConfirmation(hash, 'restriction transaction');

Sending a Test Transfer⚓︎

A test transfer is then attempted to an unauthorized address.

    # Try a dummy transfer to a random address with no mosaics
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'recipient_address': 'TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y'
    })
    transaction.fee = Amount(fee_mult * transaction.size)
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(signer_key_pair, transaction))
    transaction_hash = facade.hash_transaction(transaction)
    print('\n--- Attempting transfer to unauthorized address ---')
    announce_transaction(json_payload, 'test transfer')
    wait_for_confirmation(transaction_hash, 'test transfer')
    // Try a dummy transfer to a random address with no mosaics
    transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        recipientAddress: 'TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y'
    });
    transaction.fee = new models.Amount(feeMult * transaction.size);
    payload = SymbolTransactionFactory.attachSignature(
        transaction,
        facade.signTransaction(signerKeyPair, transaction));
    hash = facade.hashTransaction(transaction).toString();
    console.log('\n--- Attempting transfer to unauthorized address ---');
    await announceTransaction(payload, 'test transfer');
    await waitForConfirmation(hash, 'test transfer');

If the restriction has been enabled, the transfer fails with an Address_Interaction_Prohibited error. If the restriction has been removed, the transfer is confirmed successfully.

The restriction configuration transaction and the test transfer are announced and confirmed independently. Each requires its own confirmation, which may increase the total execution time.

The process could be optimized by embedding both transactions in a single aggregate transaction and announcing them together.

Output⚓︎

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

Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Authorized address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Fetching current network time from /node/time
  Network time: 104098647293 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Getting restrictions from /restrictions/account/TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
  Response: No restrictions found

--- Enabling restriction ---
Enabling the restriction with transaction:
{
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16720,
  "fee": "16000",
  "deadline": "104105847293",
  "restriction_flags": 16385,
  "restriction_additions": [
    "987D075454716222F609929E883174AD8C996D5828C938BC"
  ],
  "restriction_deletions": []
}
Announcing restriction transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for restriction transaction confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  ...
  Transaction status: confirmed
restriction transaction confirmed in 6 seconds

--- Attempting transfer to unauthorized address ---
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_RestrictionAccount_Address_Interaction_Prohibited

Key points in the output:

  • Lines 2-3: Addresses of the involved accounts.
  • Line 9 (Response: No restrictions found): No restrictions are currently configured.
  • Line 21 ("restriction_flags": 16385): 0x4001 corresponds to the combination of ADDRESS and OUTGOING.
  • Line 22-24 ("restriction_additions"): List of allowed addresses, in decoded hexadecimal format. The value corresponds to the address shown in line 3.
  • Line 41 (test transfer failed): The unauthorized recipient address results in an Address_Interaction_Prohibited error, as expected.
Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Authorized address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Fetching current network time from /node/time
  Network time: 104098759668 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Getting restrictions from /restrictions/account/TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
  Response: [{'restrictionFlags': 16385, 'values': ['987D075454716222F609929E883174AD8C996D5828C938BC']}]

--- Disabling restriction ---
Disabling the restriction with transaction:
{
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16720,
  "fee": "16000",
  "deadline": "104105959668",
  "restriction_flags": 16385,
  "restriction_additions": [],
  "restriction_deletions": [
    "987D075454716222F609929E883174AD8C996D5828C938BC"
  ]
}
Announcing restriction transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for restriction transaction confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  ...
  Transaction status: confirmed
restriction transaction confirmed in 2 seconds

--- Attempting transfer to unauthorized address ---
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 23 seconds

Key points in the output:

  • Lines 2-3: Addresses of the involved accounts.
  • Line 9 (Response: [ ... ]): Existing restrictions are detected.
  • Line 21 (restriction_flags): Same flag value used when enabling the restriction.
  • Line 23-25 (restriction_deletions): The previously configured address is removed.
  • Line 44 (test transfer confirmed): The transfer is confirmed successfully because the restriction has been lifted.

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

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Retrieve the current restriction configuration /restrictions/account/{address} GET
Enable a restriction AccountAddressRestrictionTransaction
Remove a restriction AccountAddressRestrictionTransaction