コンテンツにスキップ

アカウントへのメタデータの追加⚓︎

上級

アカウント は、モザイクネームスペース と同様に、キーと値のペアとして メタデータ を保存できます。

このチュートリアルでは、アカウントにメタデータを追加し、ネットワークから取得し、既存の値を更新する方法を説明します。

この例では、キーペア username = alice をアカウントに関連付け、その後 bob に変更します。

AccountアカウントTCHBDE...HHE32I

前提条件⚓︎

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

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

完全なコード⚓︎

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

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.FeeCalculator import calculate_transaction_fee
from symbolchain.symbol.Metadata import (
    metadata_generate_key,
    metadata_update_value
)
from symbolchain.symbol.Network import NetworkTimestamp

NODE_URL = os.getenv('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 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')


SIGNER_PRIVATE_KEY = os.getenv('SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))

facade = SymbolFacade('testnet')
signer_address = facade.network.public_key_to_address(
    signer_key_pair.public_key)
print(f'Signer address: {signer_address}')

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

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

    # --- ADDING NEW METADATA ---
    print('\n--- Adding new metadata ---')

    # Define metadata key and value
    key_string = f'username_{int(time.time())}'
    scoped_metadata_key = metadata_generate_key(key_string)
    metadata_value = 'alice'.encode('utf8')

    # Create the embedded metadata transaction
    creation_embedded_tx = facade.transaction_factory.create_embedded({
        'type': 'account_metadata_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'target_address': signer_address,
        'scoped_metadata_key': scoped_metadata_key,
        # When creating new metadata, value_size_delta
        # equals the value length
        'value_size_delta': len(metadata_value),
        'value': metadata_value
    })
    print('Created embedded metadata transaction:')
    print(json.dumps(creation_embedded_tx.to_json(), indent=2))

    # Build the aggregate transaction
    creation_embedded_txs = [creation_embedded_tx]
    creation_tx = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            creation_embedded_txs),
        'transactions': creation_embedded_txs
    })
    creation_tx.fee = Amount(
        calculate_transaction_fee(creation_tx, fee_multiplier))

    # Sign and generate final payload
    signature = facade.sign_transaction(signer_key_pair, creation_tx)
    creation_payload = facade.transaction_factory.attach_signature(
        creation_tx, signature)

    # Announce and wait for confirmation
    creation_tx_hash = facade.hash_transaction(creation_tx)
    print(f'Built aggregate transaction with hash: {creation_tx_hash}')
    announce_transaction(creation_payload, 'creation transaction')
    wait_for_confirmation(creation_tx_hash, 'creation transaction')

    # --- MODIFYING EXISTING METADATA ---
    print('\n--- Modifying existing metadata ---')

    # Fetch current metadata value from network
    metadata_path = (
        f'/metadata?sourceAddress={signer_address}'
        f'&targetAddress={signer_address}'
        f'&scopedMetadataKey={scoped_metadata_key:016X}'
        '&metadataType=0'
    )
    print(f'Fetching current metadata from {metadata_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{metadata_path}') as response:
        response_json = json.loads(response.read().decode())

    # Get the metadata entry
    if not response_json['data']:
        raise RuntimeError('Metadata entry not found')
    metadata_entry = response_json['data'][0]['metadataEntry']
    current_value = bytes.fromhex(metadata_entry['value'])
    print(f'  Current value: {current_value.decode("utf8")}')

    # XOR the current and new values
    new_value = 'bob'.encode('utf8')
    update_value = metadata_update_value(current_value, new_value)

    # Create the update transaction with XOR'd value
    update_embedded_tx = facade.transaction_factory.create_embedded({
        'type': 'account_metadata_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'target_address': signer_address,
        'scoped_metadata_key': scoped_metadata_key,
        # value_size_delta is the difference in length
        # (can be negative)
        'value_size_delta': len(new_value) - len(current_value),
        'value': update_value
    })

    # Build the aggregate for the update
    update_embedded_txs = [update_embedded_tx]
    update_tx = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            update_embedded_txs),
        'transactions': update_embedded_txs
    })
    update_tx.fee = Amount(
        calculate_transaction_fee(update_tx, fee_multiplier))

    # Sign and announce the update
    signature = facade.sign_transaction(
        signer_key_pair, update_tx)
    update_payload = facade.transaction_factory.attach_signature(
        update_tx, signature)

    # Announce and wait for confirmation
    update_tx_hash = facade.hash_transaction(update_tx)
    print(f'Built aggregate transaction with hash: {update_tx_hash}')
    announce_transaction(update_payload, 'update transaction')
    wait_for_confirmation(update_tx_hash, 'update transaction')

