コンテンツにスキップ

モザイクへの制限の追加⚓︎

上級

モザイク の所有者は、そのモザイクを取引できる アカウント を制限できます。これらの条件は モザイク制限 と呼ばれ、以下の2つの部分で構成されます。

アカウントは、自身に割り当てられた値がモザイクのすべてのグローバル条件を満たしている場合にのみ、そのモザイクを取引できます。

このチュートリアルには、制限可能 (restrictable)フラグを設定して作成された既存のモザイクが必要です。もしモザイクにまだグローバル制限が定義されていない場合、このチュートリアルでは以下の構成で制限を作成します。

キー 値 関係
security_level 1 以上 (greater-or-equal)

この構成は、security_level の制限値が 1以上 であるアカウントのみがそのモザイクを使用できることを意味します。

その後、チュートリアルではこのキーをテストアカウントに割り当てるか、すでに存在する場合は値を 1 と 0 の間で切り替え、所有者アカウントからテストアカウントへモザイクの転送を試みます。

結果として、プログラムを実行するたびに、成功と 制限違反(restriction violation) エラーによる失敗が交互に繰り返されます。

制限の設定には複数のトランザクションが必要なため、このチュートリアルではそれらを単一の コンプリートアグリゲートトランザクション にまとめて。これにより、各トランザクションが個別に承認されるのを待つ必要がなくなります。

アカウント制限との違い

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

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

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

前提条件⚓︎

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

さらに、トランザクションがどのようにアナウンスされ承認されるかを理解するために 転送トランザクション を、複数のトランザクションをまとめる方法を理解するために コンプリートアグリゲートトランザクションの作成 のチュートリアルを復習しておいてください。

完全なコード⚓︎

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

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

コード解説⚓︎

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

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

アカウントの設定⚓︎

チュートリアルは、この例に関与するアカウントの設定から始まります。

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

コードでは以下を定義しています。

  • 所有者アカウント: モザイクを制御し、その制限を設定する責任を持ちます。その秘密鍵は OWNER_PRIVATE_KEY 環境変数から64文字の16進数文字列として提供できます。
  • ターゲットアカウント: 後にモザイクとの取引許可を受け取るアカウント。そのアドレスは TARGET_ADDRESS 環境変数から Symbol テストネットアドレスとして提供できます。
  • モザイク識別子: MOSAIC_ID から16進数16文字として読み込まれます。
  • 制限名: RESTRICTION_NAME から文字列として読み込まれます。
  • 対応する制限キー: SDK の 関数を使用して制限名から派生させます。 この関数は制限名を SHA3-256 でハッシュ化し、先頭8バイトを取得します。 この方法により、人間が読みやすい名前を使いながら決定論的なキーを生成できます。 制限キーとして任意の64ビット整数を直接使用することも可能です。

これらの値がいずれも環境変数を通じて提供されない場合は、デフォルト値が使用されます。

所有者アカウントはトランザクションをアナウンスするのに十分な資金を保有している必要があります。

    # 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 から取得します。

グローバル制限の有効化⚓︎

コードはまず、モザイクに設定されたキーに対してグローバル制限がすでに定義されているかどうかを確認します。

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

これは /restrictions/mosaic GET を照会し、mosaicId と entryType=1 (グローバル制限 を選択)でフィルタリングすることによって行われます。返されたエントリはさらに、選択された restriction_key に関するものだけに絞り込まれます。

制限が見つからない場合は、アナウンスするトランザクションリストに2つのトランザクションを追加して制限を作成します。

  • 制限条件を定義する モザイクグローバル制限トランザクション。各フィールドの詳細については、 MosaicGlobalRestrictionTransactionV1 シリアライズテーブルを参照してください。 このチュートリアルで作成される制限は、security_level キーに関連付けられた値が 1以上 であることを要求します。

  • 所有者アカウントを許可する モザイクアドレス制限トランザクション。各フィールドの詳細については、 MosaicAddressRestrictionTransactionV1 シリアライズテーブルを参照してください。 コードは所有者の security_level に値 1 を割り当て、所有者アカウントが自身のモザイクを継続して取引できるようにします。

    メモ

    簡単にするため、チュートリアルではグローバル制限が存在しない場合、所有者アカウントにもアドレス制限がないと仮定しています。

    このため、以前に値が設定されていなかったことを示す 0xFFFFFFFF_FFFFFFFF を「以前の値(previous value)」として使用します。

    より堅牢な実装では、まず所有者の制限状態を照会し、ターゲットアカウントに対して以下に示すように、適切な「以前の値」を使用する必要があります。

アドレス制限の切り替え⚓︎

グローバル制限が有効な状態で、次のステップではターゲットアカウントにそのキーの制限値がすでに定義されているかどうかを確認します。

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

