Skip to content

Adding Metadata to a Mosaic⚓︎

ADVANCED

Mosaics, like accounts and namespaces, can store metadata as key-value pairs.

This tutorial shows how to add metadata to a mosaic, retrieve it from the network, and update existing values.

In this example, the pair description = My first mosaic is attached to a mosaic and then changed to Updated mosaic.

MosaicMosaic0x6D13...62C2

Prerequisites⚓︎

Before you start, make sure to:

Additionally, review the Transfer transaction tutorial to understand how transactions are announced and confirmed, and the Complete Aggregate transaction tutorial to understand how aggregate transactions work.

Full Code⚓︎

The following is the complete code listing for this tutorial. A detailed, step-by-step explanation follows in the next section.

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.Metadata import (
    metadata_generate_key,
    metadata_update_value
)

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

# Get mosaic ID from environment
MOSAIC_ID = os.getenv('MOSAIC_ID', '6D1314BE751B62C2')
mosaic_id = int(MOSAIC_ID, 16)
print(f'Mosaic ID: {mosaic_id} (0x{mosaic_id:016X})')

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

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

    # Define metadata key and value
    key_string = f'description_{int(time.time())}'
    scoped_metadata_key = metadata_generate_key(key_string)
    metadata_value = 'My first mosaic'.encode('utf8')

    # Create the embedded metadata transaction
    embedded_transaction = (
        facade.create_embedded_transaction_from_descriptor(
            {
                'type': 'mosaic_metadata_transaction_v1',
                'target_address': signer_address,
                'target_mosaic_id': mosaic_id,
                '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
            },
            signer_key_pair.public_key))
    print('Created embedded metadata transaction:')
    print(json.dumps(embedded_transaction.to_json(), indent=2))

    # Build the aggregate transaction
    embedded_transactions = [embedded_transaction]
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                embedded_transactions),
            'transactions': embedded_transactions
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)

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

    # Announce and wait for confirmation
    transaction_hash = facade.hash_transaction(transaction)
    print(f'Built aggregate transaction with hash: {transaction_hash}')
    announce_transaction(json_payload, 'aggregate transaction')
    wait_for_confirmation(transaction_hash, 'aggregate 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}'
        f'&targetId={mosaic_id:016X}'
        '&metadataType=1'
    )
    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 = 'Updated mosaic'.encode('utf8')
    update_value = metadata_update_value(current_value, new_value)

    # Create the update transaction with XOR'd value
    embedded_update = facade.create_embedded_transaction_from_descriptor(
        {
            'type': 'mosaic_metadata_transaction_v1',
            'target_address': signer_address,
            'target_mosaic_id': mosaic_id,
            '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
        },
        signer_key_pair.public_key)
    print('Created embedded update transaction:')
    print(json.dumps(embedded_update.to_json(), indent=2))

    # Build the aggregate for the update
    embedded_transactions = [embedded_update]
    update_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                embedded_transactions),
            'transactions': embedded_transactions
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)

    # Sign the update
    signature = facade.sign_transaction(
        signer_key_pair, update_transaction)
    json_payload = facade.transaction_factory.attach_signature(
        update_transaction, signature)

    # Announce and wait for confirmation
    update_hash = facade.hash_transaction(update_transaction)
    print(f'Built aggregate transaction with hash: {update_hash}')
    announce_transaction(json_payload, 'aggregate transaction')
    wait_for_confirmation(update_hash, 'aggregate transaction')

except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    SymbolFacade,
    descriptors,
    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); });
        const response = await fetch(
            `${NODE_URL}/transactionStatus/${transactionHash}`);
        if (!response.ok) {
            if (404 === response.status) {
                console.log('  Transaction status: unknown');
                continue;
            }
            throw new Error(`HTTP ${response.status}`);
        }
        const status = await response.json();
        console.log('  Transaction status:', status.group);
        if ('confirmed' === status.group) {
            console.log(`${label} confirmed in`, attempt, 'seconds');
            return;
        }
        if ('failed' === status.group)
            throw new Error(`${label} failed: ${status.code}`);
    }
    throw new Error(`${label} not confirmed after 60 seconds`);
}

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

// Get mosaic ID from environment
const MOSAIC_ID = process.env.MOSAIC_ID || '6D1314BE751B62C2';
const mosaicId = BigInt(`0x${MOSAIC_ID}`);
const mosaicIdHex = mosaicId.toString(16)
    .toUpperCase().padStart(16, '0');