except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    NetworkTimestamp,
    SymbolFacade,
    calculateTransactionFee,
    metadataGenerateKey,
    metadataUpdateValue,
    models
} 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; 60 > attempt; 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 ('confirmed' === status.group) {
                console.log(`${label} confirmed in`, attempt, 'seconds');
                return;
            }
            if ('failed' === status.group)
                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`);
}

const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000000');
const signerKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

const facade = new SymbolFacade('testnet');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer address:', signerAddress.toString());

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

    // --- ADDING NEW METADATA ---
    console.log('\n--- Adding new metadata ---');

    // Define metadata key and value
    const keyString = `username_${Date.now()}`;
    const scopedMetadataKey = metadataGenerateKey(keyString);
    const metadataValue = new TextEncoder().encode('alice');

    // Create the embedded metadata transaction
    const creationEmbeddedTx = facade.transactionFactory
        .createEmbedded({
            type: 'account_metadata_transaction_v1',
            signerPublicKey: signerKeyPair.publicKey.toString(),
            targetAddress: signerAddress.toString(),
            scopedMetadataKey,
            // When creating new metadata, valueSizeDelta
            // equals value length
            valueSizeDelta: metadataValue.length,
            value: metadataValue
        });
    console.log('Created embedded metadata transaction:');
    console.log(JSON.stringify(creationEmbeddedTx.toJson(), null, 2));

    // Build the aggregate transaction
    const creationEmbeddedTxs = [creationEmbeddedTx];
    const creationTx = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            creationEmbeddedTxs),
        transactions: creationEmbeddedTxs
    });
    creationTx.fee = new models.Amount(
        calculateTransactionFee(creationTx, feeMultiplier));

    // Sign and generate final payload
    const signature = facade.signTransaction(signerKeyPair, creationTx);
    const creationPayload = facade.transactionFactory.static
        .attachSignature(creationTx, signature);

    // Announce and wait for confirmation
    const creationTxHash =
        facade.hashTransaction(creationTx).toString();
    console.log(
        'Built aggregate transaction with hash:', creationTxHash);
    await announceTransaction(creationPayload, 'creation transaction');
    await waitForConfirmation(creationTxHash, 'creation transaction');

    // --- MODIFYING EXISTING METADATA ---
    console.log('\n--- Modifying existing metadata ---');

    // Fetch current metadata value from network
    const scopedKeyHex = scopedMetadataKey.toString(16)
        .toUpperCase().padStart(16, '0');
    const metadataPath = `/metadata?sourceAddress=${signerAddress}` +
        `&targetAddress=${signerAddress}` +
        `&scopedMetadataKey=${scopedKeyHex}` +
        '&metadataType=0';
    console.log('Fetching current metadata from', metadataPath);
    const metadataResponse = await fetch(`${NODE_URL}${metadataPath}`);
    const metadataJSON = await metadataResponse.json();

    // Get the metadata entry
    if (!metadataJSON.data.length)
        throw new Error('Metadata entry not found');

    const metadataEntry = metadataJSON.data[0].metadataEntry;
    const currentValue = Buffer.from(metadataEntry.value, 'hex');
    console.log('  Current value:', currentValue.toString('utf8'));

    // XOR the current and new values
    const newValue = new TextEncoder().encode('bob');
    const updateValue = metadataUpdateValue(currentValue, newValue);

    // Create the update transaction with XOR'd value
    const updateEmbeddedTx = facade.transactionFactory
        .createEmbedded({
            type: 'account_metadata_transaction_v1',
            signerPublicKey: signerKeyPair.publicKey.toString(),
            targetAddress: signerAddress.toString(),
            scopedMetadataKey,
            // valueSizeDelta is the difference in length
            // (can be negative)
            valueSizeDelta: newValue.length - currentValue.length,
            value: updateValue
        });

    // Build the aggregate for the update
    const updateEmbeddedTxs = [updateEmbeddedTx];
    const updateTx = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            updateEmbeddedTxs),
        transactions: updateEmbeddedTxs
    });
    updateTx.fee = new models.Amount(
        calculateTransactionFee(updateTx, feeMultiplier));

    // Sign and announce the update
    const updateSignature = facade.signTransaction(
        signerKeyPair, updateTx);
    const updatePayload = facade.transactionFactory.static
        .attachSignature(updateTx, updateSignature);

    // Announce and wait for confirmation
    const updateTxHash =
        facade.hashTransaction(updateTx).toString();
    console.log(
        'Built aggregate transaction with hash:', updateTxHash);
    await announceTransaction(updatePayload, 'update transaction');
    await waitForConfirmation(updateTxHash, 'update transaction');

} catch (e) {
    console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown');
}

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.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

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.FeeCalculator;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.Metadata;
import org.symbol.sdk.symbol.NetworkTimestamp;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.models.*;

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

    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        for (int attempt = 0; 60 > attempt; ++attempt) {
            Thread.sleep(1000);
            try {
                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());
                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()));
            } catch (final IOException ex) {
                if (ex.getMessage().contains("failed"))
                    throw ex;

                System.out.println("  Transaction status: unknown");
            }
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", label));
    }

    public static void main(final String[] args) {
        new AccountMetadata().run();
    }

    private void run() {
        System.out.printf("Using node %s%n", nodeUrl);


        final String privateKeyString = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(privateKeyString));

        final Address signerAddress = facade.network.publicKeyToAddress(
            signerKeyPair.getPublicKey());
        System.out.printf("Signer address: %s%n", signerAddress);

        try {
            // Fetch current network time
            final String timePath = "/node/time";
            System.out.printf(
                "Fetching current network time from %s%n", timePath);
            final HttpRequest timeRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + timePath)).GET().build();
            final HttpResponse<String> timeResponse = HTTP_CLIENT.send(
                timeRequest, BodyHandlers.ofString());
            final JsonNode timeJson = JSON_MAPPER.readTree(
                timeResponse.body());
            final NetworkTimestamp timestamp = new NetworkTimestamp(
                timeJson.get("communicationTimestamps")
                    .get("receiveTimestamp").asLong());
            System.out.printf("  Network time: %dms since nemesis%n",
                timestamp.timestamp);

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

            System.out.println("\n--- Adding new metadata ---");

            // Define metadata key and value
            final String keyString = String.format(
                "username_%d", System.currentTimeMillis());
            final long scopedMetadataKey = Metadata.generateKey(keyString);
            final byte[] metadataValue =
                "alice".getBytes(StandardCharsets.UTF_8);

            // Create the embedded metadata transaction
            final EmbeddedTransaction creationEmbeddedTx =
                facade.transactionFactory.createEmbedded(Map.of(
                    "type", "account_metadata_transaction_v1",
                    "signerPublicKey", signerKeyPair.getPublicKey(),
                    "targetAddress", signerAddress,
                    "scopedMetadataKey", scopedMetadataKey,
                    // When creating new metadata, valueSizeDelta
                    // equals the value length
                    "valueSizeDelta", metadataValue.length,
                    // Cast one value to infer Map<String, Object>,
                    // as expected by the SDK.
                    "value", (Object) metadataValue));
            System.out.println("Created embedded metadata transaction:");
            System.out.println(JSON_MAPPER
                .writerWithDefaultPrettyPrinter()
                .writeValueAsString(creationEmbeddedTx.toJson()));

            // Build the aggregate transaction
            final List<EmbeddedTransaction> creationEmbeddedTxs =
                List.of(creationEmbeddedTx);
            final Transaction creationTx =
                facade.transactionFactory.create(Map.of(
                    "type", "aggregate_complete_transaction_v3",
                    "signerPublicKey", signerKeyPair.getPublicKey(),
                    "deadline", timestamp.addHours(2).timestamp,
                    "transactionsHash",
                        SymbolFacade.hashEmbeddedTransactions(
                            creationEmbeddedTxs),
                    // Cast one value to infer Map<String, Object>,
                    // as expected by the SDK.
                    "transactions", (Object) creationEmbeddedTxs));
            creationTx.setFee(new Amount(
                FeeCalculator.calculateTransactionFee(
                    creationTx, feeMultiplier)));

            // Sign and generate final payload
            final String creationPayload = SymbolTransactionFactory
                .attachSignature(creationTx,
                    facade.signTransaction(signerKeyPair, creationTx));

            // Announce and wait for confirmation
            final String creationTxHash =
                facade.hashTransaction(creationTx).toString();
            System.out.printf(
                "Built aggregate transaction with hash: %s%n",
                    creationTxHash);
            announceTransaction(creationPayload, "creation transaction");
            waitForConfirmation(creationTxHash, "creation transaction");

            System.out.println("\n--- Modifying existing metadata ---");

            // Fetch current metadata value from network
            final String scopedKeyHex = "%016X".formatted(
                scopedMetadataKey);
            final String metadataPath = String.format(
                "/metadata?sourceAddress=%s&targetAddress=%s"
                    + "&scopedMetadataKey=%s&metadataType=0",
                signerAddress, signerAddress, scopedKeyHex);
            System.out.printf("Fetching current metadata from %s%n",
                metadataPath);
            final HttpRequest metadataRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + metadataPath)).GET().build();
            final HttpResponse<String> metadataResponse = HTTP_CLIENT.send(
                metadataRequest, BodyHandlers.ofString());
            final JsonNode metadataJson = JSON_MAPPER.readTree(
                metadataResponse.body());

            // Get the metadata entry
            final JsonNode metadataData = metadataJson.get("data");
            if (null == metadataData)
                throw new IOException(
                    "Unexpected metadata response: " + metadataJson);

            if (metadataData.isEmpty())
                throw new IOException("Metadata entry not found");

            final JsonNode metadataEntry = metadataData.get(0)
                .get("metadataEntry");
            final byte[] currentValue = java.util.HexFormat.of()
                .parseHex(metadataEntry.get("value").asText());
            System.out.printf("  Current value: %s%n",
                new String(currentValue, StandardCharsets.UTF_8));

            // XOR the current and new values
            final byte[] newValue = "bob".getBytes(StandardCharsets.UTF_8);
            final byte[] updateValue = Metadata.updateValue(
                currentValue, newValue);

            // Create the update transaction with XOR'd value
            final EmbeddedTransaction updateEmbeddedTx =
                facade.transactionFactory.createEmbedded(Map.of(
                    "type", "account_metadata_transaction_v1",
                    "signerPublicKey", signerKeyPair.getPublicKey(),
                    "targetAddress", signerAddress,
                    "scopedMetadataKey", scopedMetadataKey,
                    // valueSizeDelta is the difference in length
                    // (can be negative)
                    "valueSizeDelta",
                        newValue.length - currentValue.length,
                    // Cast one value to infer Map<String, Object>,
                    // as expected by the SDK.
                    "value", (Object) updateValue));

            // Build the aggregate for the update
            final List<EmbeddedTransaction> updateEmbeddedTxs =
                List.of(updateEmbeddedTx);
            final Transaction updateTx =
                facade.transactionFactory.create(Map.of(
                    "type", "aggregate_complete_transaction_v3",
                    "signerPublicKey", signerKeyPair.getPublicKey(),
                    "deadline", timestamp.addHours(2).timestamp,
                    "transactionsHash",
                        SymbolFacade.hashEmbeddedTransactions(
                            updateEmbeddedTxs),
                    // Cast one value to infer Map<String, Object>,
                    // as expected by the SDK.
                    "transactions", (Object) updateEmbeddedTxs));
            updateTx.setFee(new Amount(
                FeeCalculator.calculateTransactionFee(
                    updateTx, feeMultiplier)));

            // Sign and announce the update
            final String updatePayload = SymbolTransactionFactory
                .attachSignature(updateTx,
                    facade.signTransaction(signerKeyPair, updateTx));

            // Announce and wait for confirmation
            final String updateTxHash =
                facade.hashTransaction(updateTx).toString();
            System.out.printf(
                "Built aggregate transaction with hash: %s%n",
                updateTxHash);
            announceTransaction(updatePayload, "update transaction");
            waitForConfirmation(updateTxHash, "update transaction");

        } catch (final Exception ex) {
            System.out.println(ex.getMessage());
        }
    }
}

Download source

コード解説⚓︎

このチュートリアルでは、アカウントに新しいメタデータを追加し、その後そのメタデータを更新する方法を実演します。

アカウントのセットアップ⚓︎

SIGNER_PRIVATE_KEY = os.getenv('SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))

facade = SymbolFacade('testnet')
signer_address = facade.network.public_key_to_address(
    signer_key_pair.public_key)
print(f'Signer address: {signer_address}')
const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000000');
const signerKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

const facade = new SymbolFacade('testnet');
const signerAddress = facade.network.publicKeyToAddress(
    signerKeyPair.publicKey);
console.log('Signer address:', signerAddress.toString());
        final String privateKeyString = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(privateKeyString));

        final Address signerAddress = facade.network.publicKeyToAddress(
            signerKeyPair.getPublicKey());
        System.out.printf("Signer address: %s%n", signerAddress);

このスニペットは、SIGNER_PRIVATE_KEY 環境変数から署名者の 秘密鍵 を読み取ります。設定されていない場合はデフォルトのテストキーが使用されます。 署名者の アドレス公開鍵 から導出します。

このチュートリアルでは、署名者が自身のアカウントにメタデータを追加します。 別のアカウントにメタデータを追加する場合は、対象となるアカウントがトランザクションに連署する必要があります。

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

    # 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_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')
    // 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 medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);
            // Fetch current network time
            final String timePath = "/node/time";
            System.out.printf(
                "Fetching current network time from %s%n", timePath);
            final HttpRequest timeRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + timePath)).GET().build();
            final HttpResponse<String> timeResponse = HTTP_CLIENT.send(
                timeRequest, BodyHandlers.ofString());
            final JsonNode timeJson = JSON_MAPPER.readTree(
                timeResponse.body());
            final NetworkTimestamp timestamp = new NetworkTimestamp(
                timeJson.get("communicationTimestamps")
                    .get("receiveTimestamp").asLong());
            System.out.printf("  Network time: %dms since nemesis%n",
                timestamp.timestamp);

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

転送トランザクション チュートリアルで説明されているプロセスに従い、ネットワーク時間と推奨手数料をそれぞれ /node/time GET および /network/fees/transaction GET から取得します。

メタデータの定義⚓︎

各アカウントメタデータエントリは、以下の3つによって一意に識別されます。

  • 署名者のアドレス: メタデータを追加するアカウント
  • ターゲットアカウントのアドレス: メタデータを付与するアカウント(このアカウントの署名が必要)
  • スコープ指定されたメタデータキー: メタデータ作成者によって指定した64ビットの値

    SDKは、人間が読み取り可能な文字列からSHA3-256ハッシュを使用してこのキーを生成する ヘルパー関数を提供しています。 この方法により、キーがより意味のあるものになり、衝突の可能性が低減します。

    # Define metadata key and value
    key_string = f'username_{int(time.time())}'
    scoped_metadata_key = metadata_generate_key(key_string)
    metadata_value = 'alice'.encode('utf8')
    // Define metadata key and value
    const keyString = `username_${Date.now()}`;
    const scopedMetadataKey = metadataGenerateKey(keyString);
    const metadataValue = new TextEncoder().encode('alice');
            // Define metadata key and value
            final String keyString = String.format(
                "username_%d", System.currentTimeMillis());
            final long scopedMetadataKey = Metadata.generateKey(keyString);
            final byte[] metadataValue =
                "alice".getBytes(StandardCharsets.UTF_8);

この例では、キーは文字列 username から派生します。 デモンストレーションのためにキー文字列にはタイムスタンプが付加されているため、コードを実行するたびに新しいエントリがアカウントに追加されます。 実際には、作成または更新したい特定のメタデータエントリを識別する固定キーを使用します。

メタデータの値は、最大1024バイトまでの任意のシーケンスにすることができます。 この例では、値はUTF-8でエンコードされた文字列 alice です。

同じキーを持つ複数のエントリ

キーはメタデータエントリを識別する3つの要素のうちの1つにすぎないため、いずれかの要素が変更されると別のエントリになります。

例えば、署名者のアドレスが異なれば、別のアカウントが同じターゲットアカウントに対して同じメタデータキーを競合なく使用できます。

各エントリは独立しており、最初に作成したアカウントのみが更新できます。

埋め込みアカウントメタデータトランザクションの作成⚓︎

    # Create the embedded metadata transaction
    creation_embedded_tx = facade.transaction_factory.create_embedded({
        'type': 'account_metadata_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'target_address': signer_address,
        'scoped_metadata_key': scoped_metadata_key,
        # When creating new metadata, value_size_delta
        # equals the value length
        'value_size_delta': len(metadata_value),
        'value': metadata_value
    })
    print('Created embedded metadata transaction:')
    print(json.dumps(creation_embedded_tx.to_json(), indent=2))
    // Create the embedded metadata transaction
    const creationEmbeddedTx = facade.transactionFactory
        .createEmbedded({
            type: 'account_metadata_transaction_v1',
            signerPublicKey: signerKeyPair.publicKey.toString(),
            targetAddress: signerAddress.toString(),
            scopedMetadataKey,
            // When creating new metadata, valueSizeDelta
            // equals value length
            valueSizeDelta: metadataValue.length,
            value: metadataValue
        });
    console.log('Created embedded metadata transaction:');
    console.log(JSON.stringify(creationEmbeddedTx.toJson(), null, 2));
            // Create the embedded metadata transaction
            final EmbeddedTransaction creationEmbeddedTx =
                facade.transactionFactory.createEmbedded(Map.of(
                    "type", "account_metadata_transaction_v1",
                    "signerPublicKey", signerKeyPair.getPublicKey(),
                    "targetAddress", signerAddress,
                    "scopedMetadataKey", scopedMetadataKey,
                    // When creating new metadata, valueSizeDelta
                    // equals the value length
                    "valueSizeDelta", metadataValue.length,
                    // Cast one value to infer Map<String, Object>,
                    // as expected by the SDK.
                    "value", (Object) metadataValue));
            System.out.println("Created embedded metadata transaction:");
            System.out.println(JSON_MAPPER
                .writerWithDefaultPrettyPrinter()
                .writeValueAsString(creationEmbeddedTx.toJson()));

アカウントメタデータトランザクションは、ブロックチェーン上のアカウントにキーと値のペアを関連付けます。 同じトランザクションタイプで、新しいメタデータエントリの追加と既存の更新の両方を処理します。

Symbolでは、これらのトランザクションをターゲットアカウントの所有者の署名を含む アグリゲートトランザクション 内に含める必要があります。 これにより、所有者の許可なく不要なメタデータがアカウントに関連付けられるのを防ぎます。

アカウント所有者によってトランザクションが開始される場合でも、トランザクション形式を統一するためにアグリゲートが必要です。 このため、コードではアカウントメタデータトランザクションを 埋め込みトランザクション として定義します。

このトランザクションでは以下を指定します。

  • Type: AccountMetadataTransactionV1 を使用します。

  • 署名者の公開鍵: メタデータエントリを作成するアカウント。 この例では、このアカウントがメタデータを受け取るアカウントでもあります。

  • ターゲットアドレス: メタデータを関連付けるアカウント。 ターゲットが署名者と異なる場合、ターゲットアカウントはアグリゲートトランザクションに連署する必要があります。

  • スコープ指定されたメタデータキー: このメタデータエントリを識別するために使用される64ビットのキー。

  • 値のサイズの差分 (Value size delta): 新しいメタデータを作成する場合は、値のバイト長に設定します。 既存のメタデータを更新する場合は、新しい値と現在の値の長さの差分に設定します。

  • 値: バイト形式のメタデータ内容。 新しいメタデータを作成する場合は、生の値を指定します。 更新する場合は、計算された値を指定します(既存のメタデータの変更 セクションで説明します)。

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

    # Build the aggregate transaction
    creation_embedded_txs = [creation_embedded_tx]
    creation_tx = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            creation_embedded_txs),
        'transactions': creation_embedded_txs
    })
    creation_tx.fee = Amount(
        calculate_transaction_fee(creation_tx, fee_multiplier))
    // Build the aggregate transaction
    const creationEmbeddedTxs = [creationEmbeddedTx];
    const creationTx = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            creationEmbeddedTxs),
        transactions: creationEmbeddedTxs
    });
    creationTx.fee = new models.Amount(
        calculateTransactionFee(creationTx, feeMultiplier));
            // Build the aggregate transaction
            final List<EmbeddedTransaction> creationEmbeddedTxs =
                List.of(creationEmbeddedTx);
            final Transaction creationTx =
                facade.transactionFactory.create(Map.of(
                    "type", "aggregate_complete_transaction_v3",
                    "signerPublicKey", signerKeyPair.getPublicKey(),
                    "deadline", timestamp.addHours(2).timestamp,
                    "transactionsHash",
                        SymbolFacade.hashEmbeddedTransactions(
                            creationEmbeddedTxs),
                    // Cast one value to infer Map<String, Object>,
                    // as expected by the SDK.
                    "transactions", (Object) creationEmbeddedTxs));
            creationTx.setFee(new Amount(
                FeeCalculator.calculateTransactionFee(
                    creationTx, feeMultiplier)));

コードは、埋め込みトランザクションアグリゲートトランザクション に追加します。

署名者が自身のアカウントを変更しているため、連署 は不要であり、アグリゲートは コンプリート として作成できるため、すぐに署名してアナウンスできます。

別のアカウントへのメタデータの追加

ターゲットアカウントが署名者と異なる場合、メタデータエントリを承認するためにターゲットがアグリゲートトランザクションに連署する必要があります。

オンチェーンで連署を収集する詳細については、ボンデッドアグリゲートトランザクション のチュートリアルを参照してください。

アグリゲートトランザクションの送信⚓︎

    # Sign and generate final payload
    signature = facade.sign_transaction(signer_key_pair, creation_tx)
    creation_payload = facade.transaction_factory.attach_signature(
        creation_tx, signature)

    # Announce and wait for confirmation
    creation_tx_hash = facade.hash_transaction(creation_tx)
    print(f'Built aggregate transaction with hash: {creation_tx_hash}')
    announce_transaction(creation_payload, 'creation transaction')
    wait_for_confirmation(creation_tx_hash, 'creation transaction')
    // Sign and generate final payload
    const signature = facade.signTransaction(signerKeyPair, creationTx);
    const creationPayload = facade.transactionFactory.static
        .attachSignature(creationTx, signature);

    // Announce and wait for confirmation
    const creationTxHash =
        facade.hashTransaction(creationTx).toString();
    console.log(
        'Built aggregate transaction with hash:', creationTxHash);
    await announceTransaction(creationPayload, 'creation transaction');
    await waitForConfirmation(creationTxHash, 'creation transaction');
            // Sign and generate final payload
            final String creationPayload = SymbolTransactionFactory
                .attachSignature(creationTx,
                    facade.signTransaction(signerKeyPair, creationTx));

            // Announce and wait for confirmation
            final String creationTxHash =
                facade.hashTransaction(creationTx).toString();
            System.out.printf(
                "Built aggregate transaction with hash: %s%n",
                    creationTxHash);
            announceTransaction(creationPayload, "creation transaction");
            waitForConfirmation(creationTxHash, "creation transaction");

アグリゲートトランザクションは、コンプリートアグリゲートトランザクション チュートリアルと同じプロセスに従って署名され、アナウンスされます。

メタデータの取得⚓︎

    # Fetch current metadata value from network
    metadata_path = (
        f'/metadata?sourceAddress={signer_address}'
        f'&targetAddress={signer_address}'
        f'&scopedMetadataKey={scoped_metadata_key:016X}'
        '&metadataType=0'
    )
    print(f'Fetching current metadata from {metadata_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{metadata_path}') as response:
        response_json = json.loads(response.read().decode())

    # Get the metadata entry
    if not response_json['data']:
        raise RuntimeError('Metadata entry not found')
    metadata_entry = response_json['data'][0]['metadataEntry']
    current_value = bytes.fromhex(metadata_entry['value'])
    print(f'  Current value: {current_value.decode("utf8")}')
    // Fetch current metadata value from network
    const scopedKeyHex = scopedMetadataKey.toString(16)
        .toUpperCase().padStart(16, '0');
    const metadataPath = `/metadata?sourceAddress=${signerAddress}` +
        `&targetAddress=${signerAddress}` +
        `&scopedMetadataKey=${scopedKeyHex}` +
        '&metadataType=0';
    console.log('Fetching current metadata from', metadataPath);
    const metadataResponse = await fetch(`${NODE_URL}${metadataPath}`);
    const metadataJSON = await metadataResponse.json();

    // Get the metadata entry
    if (!metadataJSON.data.length)
        throw new Error('Metadata entry not found');

    const metadataEntry = metadataJSON.data[0].metadataEntry;
    const currentValue = Buffer.from(metadataEntry.value, 'hex');
    console.log('  Current value:', currentValue.toString('utf8'));
            // Fetch current metadata value from network
            final String scopedKeyHex = "%016X".formatted(
                scopedMetadataKey);
            final String metadataPath = String.format(
                "/metadata?sourceAddress=%s&targetAddress=%s"
                    + "&scopedMetadataKey=%s&metadataType=0",
                signerAddress, signerAddress, scopedKeyHex);
            System.out.printf("Fetching current metadata from %s%n",
                metadataPath);
            final HttpRequest metadataRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + metadataPath)).GET().build();
            final HttpResponse<String> metadataResponse = HTTP_CLIENT.send(
                metadataRequest, BodyHandlers.ofString());
            final JsonNode metadataJson = JSON_MAPPER.readTree(
                metadataResponse.body());

            // Get the metadata entry
            final JsonNode metadataData = metadataJson.get("data");
            if (null == metadataData)
                throw new IOException(
                    "Unexpected metadata response: " + metadataJson);

            if (metadataData.isEmpty())
                throw new IOException("Metadata entry not found");

            final JsonNode metadataEntry = metadataData.get(0)
                .get("metadataEntry");
            final byte[] currentValue = java.util.HexFormat.of()
                .parseHex(metadataEntry.get("value").asText());
            System.out.printf("  Current value: %s%n",
                new String(currentValue, StandardCharsets.UTF_8));

メタデータエントリの現在の値を取得するために、コードは sourceAddresstargetAddressscopedMetadataKey、および metadataType(アカウントメタデータの場合は 0)のフィルタを指定して /metadata GET エンドポイントを使用します。

エンドポイントはフィルタに一致するエントリのリストを返します。この例では単一のアイテムが含まれます。

既存のメタデータの変更⚓︎

    # XOR the current and new values
    new_value = 'bob'.encode('utf8')
    update_value = metadata_update_value(current_value, new_value)

    # Create the update transaction with XOR'd value
    update_embedded_tx = facade.transaction_factory.create_embedded({
        'type': 'account_metadata_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'target_address': signer_address,
        'scoped_metadata_key': scoped_metadata_key,
        # value_size_delta is the difference in length
        # (can be negative)
        'value_size_delta': len(new_value) - len(current_value),
        'value': update_value
    })
    // XOR the current and new values
    const newValue = new TextEncoder().encode('bob');
    const updateValue = metadataUpdateValue(currentValue, newValue);

    // Create the update transaction with XOR'd value
    const updateEmbeddedTx = facade.transactionFactory
        .createEmbedded({
            type: 'account_metadata_transaction_v1',
            signerPublicKey: signerKeyPair.publicKey.toString(),
            targetAddress: signerAddress.toString(),
            scopedMetadataKey,
            // valueSizeDelta is the difference in length
            // (can be negative)
            valueSizeDelta: newValue.length - currentValue.length,
            value: updateValue
        });
            // XOR the current and new values
            final byte[] newValue = "bob".getBytes(StandardCharsets.UTF_8);
            final byte[] updateValue = Metadata.updateValue(
                currentValue, newValue);

            // Create the update transaction with XOR'd value
            final EmbeddedTransaction updateEmbeddedTx =
                facade.transactionFactory.createEmbedded(Map.of(
                    "type", "account_metadata_transaction_v1",
                    "signerPublicKey", signerKeyPair.getPublicKey(),
                    "targetAddress", signerAddress,
                    "scopedMetadataKey", scopedMetadataKey,
                    // valueSizeDelta is the difference in length
                    // (can be negative)
                    "valueSizeDelta",
                        newValue.length - currentValue.length,
                    // Cast one value to infer Map<String, Object>,
                    // as expected by the SDK.
                    "value", (Object) updateValue));

既存のメタデータエントリを更新するには、前述のようにネットワークから取得した現在の値が必要です。

メタデータの更新を実演するために、コードは同じメタデータキーを使用して別の AccountMetadataTransactionV1 トランザクションを作成し、ユーザー名を alice から bob に変更します。

既存のメタデータ値を変更することは、新しい値が現在の値を基準として以下のフィールドを使用して定義される必要があるという点で、新規作成とは異なります。

  • value_size_delta: 新しい値と現在の値の長さの差。 この例では、bob(3バイト)は alice(5バイト)より2バイト短いため、デルタは -2 になります。

  • value: 現在の値と新しい値をバイトごとに比較して計算された XOR されたバイト。

    SDKは XOR 計算を処理する ヘルパー関数を提供しています。 XOR 操作は各バイトを比較します。一致するバイトはゼロになり、異なるバイトが変更箇所を捉えます。

value_size_delta は、XOR されたバイト自体の長さではなく、最終的な値の長さの差(新 vs 旧)を表すことに注意してください。

メタデータエントリの削除

メタデータエントリを削除するには、value_size_delta を現在の値の長さの負の値に設定し、現在の値を value として提供します。XOR によって空の結果が生成され、ネットワークからエントリが削除されます。

最初のメタデータ作成 と同様に、このメタデータの変更はアグリゲートトランザクションにラップされ、署名してアナウンスされます。

    # Build the aggregate for the update
    update_embedded_txs = [update_embedded_tx]
    update_tx = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            update_embedded_txs),
        'transactions': update_embedded_txs
    })
    update_tx.fee = Amount(
        calculate_transaction_fee(update_tx, fee_multiplier))

    # Sign and announce the update
    signature = facade.sign_transaction(
        signer_key_pair, update_tx)
    update_payload = facade.transaction_factory.attach_signature(
        update_tx, signature)

    # Announce and wait for confirmation
    update_tx_hash = facade.hash_transaction(update_tx)
    print(f'Built aggregate transaction with hash: {update_tx_hash}')
    announce_transaction(update_payload, 'update transaction')
    wait_for_confirmation(update_tx_hash, 'update transaction')
    // Build the aggregate for the update
    const updateEmbeddedTxs = [updateEmbeddedTx];
    const updateTx = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            updateEmbeddedTxs),
        transactions: updateEmbeddedTxs
    });
    updateTx.fee = new models.Amount(
        calculateTransactionFee(updateTx, feeMultiplier));

    // Sign and announce the update
    const updateSignature = facade.signTransaction(
        signerKeyPair, updateTx);
    const updatePayload = facade.transactionFactory.static
        .attachSignature(updateTx, updateSignature);

    // Announce and wait for confirmation
    const updateTxHash =
        facade.hashTransaction(updateTx).toString();
    console.log(
        'Built aggregate transaction with hash:', updateTxHash);
    await announceTransaction(updatePayload, 'update transaction');
    await waitForConfirmation(updateTxHash, 'update transaction');
            // Build the aggregate for the update
            final List<EmbeddedTransaction> updateEmbeddedTxs =
                List.of(updateEmbeddedTx);
            final Transaction updateTx =
                facade.transactionFactory.create(Map.of(
                    "type", "aggregate_complete_transaction_v3",
                    "signerPublicKey", signerKeyPair.getPublicKey(),
                    "deadline", timestamp.addHours(2).timestamp,
                    "transactionsHash",
                        SymbolFacade.hashEmbeddedTransactions(
                            updateEmbeddedTxs),
                    // Cast one value to infer Map<String, Object>,
                    // as expected by the SDK.
                    "transactions", (Object) updateEmbeddedTxs));
            updateTx.setFee(new Amount(
                FeeCalculator.calculateTransactionFee(
                    updateTx, feeMultiplier)));

            // Sign and announce the update
            final String updatePayload = SymbolTransactionFactory
                .attachSignature(updateTx,
                    facade.signTransaction(signerKeyPair, updateTx));

            // Announce and wait for confirmation
            final String updateTxHash =
                facade.hashTransaction(updateTx).toString();
            System.out.printf(
                "Built aggregate transaction with hash: %s%n",
                updateTxHash);
            announceTransaction(updatePayload, "update transaction");
            waitForConfirmation(updateTxHash, "update transaction");

出力⚓︎

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

Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Fetching current network time from /node/time
  Network time: 98406073135 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100

--- Adding new metadata ---
Created embedded metadata transaction:
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16708,
  "target_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9",
  "scoped_metadata_key": "16705141506538718608",
  "value_size_delta": 5,
  "value": "616c696365"
}
Built aggregate transaction with hash: 976C555DD4FEEA2B48088457AC445A41FA28E13C88B12823844F5EC399B97144
Announcing aggregate transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for aggregate transaction confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
aggregate transaction confirmed in 11 seconds

--- Modifying existing metadata ---
Fetching current metadata from /metadata?sourceAddress=TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I&targetAddress=TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I&scopedMetadataKey=E7F71DA42B54B310&metadataType=0
  Current value: alice
Created embedded update transaction:
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16708,
  "target_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9",
  "scoped_metadata_key": "16705141506538718608",
  "value_size_delta": -2,
  "value": "03030b6365"
}
Built aggregate transaction with hash: AA58533BB20A54FE0CF3AD44EC4CEA05C1BC6A47D817850DBB072D0CB254116F
Announcing aggregate transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for aggregate transaction confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
aggregate transaction confirmed in 31 seconds

出力の主なポイント:

  • 16行目 ("scoped_metadata_key"): 入力文字列からSHA3-256ハッシュを使用して生成された64ビットのキー。
  • 17行目 ("value_size_delta": 5): 新しいメタデータを作成する場合、これは値のバイト長に等しくなります("alice" = 5バイト)。
  • 18行目 ("value": "616c696365"): 16進数でエンコードされたメタデータの値(UTF-8の "alice")。
  • 20行目: エクスプローラーでメタデータの作成を確認するためのトランザクション ハッシュ
  • 31行目 (Current value: alice): 更新前にネットワークから取得された値。
  • 40行目 ("value_size_delta": -2): 新しい値("bob" = 3バイト)が現在の値(5バイト)より短いため、負の値になります。差は -2 です。
  • 41行目 ("value": "03030b6365"): 生の新しい値ではなく、現在の値と新しい値から計算された XOR 値。
  • 43行目: エクスプローラーでメタデータの更新を確認するためのトランザクションハッシュ。

トランザクションハッシュを使用して、Symbol Testnet Explorer でトランザクションを検索できます。

結論⚓︎

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

ステップ 関連ドキュメント
メタデータのキーと値の定義
アカウントメタデータトランザクションの作成 , AccountMetadataTransactionV1
メタデータの取得 /metadata GET
既存のメタデータの変更