グローバル制限の場合と同様に、現在の値は /restrictions/mosaic GET を照会し、mosaicId、targetAddress、および entryType=0 (アドレス制限 を選択)でフィルタリングすることで取得されます。

ターゲットアカウントの制限の現在値に応じて、アカウントを許可または禁止するトランザクションが作成されます。このトランザクションは、アナウンスするトランザクションリストに追加されます。

    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));
        }
  • アカウントにまだ制限値がない、または値が 1 でない場合、コードは値 1 を割り当て、モザイクの使用を許可します。
  • アカウントにすでに値 1 が設定されている場合、コードはそれを 0 に置き換え、許可を取り消します。

したがって、このチュートリアルを繰り返し実行すると、ターゲットアカウントの許可と禁止が交互に切り替わります。

restriction_key でフィルタリングした後は、リストが空であるか、単一のエントリが含まれているかのどちらかであるため、返されたリストの最初の制限のみが検査されます。

どちらの場合も同じ MosaicAddressRestrictionTransactionV1 が使用され、制限に割り当てられる値のみが変更されます。

以前の制限が存在しない場合は、特別な値 0xFFFFFFFF_FFFFFFFF を「以前の値」として使用する必要があります。

アグリゲートトランザクションの構築⚓︎

上記で作成されたすべての設定トランザクションは、単一の コンプリートアグリゲートトランザクション に結論られます。これにより、ユーザーは各トランザクションが個別に承認されるのを待つ必要がなくなります。

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

手数料を支払うのはアグリゲートトランザクションのみであるため、 埋め込みトランザクション では fee フィールドを使用しません。

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

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

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

テスト転送の送信⚓︎

最後に、チュートリアルでは標準的な 転送トランザクション を使用して、所有者アカウントからターゲットアカウントへモザイク1ユニットの送信を試みます。

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

ターゲットアカウントが現在制限を満たしている(security_level ≥ 1)場合、転送は正常に承認されます。

制限値が 0 に切り替えられていた場合、トランザクションは Account_Unauthorized エラーで失敗します。

チュートリアルを複数回実行することで、転送の成功と失敗が交互に発生し、モザイク制限がどのアカウントに取引を許可するかをどのように制御しているかが実証されます。

出力⚓︎

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

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

出力の主なポイント:

  • 2-3行目: 関与するアカウントのアドレス。
  • 4行目: 制限されるモザイク。
  • 5行目: 制限名と対応するキー。
  • 10行目 (Response: []): 現在このモザイクにはグローバル制限がありません。
  • 11行目: モザイク制限を設定するトランザクション。これにはモザイクID(10進数)、制限キー(10進数)、制限値(1)、および制限条件(6。これは greater-or-equal の MosaicRestrictionType に対応します)が含まれます。
  • 25行目: 所有者アカウントを許可するトランザクション。
  • 39行目 (Response: []): ターゲットアカウントは現在、制限キーに関連付けられた値がないため、許可されていません。
  • 40行目: ターゲットアカウントを許可するトランザクション。
  • 70行目 (test transfer confirmed): 両方のアカウントが制限を満たし、許可されているため、テストトランザクションが成功しました。
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

出力の主なポイント:

  • 2-3行目: 関与するアカウントのアドレス。
  • 4行目: 制限されるモザイク。
  • 5行目: 制限名と対応するキー。
  • 10行目 (Response: [ ... ]): 既存の制限が検出されました。
  • 13行目 (Response: [ ... ]): ターゲットアカウントは制限値 1 を持っており、許可されています。
  • 14行目: ターゲットアカウントの許可を取り消すトランザクション。制限値を 0 に設定します。
  • 41行目 (test transfer failed): 期待通り、ターゲットアカウントが制限を満たさなくなったため、テストトランザクションが失敗しました。

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

トラブルシューティング⚓︎

プロトコル制約に違反した場合、トランザクションは拒否されます。以下の表は、最も一般的なエラーの原因をまとめたものです。

エラーメッセージ 考えられる原因
Mosaic_Expired モザイクが存在しないか、期限切れ です。
Mosaic_Owner_Conflict モザイクを制限しようとしているアカウントが、その所有者ではありません。
Required_Property_Flag_Unset モザイクが 制限可能 (restrictable)フラグを設定して作成されていません。
Account_Unauthorized 所有者またはターゲットアカウントのいずれかが、モザイクを取引する許可を持っていません。

結論⚓︎

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

ステップ 関連ドキュメント
現在のモザイク制限設定の取得 /restrictions/mosaic GET
モザイクグローバル制限の設定 , MosaicGlobalRestrictionTransactionV1
アカウントのモザイク制限設定の取得 /restrictions/mosaic GET
モザイクアドレス制限の設定 , MosaicAddressRestrictionTransactionV1