console.log('Mosaic ID:', mosaicId.toString(), `(0x${mosaicIdHex})`);

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

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

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

    // Create the embedded metadata transaction
    const embeddedTransaction =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.MosaicMetadataTransactionV1Descriptor(
                signerAddress,
                scopedMetadataKey,
                new models.UnresolvedMosaicId(mosaicId),
                // When creating new metadata, valueSizeDelta
                // equals value length
                metadataValue.length,
                metadataValue),
            signerKeyPair.publicKey);
    console.log('Created embedded metadata transaction:');
    console.log(JSON.stringify(embeddedTransaction.toJson(), null, 2));

    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTransaction];
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateCompleteTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions(embeddedTransactions),
            embeddedTransactions,
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

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

    // Announce and wait for confirmation
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log(
        'Built aggregate transaction with hash:', transactionHash);
    await announceTransaction(jsonPayload, 'aggregate transaction');
    await waitForConfirmation(transactionHash, 'aggregate 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}` +
        `&targetId=${mosaicIdHex}` +
        '&metadataType=1';
    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('Updated mosaic');
    const updateValue = metadataUpdateValue(currentValue, newValue);

    // Create the update transaction with XOR'd value
    const embeddedUpdate =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.MosaicMetadataTransactionV1Descriptor(
                signerAddress,
                scopedMetadataKey,
                new models.UnresolvedMosaicId(mosaicId),
                // valueSizeDelta is the difference in length
                // (can be negative)
                newValue.length - currentValue.length,
                updateValue),
            signerKeyPair.publicKey);
    console.log('Created embedded update transaction:');
    console.log(JSON.stringify(embeddedUpdate.toJson(), null, 2));

    // Build the aggregate for the update
    const updateEmbedded = [embeddedUpdate];
    const updateTransaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateCompleteTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions(updateEmbedded),
            updateEmbedded,
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

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

    // Announce and wait for confirmation
    const updateHash =
        facade.hashTransaction(updateTransaction).toString();
    console.log(
        'Built aggregate transaction with hash:', updateHash);
    await announceTransaction(updatePayload, 'aggregate transaction');
    await waitForConfirmation(updateHash, 'aggregate 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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
import org.symbol.sdk.symbol.Address;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.Metadata;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.descriptors.*;
import org.symbol.sdk.symbol.models.*;

public final class MosaicMetadata {
    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);
            final String statusPath =
                "/transactionStatus/" + transactionHash;
            final HttpRequest statusRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> statusResponse = HTTP_CLIENT
                .send(statusRequest, BodyHandlers.ofString());
            if (404 == statusResponse.statusCode()) {
                System.out.println("  Transaction status: unknown");
                continue;
            }
            if (2 != statusResponse.statusCode() / 100)
                throw new IOException(
                    "HTTP " + statusResponse.statusCode());

            final JsonNode status =
                JSON_MAPPER.readTree(statusResponse.body());
            final String group = status.get("group").asText();
            System.out.printf("  Transaction status: %s%n", group);
            if ("confirmed".equals(group)) {
                System.out.printf("%s confirmed in %d seconds%n",
                    label, attempt);
                return;
            }
            if ("failed".equals(group))
                throw new IOException(String.format("%s failed: %s",
                    label, status.get("code").asText()));
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", label));
    }

    public static void main(final String[] args) {
        try {
            new MosaicMetadata().run();
        } catch (final Exception ex) {
            System.out.println(null == ex.getMessage()
                ? ex.toString()
                : ex.getMessage());
        }
    }

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


        final String privateKeyString = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        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);

        // Get mosaic ID from environment
        final String mosaicIdHex = System.getenv().getOrDefault(
            "MOSAIC_ID", "6D1314BE751B62C2");
        final long mosaicId = Long.parseUnsignedLong(mosaicIdHex, 16);
        System.out.printf("Mosaic ID: %d (0x%016X)%n",
            mosaicId, mosaicId);


        // 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(
            "description_%d", System.currentTimeMillis() / 1000);
        final long scopedMetadataKey = Metadata.generateKey(keyString);
        final byte[] metadataValue =
            "My first mosaic".getBytes(StandardCharsets.UTF_8);

        // Create the embedded metadata transaction
        final EmbeddedTransaction embeddedTransaction =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new MosaicMetadataTransactionV1Descriptor(
                    signerAddress,
                    scopedMetadataKey,
                    new UnresolvedMosaicId(mosaicId),
                    // When creating new metadata, valueSizeDelta
                    // equals value length
                    metadataValue.length,
                    metadataValue),
                signerKeyPair.getPublicKey());
        System.out.println("Created embedded metadata transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(embeddedTransaction.toJson()));

        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(embeddedTransaction);
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        embeddedTransactions),
                    embeddedTransactions,
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign and generate final payload
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        String jsonPayload = SymbolTransactionFactory.attachSignature(
            transaction, signature);

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

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

        // Fetch current metadata value from network
        final String metadataPath = String.format(
            "/metadata?sourceAddress=%s&targetAddress=%s"
                + "&scopedMetadataKey=%016X&targetId=%016X"
                + "&metadataType=1",
            signerAddress, signerAddress, scopedMetadataKey, mosaicId);
        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
        if (metadataJson.get("data").isEmpty())
            throw new IOException("Metadata entry not found");
        final JsonNode metadataEntry = metadataJson.get("data").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 =
            "Updated mosaic".getBytes(StandardCharsets.UTF_8);
        final byte[] updateValue = Metadata.updateValue(
            currentValue, newValue);

        // Create the update transaction with XOR'd value
        final EmbeddedTransaction embeddedUpdate =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new MosaicMetadataTransactionV1Descriptor(
                    signerAddress,
                    scopedMetadataKey,
                    new UnresolvedMosaicId(mosaicId),
                    // valueSizeDelta is the difference in length
                    // (can be negative)
                    newValue.length - currentValue.length,
                    updateValue),
                signerKeyPair.getPublicKey());
        System.out.println("Created embedded update transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(embeddedUpdate.toJson()));

        // Build the aggregate for the update
        final List<EmbeddedTransaction> updateEmbeddedTransactions =
            List.of(embeddedUpdate);
        final Transaction updateTransaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        updateEmbeddedTransactions),
                    updateEmbeddedTransactions,
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign the update
        final CryptoTypes.Signature updateSignature =
            facade.signTransaction(signerKeyPair, updateTransaction);
        final String updatePayload = SymbolTransactionFactory
            .attachSignature(updateTransaction, updateSignature);

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

    }
}

