コンテンツにスキップ

アカウントへの制限の追加⚓︎

上級

アカウントは、以下の項目に対して制限を課すことができます。

  • インタラクション可能な他の アカウント
  • 取引可能な モザイク
  • 実行可能な操作(トランザクションタイプ)の種類

これらの制限はアカウント制限を使用して設定されます。

このチュートリアルでは、アカウントの 送信トランザクション を制限し、許可された単一のアドレスにのみトランザクションを送信できるようにする方法を実演します。

もし制限がすでに有効である場合は、代わりにその制限を解除する方法を実演します。

制限の有効化または無効化を行った後、未承認のアドレスに対してテスト用の転送トランザクションをアナウンスし、ネットワークがそれをどのように拒否するかを確認します。

モザイク制限との違い

Symbolは、このチュートリアルで説明するアカウントレベルの制限とは別に、モザイクレベルで定義される モザイク制限 もサポートしています。

これらは異なる仕組みです。異なるトランザクションタイプを使用して設定され、異なるルールに基づいて動作します。

アカウント制限はアカウントがインタラクションできるモザイクを制限でき、モザイク制限はモザイクとインタラクションできるアカウントを制限できるため、概念的な重複が混乱の元となることがよくあります。

前提条件⚓︎

開始する前に、以下を確認してください。

さらに、トランザクションがどのようにアナウンスされ承認されるかを理解するために、転送トランザクション のチュートリアルを復習してください。

完全なコード⚓︎

このチュートリアルの完全なコード一覧を以下に示します。 詳細な手順ごとの説明は次のセクションで行います。

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import Address, SymbolFacade
from symbolchain.sc import AccountRestrictionFlags

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

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 = Address('TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA')
print(f'Authorized address: {auth_address}')


# 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 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 restr_response:
            status = json.loads(restr_response.read().decode())
            found_restr = status['accountRestrictions']['restrictions']
            print(f'  Response: {found_restr}')
            return found_restr
    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():
    enable_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'account_address_restriction_transaction_v1',
            # 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]
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    print('Enabling the restriction with transaction:')
    print(json.dumps(enable_transaction.to_json(), indent=2))

    return enable_transaction



# Returns a transaction that removes a restriction from an account
def restriction_disable_transaction(restriction):
    disable_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'account_address_restriction_transaction_v1',
            # Lift restrictions for OUTGOING ADDRESSES
            'restriction_flags':
                AccountRestrictionFlags.ADDRESS |
                AccountRestrictionFlags.OUTGOING,
            # Remove all addresses currently restricted
            'restriction_deletions': [
                Address.from_decoded_address_hex_string(addr)
                for addr in restriction['values']
            ]
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    print('Disabling the restriction with transaction:')
    print(json.dumps(disable_transaction.to_json(), indent=2))

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

    # 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 ---')
        agg_transaction = restriction_enable_transaction()
    else:
        # Disable the restriction
        print('\n--- Disabling restriction ---')
        agg_transaction = restriction_disable_transaction(
            restrictions[0])

    # Sign, announce and wait for confirmation
    json_payload = facade.transaction_factory.attach_signature(
        agg_transaction,
        facade.sign_transaction(signer_key_pair, agg_transaction))
    transaction_hash = facade.hash_transaction(agg_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.create_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address': Address(
                'TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y')
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    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 {
    Address,
    KeyPair,
    SymbolFacade,
    descriptors,
    models
} 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 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 = new Address('TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA');
console.log(`Authorized address: ${authAddress}`);


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

