コンテンツにスキップ
上級

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

モザイク の所有者は、そのモザイクを取引できる アカウント を制限できます。これらの条件は モザイク制限 と呼ばれ、以下の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.sc import Amount
from symbolchain.symbol.Network import NetworkTimestamp
from symbolchain.symbol.Restriction import mosaic_restriction_generate_key

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

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

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

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

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

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

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

    return transaction

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

    return transaction

facade = SymbolFacade('testnet')

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

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

mosaic_id = int(os.getenv('MOSAIC_ID', '6A5ACF2376E50D4A'), 16)
print(f'Mosaic ID: 0x{mosaic_id:08X}')
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})')

try:
    # Fetch current network time
    time_path = '/node/time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')

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

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

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

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

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

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

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

except Exception as e:
    print(e)

Download source

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

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

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

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

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

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

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

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

    return transaction;
}

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

    return transaction;
}

const facade = new SymbolFacade('testnet');

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Download source

コード解説⚓︎

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

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

アカウントの設定⚓︎

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

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:08X}')
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()}`);

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

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

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

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

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

ネットワーク時間と手数料の取得⚓︎

    # Fetch current network time
    time_path = '/node/time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')

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

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

転送トランザクション チュートリアルで説明されているプロセスに従い、ネットワーク時間と推奨手数料をそれぞれ /node/time GET および /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(global_restriction_enable_transaction())

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

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

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

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

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

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

    メモ

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

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

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

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

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

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

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

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

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

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

グローバル制限の場合と同様に、現在の値は /restrictions/mosaic GET を照会し、mosaicIdtargetAddress、および 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 (addressRestrictions.length > 0)
        prevValue = BigInt(addressRestrictions[0].value);
    if (prevValue !== 1n) {
        // Enable the address restriction
        console.log('+ Authorizing target account');
        transactions.push(addressRestrictionSetValue(
            prevValue, 1n, targetAddress));
    } else {
        // Disable the address restriction
        console.log('+ Deauthorizing target account');
        transactions.push(addressRestrictionSetValue(
            prevValue, 0n, targetAddress));
    }
  • アカウントにまだ制限値がない、または値が 1 でない場合、コードは値 1 を割り当て、モザイクの使用を許可します。
  • アカウントにすでに値 1 が設定されている場合、コードはそれを 0 に置き換え、許可を取り消します。

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

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

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

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

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

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

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

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

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

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

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

テスト転送の送信⚓︎

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

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

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

ターゲットアカウントが現在制限を満たしている(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 current network time from /node/time
  Network time: 109528513581 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Checking if the global restriction is enabled:
  Getting restrictions from /restrictions/mosaic?mosaicId=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行目: 制限名と対応するキー。
  • 12行目 (Response: []): 現在このモザイクにはグローバル制限がありません。
  • 13行目: モザイク制限を設定するトランザクション。これにはモザイクID(10進数)、制限キー(10進数)、制限値(1)、および制限条件(6。これは greater-or-equalMosaicRestrictionType に対応します)が含まれます。
  • 27行目: 所有者アカウントを許可するトランザクション。
  • 41行目 (Response: []): ターゲットアカウントは現在、制限キーに関連付けられた値がないため、許可されていません。
  • 42行目: ターゲットアカウントを許可するトランザクション。
  • 72行目 (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 current network time from /node/time
  Network time: 109528567086 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Checking if the global restriction is enabled:
  Getting restrictions from /restrictions/mosaic?mosaicId=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行目: 制限名と対応するキー。
  • 12行目 (Response: [ ... ]): 既存の制限が検出されました。
  • 15行目 (Response: [ ... ]): ターゲットアカウントは制限値 1 を持っており、許可されています。
  • 16行目: ターゲットアカウントの許可を取り消すトランザクション。制限値を 0 に設定します。
  • 43行目 (test transfer failed): 期待通り、ターゲットアカウントが制限を満たさなくなったため、テストトランザクションが失敗しました。

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

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

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

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

結論⚓︎

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

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