Download source

Code Explanation⚓︎

This tutorial demonstrates adding new metadata to a mosaic and then updating that metadata.

Setting Up the Account and Mosaic⚓︎

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

# Get mosaic ID from environment
MOSAIC_ID = os.getenv('MOSAIC_ID', '6D1314BE751B62C2')
mosaic_id = int(MOSAIC_ID, 16)
print(f'Mosaic ID: {mosaic_id} (0x{mosaic_id:016X})')
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());

// Get mosaic ID from environment
const MOSAIC_ID = process.env.MOSAIC_ID || '6D1314BE751B62C2';
const mosaicId = BigInt(`0x${MOSAIC_ID}`);
const mosaicIdHex = mosaicId.toString(16)
    .toUpperCase().padStart(16, '0');
console.log('Mosaic ID:', mosaicId.toString(), `(0x${mosaicIdHex})`);
        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);

        // Get mosaic ID from environment
        final String mosaicIdHex = System.getenv().getOrDefault(
            "MOSAIC_ID", "6D1314BE751B62C2");
        final long mosaicId = Long.parseUnsignedLong(mosaicIdHex, 16);
        System.out.printf("Mosaic ID: %d (0x%016X)%n",
            mosaicId, mosaicId);

The snippet reads the signer's private key from the SIGNER_PRIVATE_KEY environment variable, which defaults to a test key if not set. The signer's address is derived from the public key.

The ID of the mosaic to which the metadata will be attached is read from the MOSAIC_ID environment variable. This value is an unprefixed hexadecimal string, which defaults to a test value if not set.

Mosaic must exist

The mosaic must already be created and owned by the signer account, or the transaction adding the metadata will be rejected.

See the Creating a Mosaic tutorial to learn how to create one.

    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')
    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);
        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJson = JSON_MAPPER.readTree(feeResponse.body());
        final long feeMultiplier = Math.max(
            feeJson.get("medianFeeMultiplier").asLong(),
            feeJson.get("minFeeMultiplier").asLong());
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

Recommended fees are fetched from /network/fees/transaction GET, following the process described in the Transfer Transaction tutorial.