// Returns a transaction that restricts an account
function restrictionEnableTransaction(feeMultiplier) {
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AccountAddressRestrictionTransactionV1Descriptor(
            // Allow only OUTGOING transactions to the authorized ADDRESS
            models.AccountRestrictionFlags.ADDRESS.value |
                models.AccountRestrictionFlags.OUTGOING.value,
            // This is the only authorized outgoing address
            [authAddress],
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
    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(feeMultiplier, restriction) {
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AccountAddressRestrictionTransactionV1Descriptor(
            // Lift restrictions for OUTGOING ADDRESSES
            models.AccountRestrictionFlags.ADDRESS.value |
                models.AccountRestrictionFlags.OUTGOING.value,
            undefined,
            // Remove all addresses currently restricted
            restriction.values.map(hex =>
                Address.fromDecodedAddressHexString(hex))),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
    console.log('Disabling the restriction with transaction:');
    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);

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

    // Sign, announce and wait for confirmation
    let payload = facade.transactionFactory.static.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.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV1Descriptor(
            new Address('TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y'),
            undefined,
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
    payload = facade.transactionFactory.static.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

//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.SymbolTransactionFactory;
import org.symbol.sdk.symbol.descriptors.*;
import org.symbol.sdk.symbol.models.*;

public final class AccountRestrictions {
    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 signerKeyPair;

    private Address signerAddress;

    private final Address authAddress = new Address(
        "TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA");

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

    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 the restrictions currently applied to the account
    private JsonNode getAccountRestrictions(
        final Address address
    ) throws IOException, InterruptedException {
        final String restrictionsPath = String.format(
            "/restrictions/account/%s", address);
        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());
        if (2 != response.statusCode() / 100) {
            System.out.println("  Response: No restrictions found");
            return JSON_MAPPER.createArrayNode();
        }

        final JsonNode restrictions = JSON_MAPPER
            .readTree(response.body()).get("accountRestrictions")
            .get("restrictions");
        System.out.printf("  Response: %s%n", restrictions);
        return restrictions;
    }

    // Returns a transaction that restricts an account
    private Transaction restrictionEnableTransaction(
        final long feeMultiplier
    ) throws IOException {
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AccountAddressRestrictionTransactionV1Descriptor(
                    new AccountRestrictionFlags(
                        AccountRestrictionFlags.ADDRESS.value |
                        AccountRestrictionFlags.OUTGOING.value),
                    List.of(authAddress),
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);
        System.out.println("Enabling the restriction with transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        return transaction;
    }

    // Returns a transaction that removes a restriction from an account
    private Transaction restrictionDisableTransaction(
        final long feeMultiplier,
        final JsonNode restriction
    ) throws IOException {
        final List<Address> restrictionDeletions = new ArrayList<>();
        for (final JsonNode value : restriction.get("values"))
            restrictionDeletions.add(
                Address.fromDecodedAddressHexString(value.asText()));

        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AccountAddressRestrictionTransactionV1Descriptor(
                    new AccountRestrictionFlags(
                        AccountRestrictionFlags.ADDRESS.value |
                        AccountRestrictionFlags.OUTGOING.value),
                    null,
                    restrictionDeletions),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);
        System.out.println("Disabling the restriction with transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        return transaction;
    }

    public static void main(final String[] args) {
        try {
            new AccountRestrictions().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(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(privateKeyString));
        signerAddress = facade.network.publicKeyToAddress(
            signerKeyPair.getPublicKey());
        System.out.printf("Signer address: %s%n", signerAddress);
        System.out.printf("Authorized address: %s%n", authAddress);


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

        // Get current state of the restriction and decide which
        // operation to perform

        final JsonNode restrictions = getAccountRestrictions(
            signerAddress);
        Transaction transaction;
        if (restrictions.isEmpty()) {
            System.out.println("\n--- Enabling restriction ---");
            transaction = restrictionEnableTransaction(
                feeMultiplier);
        } else {
            System.out.println("\n--- Disabling restriction ---");
            transaction = restrictionDisableTransaction(
                feeMultiplier, restrictions.get(0));
        }

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


        // Try a dummy transfer to a random address with no mosaics
        transaction = facade.createTransactionFromTypedDescriptor(
            new TransferTransactionV1Descriptor(
                new Address("TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y"),
                null,
                null),
            signerKeyPair.getPublicKey(),
            feeMultiplier,
            2 * 60 * 60);
        payload = SymbolTransactionFactory.attachSignature(
            transaction,
            facade.signTransaction(signerKeyPair, transaction));
        hash = facade.hashTransaction(transaction).toString();
        System.out.println(
            "\n--- Attempting transfer to unauthorized address ---");
        announceTransaction(payload, "test transfer");
        waitForConfirmation(hash, "test transfer");

    }
}

Download source

コード解説⚓︎

コードは、2つのヘルパー関数の定義から始まります。 トランザクションのアナウンス方法や承認の追跡方法の詳細については、転送トランザクション のチュートリアルを参照してください。その他のヘルパー関数については、以下のセクションで説明します。

その後、チュートリアルは以下の手順で進みます。

アカウントがすでに制限されているかどうかに応じて、以下のいずれかのトランザクションが作成されます。

その後、トランザクションは アナウンスおよび承認 され、最後に テスト転送 が送信されます。

アカウントの設定⚓︎

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 = 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 = new Address('TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA');
console.log(`Authorized address: ${authAddress}`);
        final String privateKeyString = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(privateKeyString));
        signerAddress = facade.network.publicKeyToAddress(
            signerKeyPair.getPublicKey());
        System.out.printf("Signer address: %s%n", signerAddress);
        System.out.printf("Authorized address: %s%n", authAddress);

アカウントは自分自身に対してのみ制限を設定できるため、このチュートリアルでは単一の 秘密鍵 が必要です。 秘密鍵は SIGNER_PRIVATE_KEY 環境変数(64文字の16進数文字列)を通じて提供できます。提供されない場合は、デフォルト値が使用されます。

アカウントはトランザクションをアナウンスするのに十分な資金を保有している必要があります。デフォルトの鍵を使用する場合、対応するアカウントにはすでに資金が供給されている可能性があります。

この段階で、許可されたアドレスも設定されます。制限によって、後に送信トランザクションはこのアドレスのみに限定されます。

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

推奨手数料は /network/fees/transaction GET から取得します。 この手数料乗数は、後で各トランザクションの手数料を計算するために使用されます。

制限状態の検出⚓︎

以下の関数は、/restrictions/account/{address} GET エンドポイントを使用して、指定されたアドレスに適用されている現在のアカウント制限を取得します。制限が設定されていない場合、関数は空のリストを返します。

# 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 restr_response:
            status = json.loads(restr_response.read().decode())
            found_restr = status['accountRestrictions']['restrictions']
            print(f'  Response: {found_restr}')
            return found_restr
    except urllib.error.HTTPError:
        # The address has never been used
        print('  Response: No restrictions found')
    return []
// 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}`);
    const response = await fetch(`${NODE_URL}${restrictionsPath}`);
    if (!response.ok) {
        console.log('  Response: No restrictions found');
        return [];
    }
    const json = await response.json();
    const restrictions = json.accountRestrictions.restrictions;
    console.log('  Response:', restrictions);
    return restrictions;
}
    private JsonNode getAccountRestrictions(
        final Address address
    ) throws IOException, InterruptedException {
        final String restrictionsPath = String.format(
            "/restrictions/account/%s", address);
        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());
        if (2 != response.statusCode() / 100) {
            System.out.println("  Response: No restrictions found");
            return JSON_MAPPER.createArrayNode();
        }

        final JsonNode restrictions = JSON_MAPPER
            .readTree(response.body()).get("accountRestrictions")
            .get("restrictions");
        System.out.printf("  Response: %s%n", restrictions);
        return restrictions;
    }

返されたリストを評価して、チュートリアルの実行パスを決定します。その内容に基づいて、制限を有効化するか解除するか、適切な設定トランザクションが構築されます。

    # 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 ---')
        agg_transaction = restriction_enable_transaction()
    else:
        # Disable the restriction
        print('\n--- Disabling restriction ---')
        agg_transaction = restriction_disable_transaction(
            restrictions[0])
    // Get current state of the restriction and decide which
    // operation to perform
    const restrictions = await getAccountRestrictions(signerAddress);
    let transaction;
    if (0 === restrictions.length) {
        // Enable the restriction
        console.log('\n--- Enabling restriction ---');
        transaction = restrictionEnableTransaction(feeMultiplier);
    } else {
        // Disable the restriction
        console.log('\n--- Disabling restriction ---');
        transaction = restrictionDisableTransaction(
            feeMultiplier, restrictions[0]);
    }
        final JsonNode restrictions = getAccountRestrictions(
            signerAddress);
        Transaction transaction;
        if (restrictions.isEmpty()) {
            System.out.println("\n--- Enabling restriction ---");
            transaction = restrictionEnableTransaction(
                feeMultiplier);
        } else {
            System.out.println("\n--- Disabling restriction ---");
            transaction = restrictionDisableTransaction(
                feeMultiplier, restrictions.get(0));
        }

アカウントに複数の制限が設定されている場合、エンドポイントから返された最初の制限のみが削除されます。このチュートリアルの範囲内では、そのような状況は発生しないはずです。

制限の有効化⚓︎

アカウントがインタラクションできるアドレスのリストを制限するには、AccountAddressRestrictionTransactionV1 を使用します。

このチュートリアルでは扱いませんが、他の2つのアカウント制限タイプは以下の通りです。

# Returns a transaction that restricts an account
def restriction_enable_transaction():
    enable_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'account_address_restriction_transaction_v1',
            # 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]
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    print('Enabling the restriction with transaction:')
    print(json.dumps(enable_transaction.to_json(), indent=2))

    return enable_transaction
// Returns a transaction that restricts an account
function restrictionEnableTransaction(feeMultiplier) {
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AccountAddressRestrictionTransactionV1Descriptor(
            // Allow only OUTGOING transactions to the authorized ADDRESS
            models.AccountRestrictionFlags.ADDRESS.value |
                models.AccountRestrictionFlags.OUTGOING.value,
            // This is the only authorized outgoing address
            [authAddress],
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
    console.log('Enabling the restriction with transaction:');
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}
    private Transaction restrictionEnableTransaction(
        final long feeMultiplier
    ) throws IOException {
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AccountAddressRestrictionTransactionV1Descriptor(
                    new AccountRestrictionFlags(
                        AccountRestrictionFlags.ADDRESS.value |
                        AccountRestrictionFlags.OUTGOING.value),
                    List.of(authAddress),
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);
        System.out.println("Enabling the restriction with transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        return transaction;
    }

は、以下の引数を受け取ります。

  • トランザクションのディスクリプタ: AccountAddressRestrictionTransactionV1 と制限フィールドを定義します。
  • 署名者の公開鍵: 制限設定を変更するアカウントの 公開鍵
  • 手数料乗数: トランザクション手数料の計算に使用されます。
  • デッドラインの期間: 現在時刻から2時間後に設定されます。

トランザクションのディスクリプタには、以下のフィールドが含まれます。

  • restriction_flags: これらは AccountRestrictionFlags です。

    • ADDRESS は、制限がアドレスに適用されることを指定します。他の可能なスコープは MOSAIC_IDTRANSACTION_TYPE です。
    • OUTGOING は、送信トランザクションのみが影響を受けることを指定します。受信トランザクションの制限は、このフラグを除外することで独立して設定できます。

    デフォルトでは、リストされた値は「許可リスト(allowlist)」を形成します。指定されたアドレスのみが許可されます。

    リストされたアドレスを禁止する「拒否リスト(blocklist)」モードで制限を設定するには、BLOCK フラグを含めます。

    ネットワークはこれらのフラグを現在の値と XOR(排他的論理和)演算します。このチュートリアルでは、有効化する前に制限が存在しないことを確認しているため、この時点での現在の値は 0 です。

  • restriction_additions: 制限に追加するアドレス(またはモザイクID、トランザクションタイプ)のリスト。

    この例では、リストには許可されたアドレスのみが含まれます。

制限の解除⚓︎

制限を無効にするには、設定されているフラグとリストされたアドレスの両方をクリアする必要があります。

# Returns a transaction that removes a restriction from an account
def restriction_disable_transaction(restriction):
    disable_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'account_address_restriction_transaction_v1',
            # Lift restrictions for OUTGOING ADDRESSES
            'restriction_flags':
                AccountRestrictionFlags.ADDRESS |
                AccountRestrictionFlags.OUTGOING,
            # Remove all addresses currently restricted
            'restriction_deletions': [
                Address.from_decoded_address_hex_string(addr)
                for addr in restriction['values']
            ]
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    print('Disabling the restriction with transaction:')
    print(json.dumps(disable_transaction.to_json(), indent=2))

    return disable_transaction
// Returns a transaction that removes a restriction from an account
function restrictionDisableTransaction(feeMultiplier, restriction) {
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AccountAddressRestrictionTransactionV1Descriptor(
            // Lift restrictions for OUTGOING ADDRESSES
            models.AccountRestrictionFlags.ADDRESS.value |
                models.AccountRestrictionFlags.OUTGOING.value,
            undefined,
            // Remove all addresses currently restricted
            restriction.values.map(hex =>
                Address.fromDecodedAddressHexString(hex))),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
    console.log('Disabling the restriction with transaction:');
    console.dir(transaction.toJson(), { colors: true, depth: null });

    return transaction;
}
    private Transaction restrictionDisableTransaction(
        final long feeMultiplier,
        final JsonNode restriction
    ) throws IOException {
        final List<Address> restrictionDeletions = new ArrayList<>();
        for (final JsonNode value : restriction.get("values"))
            restrictionDeletions.add(
                Address.fromDecodedAddressHexString(value.asText()));

        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AccountAddressRestrictionTransactionV1Descriptor(
                    new AccountRestrictionFlags(
                        AccountRestrictionFlags.ADDRESS.value |
                        AccountRestrictionFlags.OUTGOING.value),
                    null,
                    restrictionDeletions),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);
        System.out.println("Disabling the restriction with transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        return transaction;
    }

は、トランザクションのディスクリプタ、署名者の公開鍵、手数料乗数、デッドラインの期間を受け取ります。

制限を有効にした時と同じ restriction_flags の値が再度提供されます。フラグはネットワークによって XOR されるため、同じ値を提供するとそれらがオフに切り替わり、実質的に制限がクリアされます。

現在制限に設定されているアドレスは restriction_deletions フィールドに指定され、設定から削除されます。

メソッドは、REST API から返される16進文字列形式を、トランザクション構築時に期待されるアドレス表現に変換します。

トランザクションの送信⚓︎

構築されたトランザクションは、転送トランザクション チュートリアルで説明されている通り、署名、アナウンス、承認されます。

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

テスト転送の送信⚓︎

その後、未承認のアドレスに対してテスト用の転送が試行されます。

    # Try a dummy transfer to a random address with no mosaics
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'transfer_transaction_v1',
            'recipient_address': Address(
                'TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y')
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    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.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV1Descriptor(
            new Address('TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y'),
            undefined,
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
    payload = facade.transactionFactory.static.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');
        // Try a dummy transfer to a random address with no mosaics
        transaction = facade.createTransactionFromTypedDescriptor(
            new TransferTransactionV1Descriptor(
                new Address("TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y"),
                null,
                null),
            signerKeyPair.getPublicKey(),
            feeMultiplier,
            2 * 60 * 60);
        payload = SymbolTransactionFactory.attachSignature(
            transaction,
            facade.signTransaction(signerKeyPair, transaction));
        hash = facade.hashTransaction(transaction).toString();
        System.out.println(
            "\n--- Attempting transfer to unauthorized address ---");
        announceTransaction(payload, "test transfer");
        waitForConfirmation(hash, "test transfer");

制限が有効になっている場合、転送は Address_Interaction_Prohibited エラーで失敗します。制限が解除されている場合、転送は正常に承認されます。

制限設定トランザクションとテスト転送は独立してアナウンスされ、承認されます。それぞれに個別の承認が必要なため、全体の実行時間が長くなる可能性があります。

このプロセスは、両方のトランザクションを単一の アグリゲートトランザクション に組み込んで一緒にアナウンスすることで最適化できます。

出力⚓︎

以下に示す出力は、プログラムの典型的な2つの実行結果に対応しています。

Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Authorized address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
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

出力の主なポイント:

  • 2-3行目: 関与するアカウントのアドレス。
  • 7行目 (Response: No restrictions found): 現在制限は設定されていません。
  • 19行目 ("restriction_flags": 16385): 0x4001ADDRESSOUTGOING の組み合わせに対応します。
  • 20-22行目 ("restriction_additions"): デコードされた16進数形式の、許可されたアドレスのリスト。この値は3行目に示されているアドレスに対応します。
  • 39行目 (test transfer failed): 期待通り、未承認の受信者アドレスにより Address_Interaction_Prohibited エラーが発生しています。
Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Authorized address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
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

出力の主なポイント:

  • 2-3行目: 関与するアカウントのアドレス。
  • 7行目 (Response: [ ... ]): 既存の制限が検出されました。
  • 19行目 (restriction_flags): 制限を有効にした時と同じフラグ値。
  • 21-23行目 (restriction_deletions): 以前に設定されていたアドレスが削除されます。
  • 42行目 (test transfer confirmed): 制限が解除されたため、転送が正常に承認されました。

出力に示されているトランザクションハッシュを使用して、Symbol Testnet Explorer でトランザクションを検索できます。

結論⚓︎

このチュートリアルでは、以下の方法を説明しました。

ステップ 関連ドキュメント
現在の制限設定の取得 /restrictions/account/{address} GET
制限の有効化 , AccountAddressRestrictionTransactionV1
制限の解除 , AccountAddressRestrictionTransactionV1
テスト転送の作成 , TransferTransactionV1