Defining the Metadata⚓︎

Each mosaic metadata entry is uniquely identified by:

  • The signer's address: the account adding the metadata
  • The target account's address: the mosaic owner, whose signature is required
  • The target mosaic ID
  • A scoped metadata key: a 64-bit value chosen by the metadata creator

    The SDK provides a helper function that generates this key from a human-readable string using SHA3-256 hashing. This approach makes keys more meaningful and reduces the chance of collisions.

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

In this example, the key is derived from the string description. For demonstration purposes, a timestamp is appended to the key string, so each time the code is executed a new entry is added to the mosaic. In practice, you would use a fixed key that identifies the specific metadata entry you want to create or update.

The metadata value can be any sequence of up to 1024 bytes. In this example, the value is the string My first mosaic encoded in UTF-8.

Multiple entries with the same key

The key is only one of the 4 parts that identify a metadata entry, so a change in any part produces a different entry.

For example, different accounts can use the same scoped metadata key on the same mosaic without conflict, because the signer's address is different.

Each entry is independent and can only be updated by the account that originally created it.

Creating the Embedded Mosaic Metadata Transaction⚓︎

    # Create the embedded metadata transaction
    embedded_transaction = (
        facade.create_embedded_transaction_from_descriptor(
            {
                'type': 'mosaic_metadata_transaction_v1',
                'target_address': signer_address,
                'target_mosaic_id': mosaic_id,
                '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
            },
            signer_key_pair.public_key))
    print('Created embedded metadata transaction:')
    print(json.dumps(embedded_transaction.to_json(), indent=2))
    // Create the embedded metadata transaction
    const embeddedTransaction =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.MosaicMetadataTransactionV1Descriptor(
                signerAddress,
                scopedMetadataKey,
                new models.UnresolvedMosaicId(mosaicId),
                // When creating new metadata, valueSizeDelta
                // equals value length
                metadataValue.length,
                metadataValue),
            signerKeyPair.publicKey);
    console.log('Created embedded metadata transaction:');
    console.log(JSON.stringify(embeddedTransaction.toJson(), null, 2));
        // Create the embedded metadata transaction
        final EmbeddedTransaction embeddedTransaction =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new MosaicMetadataTransactionV1Descriptor(
                    signerAddress,
                    scopedMetadataKey,
                    new UnresolvedMosaicId(mosaicId),
                    // When creating new metadata, valueSizeDelta
                    // equals value length
                    metadataValue.length,
                    metadataValue),
                signerKeyPair.getPublicKey());
        System.out.println("Created embedded metadata transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(embeddedTransaction.toJson()));

A mosaic metadata transaction attaches a key-value pair to a mosaic on the blockchain. The same transaction type handles both adding new metadata entries and updating existing ones.

Symbol requires these transactions to be inside an aggregate transaction that includes both the signer account and the mosaic owner's signature. This prevents unwanted metadata from being attached to a mosaic without its owner's permission.

In this tutorial, the signer is also the mosaic owner so only one signature is needed. However, the transaction still needs to be inside an aggregate, so the code defines it as an embedded transaction. The transaction's descriptor contains:

  • Type: Use MosaicMetadataTransactionV1.

  • Target address: The mosaic owner's address. When the signer differs from the mosaic owner, the owner must cosign the aggregate transaction.

  • Target mosaic ID: The mosaic to attach the metadata to.

  • Scoped metadata key: The 64-bit key used to identify this metadata entry.

  • Value size delta: When creating new metadata, set this to the byte length of the value. When updating existing metadata, set this to the difference between the new and current value lengths.

  • Value: The metadata content as bytes. When creating new metadata, provide the raw value. When updating, provide a computed value (explained in the Modifying Existing Metadata section).

The signer public key is passed separately to .

Building the Aggregate Transaction⚓︎

    # Build the aggregate transaction
    embedded_transactions = [embedded_transaction]
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                embedded_transactions),
            'transactions': embedded_transactions
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTransaction];
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateCompleteTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions(embeddedTransactions),
            embeddedTransactions,
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(embeddedTransaction);
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        embeddedTransactions),
                    embeddedTransactions,
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

The code adds the embedded mosaic metadata transaction to an aggregate transaction.

Since the signer is the mosaic owner, no cosignatures are required and the aggregate can be created as complete, allowing it to be signed and announced immediately.

Adding metadata by a different account

If the signer is different from the mosaic owner, the owner must cosign the aggregate transaction to approve the metadata entry.

For details on collecting cosignatures on-chain, see the Bonded Aggregate tutorial.

Submitting the Aggregate Transaction⚓︎

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

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

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

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

The aggregate transaction is signed and announced following the same process as in Creating a Complete Aggregate Transaction.

Retrieving Metadata⚓︎

    # Fetch current metadata value from network
    metadata_path = (
        f'/metadata?sourceAddress={signer_address}'
        f'&targetAddress={signer_address}'
        f'&scopedMetadataKey={scoped_metadata_key:016X}'
        f'&targetId={mosaic_id:016X}'
        '&metadataType=1'
    )
    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}` +
        `&targetId=${mosaicIdHex}` +
        '&metadataType=1';
    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 metadataPath = String.format(
            "/metadata?sourceAddress=%s&targetAddress=%s"
                + "&scopedMetadataKey=%016X&targetId=%016X"
                + "&metadataType=1",
            signerAddress, signerAddress, scopedMetadataKey, mosaicId);
        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
        if (metadataJson.get("data").isEmpty())
            throw new IOException("Metadata entry not found");
        final JsonNode metadataEntry = metadataJson.get("data").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));

To retrieve the current value of a metadata entry, the code uses the /metadata GET endpoint with filters for sourceAddress, targetAddress, scopedMetadataKey, targetId (the mosaic ID), and metadataType (1 for mosaic metadata).

The endpoint returns the list of entries matching the filters, which in this case contains a single item.

Modifying Existing Metadata⚓︎

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

    # Create the update transaction with XOR'd value
    embedded_update = facade.create_embedded_transaction_from_descriptor(
        {
            'type': 'mosaic_metadata_transaction_v1',
            'target_address': signer_address,
            'target_mosaic_id': mosaic_id,
            '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
        },
        signer_key_pair.public_key)
    print('Created embedded update transaction:')
    print(json.dumps(embedded_update.to_json(), indent=2))
    // XOR the current and new values
    const newValue = new TextEncoder().encode('Updated mosaic');
    const updateValue = metadataUpdateValue(currentValue, newValue);

    // Create the update transaction with XOR'd value
    const embeddedUpdate =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.MosaicMetadataTransactionV1Descriptor(
                signerAddress,
                scopedMetadataKey,
                new models.UnresolvedMosaicId(mosaicId),
                // valueSizeDelta is the difference in length
                // (can be negative)
                newValue.length - currentValue.length,
                updateValue),
            signerKeyPair.publicKey);
    console.log('Created embedded update transaction:');
    console.log(JSON.stringify(embeddedUpdate.toJson(), null, 2));
        // XOR the current and new values
        final byte[] newValue =
            "Updated mosaic".getBytes(StandardCharsets.UTF_8);
        final byte[] updateValue = Metadata.updateValue(
            currentValue, newValue);

        // Create the update transaction with XOR'd value
        final EmbeddedTransaction embeddedUpdate =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new MosaicMetadataTransactionV1Descriptor(
                    signerAddress,
                    scopedMetadataKey,
                    new UnresolvedMosaicId(mosaicId),
                    // valueSizeDelta is the difference in length
                    // (can be negative)
                    newValue.length - currentValue.length,
                    updateValue),
                signerKeyPair.getPublicKey());
        System.out.println("Created embedded update transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(embeddedUpdate.toJson()));

Updating an existing metadata entry requires the current value, retrieved from the network as previously shown.

To demonstrate updating metadata, the code changes the description from My first mosaic to Updated mosaic by creating another MosaicMetadataTransactionV1 transaction with the same scoped metadata key.

Modifying an existing metadata value differs from creating a new one in that the updated value must be defined in terms of the current value, using the following fields:

  • : The difference in length between the new and current values. In this example, the delta is -1 because the string Updated mosaic (14 bytes) is one byte shorter than My first mosaic (15 bytes).

  • value: The XOR'd bytes computed by comparing the current and new values byte-by-byte.

    The SDK provides a helper function that handles the XOR calculation. The XOR operation compares each byte: matching bytes become zero, and differing bytes capture the change.

Note that represents the difference in final value lengths (new vs current), not the length of the XOR'd bytes themselves.

Deleting a metadata entry

To delete a metadata entry, set to the negative of the current value length and provide the current value as value. The XOR produces an empty result, which removes the entry from the network.

As with the initial metadata creation, this metadata modification is wrapped in an aggregate transaction and then signed and announced.

    # Build the aggregate for the update
    embedded_transactions = [embedded_update]
    update_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                embedded_transactions),
            'transactions': embedded_transactions
        },
        signer_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)

    # Sign the update
    signature = facade.sign_transaction(
        signer_key_pair, update_transaction)
    json_payload = facade.transaction_factory.attach_signature(
        update_transaction, signature)

    # Announce and wait for confirmation
    update_hash = facade.hash_transaction(update_transaction)
    print(f'Built aggregate transaction with hash: {update_hash}')
    announce_transaction(json_payload, 'aggregate transaction')
    wait_for_confirmation(update_hash, 'aggregate transaction')
    // Build the aggregate for the update
    const updateEmbedded = [embeddedUpdate];
    const updateTransaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateCompleteTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions(updateEmbedded),
            updateEmbedded,
            undefined),
        signerKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

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

    // Announce and wait for confirmation
    const updateHash =
        facade.hashTransaction(updateTransaction).toString();
    console.log(
        'Built aggregate transaction with hash:', updateHash);
    await announceTransaction(updatePayload, 'aggregate transaction');
    await waitForConfirmation(updateHash, 'aggregate transaction');
        // Build the aggregate for the update
        final List<EmbeddedTransaction> updateEmbeddedTransactions =
            List.of(embeddedUpdate);
        final Transaction updateTransaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        updateEmbeddedTransactions),
                    updateEmbeddedTransactions,
                    null),
                signerKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign the update
        final CryptoTypes.Signature updateSignature =
            facade.signTransaction(signerKeyPair, updateTransaction);
        final String updatePayload = SymbolTransactionFactory
            .attachSignature(updateTransaction, updateSignature);

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

Output⚓︎

The output shown below corresponds to a typical run of the program.

Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Mosaic ID: 7859648582932718274 (0x6D1314BE751B62C2)
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": 16964,
  "target_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9",
  "scoped_metadata_key": "15731601119740876263",
  "target_mosaic_id": "7859648582932718274",
  "value_size_delta": 15,
  "value": "4d79206669727374206d6f73616963"
}
Built aggregate transaction with hash: 900741B895CD8CA31F969C8929BBDE1331F1B93565CEA276283FD0122EAC77C9
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: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
aggregate transaction confirmed in 21 seconds

--- Modifying existing metadata ---
Fetching current metadata from /metadata?sourceAddress=TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I&targetAddress=TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I&scopedMetadataKey=DA51DFDE6A4AD5E7&targetId=6D1314BE751B62C2&metadataType=1
  Current value: My first mosaic
Created embedded update transaction:
{
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16964,
  "target_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9",
  "scoped_metadata_key": "15731601119740876263",
  "target_mosaic_id": "7859648582932718274",
  "value_size_delta": -1,
  "value": "180944071d1717544d021c12080a63"
}
Built aggregate transaction with hash: A05078F485403FAEC0F740EFAA997241C26E2C25DFFD9E023113C861943ACB92
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: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
aggregate transaction confirmed in 26 seconds

Key points in the output:

  • Line 3 (Mosaic ID): The mosaic ID used, in decimal and hexadecimal formats.
  • Line 15 ("scoped_metadata_key"): The 64-bit key generated from the input string using SHA3-256 hashing.
  • Line 16 ("target_mosaic_id"): The mosaic receiving the metadata.
  • Line 17 ("value_size_delta": 15): When creating new metadata, this equals the byte length of the value ("My first mosaic" = 15 bytes).
  • Line 20: The transaction hash for looking up the metadata creation in the explorer.
  • Line 34 (Current value: My first mosaic): Retrieved from the network before updating.
  • Line 44 ("value_size_delta": -1): Negative because the new value ("Updated mosaic" = 14 bytes) is shorter than the current value (15 bytes).
  • Line 45 ("value"): The XOR'd value computed from the current and new values, not the raw new value.
  • Line 47: The transaction hash for looking up the metadata update in the explorer.

The transaction hashes can be used to search for the transactions in the Symbol Testnet Explorer.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Define metadata key and value
Create a mosaic metadata transaction , MosaicMetadataTransactionV1
Retrieve metadata /metadata GET
Modify existing metadata