コンテンツにスキップ

転送トランザクションでモザイクを送信する⚓︎

中級

転送トランザクション には、XEM の代わりに、または XEM と一緒に、他の モザイク を含められます。

このチュートリアルでは、通常の XEM 送信 と異なる部分に焦点を当て、モザイクを送信する方法を説明します。

Transfer company:tokenAABBA->B100 company:token

この例では、テストネットに存在する company:token モザイクを 100 単位送信します。すでにいくらか保有しているデフォルトのテストアカウントを使います。 署名アカウントが十分な量を所有していれば、他のモザイクでも同じ流れを使えます。

前提条件⚓︎

始める前に、次の準備をしてください。

完全なコード⚓︎

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

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.NemFacade import NemFacade
from symbolchain.nc import Amount
from symbolchain.nem.FeeCalculator import calculate_transaction_fee

NODE_URL = os.getenv('NODE_URL', 'http://libertalia.nemtest.net:7890')
print(f'Using node {NODE_URL}')


# Helper function to announce a transaction
def announce_transaction(payload, label):
    announce_path = '/transaction/announce'
    print(f'Announcing {label} to {announce_path}')
    request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    with urllib.request.urlopen(request) as announce_response:
        result = json.loads(announce_response.read().decode())
    print(f'  Result: {result["message"]}')
    return result['message']


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    status_path = f'/transaction/get?hash={tx_hash}'
    print(f'Waiting for {label} confirmation from {status_path}')
    is_confirmed = False
    for _ in range(120):
        try:
            with urllib.request.urlopen(
                f'{NODE_URL}{status_path}'
            ) as status_response:
                confirmed = json.loads(status_response.read().decode())
                height = confirmed['meta']['height']
                print(f'{label} confirmed in block {height}')
                is_confirmed = True
                break
        except urllib.error.HTTPError:
            print('  Transaction status: pending')
        time.sleep(1)
    if not is_confirmed:
        print(f'{label} confirmation took too long.')



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

RECIPIENT_ADDRESS = os.getenv(
    'RECIPIENT_ADDRESS',
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4')


MOSAIC_ID = os.getenv('MOSAIC_ID', 'company:token')
MOSAIC_NAMESPACE, MOSAIC_NAME = MOSAIC_ID.split(':')
QUANTITY = int(os.getenv('QUANTITY', '100'))
print(f'Sending mosaic {MOSAIC_ID}')
print(f'  Amount: {QUANTITY} units')

facade = NemFacade('testnet')

try:
    # Fetch the mosaic's divisibility and supply
    definition_path = f'/mosaic/definition?mosaicId={MOSAIC_ID}'
    print(f'Fetching mosaic definition from {definition_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{definition_path}') as response:
        definition = json.loads(response.read().decode())
    properties = {
        prop['name']: prop['value']
        for prop in definition['properties']
    }
    divisibility = int(properties['divisibility'])

    supply_path = f'/mosaic/supply?mosaicId={MOSAIC_ID}'
    print(f'Fetching mosaic supply from {supply_path}')
    with urllib.request.urlopen(f'{NODE_URL}{supply_path}') as response:
        supply = json.loads(response.read().decode())['supply']
    print(f'  {MOSAIC_ID}: divisibility {divisibility}, supply {supply}')

    # Build the transaction
    atomic_quantity = QUANTITY * (10 ** divisibility)
    multiplier = 1
    scaled_multiplier = multiplier * 1_000_000
    transaction = facade.create_transaction_from_descriptor({
        'type': 'transfer_transaction_v2',
        'recipient_address': RECIPIENT_ADDRESS,
        'amount': scaled_multiplier,
        'mosaics': [{
            'mosaic': {
                'mosaic_id': {
                    'namespace_id': {'name': MOSAIC_NAMESPACE},
                    'name': MOSAIC_NAME
                },
                'amount': atomic_quantity
            }
        }]
    }, signer_key_pair.public_key, 0, 2 * 60 * 60)

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(
        transaction,
        {MOSAIC_ID: {'supply': supply, 'divisibility': divisibility}})
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')

    # Sign transaction and generate final payload
    signature = facade.sign_transaction(signer_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)
    print('Built transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    # Announce the transaction
    announce_result = announce_transaction(json_payload, 'transaction')

    # Wait for confirmation
    if 'SUCCESS' == announce_result:
        transaction_hash = facade.hash_transaction(transaction)
        print(f'Transaction hash: {transaction_hash}')
        wait_for_confirmation(transaction_hash, 'transaction')
    else:
        print(f'Transaction rejected: {announce_result}')

except Exception as error:
    print(error)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    Address,
    NemFacade,
    calculateTransactionFee,
    descriptors,
    models
} from 'symbol-sdk/nem';

const NODE_URL = process.env.NODE_URL ||
    'http://libertalia.nemtest.net:7890';
console.log('Using node', NODE_URL);

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    const announcePath = '/transaction/announce';
    console.log(`Announcing ${label} to ${announcePath}`);
    const response = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    if (!response.ok)
        throw new Error(`HTTP ${response.status}`);
    const result = await response.json();
    console.log('  Result:', result.message);
    return result.message;
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    const statusPath = `/transaction/get?hash=${transactionHash}`;
    console.log(`Waiting for ${label} confirmation from`, statusPath);
    let isConfirmed = false;
    for (let attempt = 1; 120 >= attempt; ++attempt) {
        const response = await fetch(`${NODE_URL}${statusPath}`);
        if (!response.ok) {
            console.log('  Transaction status: pending');
            await new Promise(resolve => { setTimeout(resolve, 1000); });
        } else {
            const confirmed = await response.json();
            console.log(`${label} confirmed in block`,
                confirmed.meta.height);
            isConfirmed = true;
            break;
        }
    }
    if (!isConfirmed)
        console.warn(`${label} confirmation took too long.`);
}


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

const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS ||
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';


const MOSAIC_ID = process.env.MOSAIC_ID || 'company:token';
const [MOSAIC_NAMESPACE, MOSAIC_NAME] = MOSAIC_ID.split(':');
const QUANTITY = parseInt(process.env.QUANTITY || '100', 10);
console.log('Sending mosaic', MOSAIC_ID);
console.log(`  Amount: ${QUANTITY} units`);

const facade = new NemFacade('testnet');

try {
    // Fetch the mosaic's divisibility and supply
    const definitionPath = `/mosaic/definition?mosaicId=${MOSAIC_ID}`;
    console.log('Fetching mosaic definition from', definitionPath);
    const definitionResponse =
        await fetch(`${NODE_URL}${definitionPath}`);
    const definition = await definitionResponse.json();
    const properties = Object.fromEntries(
        definition.properties.map(
            property => [property.name, property.value]));
    const divisibility = parseInt(properties.divisibility, 10);

    const supplyPath = `/mosaic/supply?mosaicId=${MOSAIC_ID}`;
    console.log('Fetching mosaic supply from', supplyPath);
    const supplyResponse = await fetch(`${NODE_URL}${supplyPath}`);
    const { supply } = await supplyResponse.json();
    console.log(`  ${MOSAIC_ID}: divisibility ${divisibility},`,
        `supply ${supply}`);

    // Build the transaction
    const atomicQuantity = QUANTITY * (10 ** divisibility);
    const multiplier = 1;
    const scaledMultiplier = multiplier * 1_000_000;
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV2Descriptor(
            new Address(RECIPIENT_ADDRESS),
            new models.Amount(BigInt(scaledMultiplier)),
            undefined,
            [new descriptors.SizePrefixedMosaicDescriptor(
                new descriptors.MosaicDescriptor(
                    new descriptors.MosaicIdDescriptor(
                        new descriptors.NamespaceIdDescriptor(
                            MOSAIC_NAMESPACE),
                        MOSAIC_NAME),
                    new models.Amount(BigInt(atomicQuantity))))]),
        signerKeyPair.publicKey,
        0n,
        2 * 60 * 60);

    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction, {
        [MOSAIC_ID]: { supply: BigInt(supply), divisibility }
    });
    transaction.fee = new models.Amount(fee);
    console.log(`  Transaction fee: ${Number(fee) / 1_000_000} XEM`);

    // Sign transaction and generate final payload
    const signature = facade.signTransaction(signerKeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built transaction:');
    console.dir(transaction.toJson(), { colors: true, depth: null });

    // Announce the transaction
    const announceResult = await announceTransaction(
        jsonPayload, 'transaction');

    // Wait for confirmation
    if ('SUCCESS' === announceResult) {
        const transactionHash = facade.hashTransaction(transaction)
            .toString();
        console.log('Transaction hash:', transactionHash);
        await waitForConfirmation(transactionHash, 'transaction');
    } else {
        console.log('Transaction rejected:', announceResult);
    }

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

Download source

//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.3

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.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.NemFacade;
import org.symbol.sdk.nem.Address;
import org.symbol.sdk.nem.FeeCalculator;
import org.symbol.sdk.nem.FeeCalculator.MosaicInformation;
import org.symbol.sdk.nem.KeyPair;
import org.symbol.sdk.nem.NemTransactionFactory;
import org.symbol.sdk.nem.descriptors.MosaicDescriptor;
import org.symbol.sdk.nem.descriptors.MosaicIdDescriptor;
import org.symbol.sdk.nem.descriptors.NamespaceIdDescriptor;
import org.symbol.sdk.nem.descriptors.SizePrefixedMosaicDescriptor;
import org.symbol.sdk.nem.descriptors.TransferTransactionV2Descriptor;
import org.symbol.sdk.nem.models.Amount;
import org.symbol.sdk.nem.models.Transaction;

public final class TransferMosaics {
    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", "http://libertalia.nemtest.net:7890");

    private final NemFacade facade = new NemFacade("testnet");

    private String announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        final String announcePath = "/transaction/announce";
        System.out.printf("Announcing %s to %s%n", label, announcePath);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + announcePath))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        if (2 != response.statusCode() / 100)
            throw new IOException("HTTP " + response.statusCode());
        final String result = JSON_MAPPER.readTree(response.body())
            .get("message").asText();
        System.out.printf("  Result: %s%n", result);
        return result;
    }

    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        final String statusPath = String.format(
            "/transaction/get?hash=%s", transactionHash);
        System.out.printf("Waiting for %s confirmation from %s%n",
            label, statusPath);
        boolean isConfirmed = false;
        for (int attempt = 1; 120 >= attempt; ++attempt) {
            final HttpRequest request = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> response = HTTP_CLIENT.send(
                request, BodyHandlers.ofString());
            if (2 != response.statusCode() / 100) {
                System.out.println("  Transaction status: pending");
                Thread.sleep(1000);
            } else {
                final JsonNode confirmed = JSON_MAPPER.readTree(
                    response.body());
                System.out.printf("%s confirmed in block %s%n", label,
                    confirmed.get("meta").get("height").asText());
                isConfirmed = true;
                break;
            }
        }
        if (!isConfirmed)
            System.out.printf("%s confirmation took too long.%n", label);
    }

    public static void main(final String[] args) {
        try {
            new TransferMosaics().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 signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));
        final String recipientAddress = System.getenv().getOrDefault(
            "RECIPIENT_ADDRESS",
            "TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4");


        final String mosaicId = System.getenv().getOrDefault(
            "MOSAIC_ID", "company:token");
        final String[] mosaicIdParts = mosaicId.split(":");
        final String mosaicNamespace = mosaicIdParts[0];
        final String mosaicName = mosaicIdParts[1];
        final int quantity = Integer.parseInt(
            System.getenv().getOrDefault("QUANTITY", "100"));
        System.out.printf("Sending mosaic %s%n", mosaicId);
        System.out.printf("  Amount: %d units%n", quantity);


        // Fetch the mosaic's divisibility and supply
        final String definitionPath = String.format(
            "/mosaic/definition?mosaicId=%s", mosaicId);
        System.out.printf("Fetching mosaic definition from %s%n",
            definitionPath);
        final HttpRequest definitionRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + definitionPath)).GET().build();
        final HttpResponse<String> definitionResponse = HTTP_CLIENT.send(
            definitionRequest, BodyHandlers.ofString());
        final JsonNode definition = JSON_MAPPER.readTree(
            definitionResponse.body());
        int divisibility = 0;
        for (final JsonNode property : definition.get("properties")) {
            if ("divisibility".equals(property.get("name").asText()))
                divisibility = property.get("value").asInt();
        }
        final String supplyPath = String.format(
            "/mosaic/supply?mosaicId=%s", mosaicId);
        System.out.printf("Fetching mosaic supply from %s%n", supplyPath);
        final HttpRequest supplyRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + supplyPath)).GET().build();
        final HttpResponse<String> supplyResponse = HTTP_CLIENT.send(
            supplyRequest, BodyHandlers.ofString());
        final long supply = JSON_MAPPER.readTree(supplyResponse.body())
            .get("supply").asLong();
        System.out.printf("  %s: divisibility %d, supply %d%n",
            mosaicId, divisibility, supply);

        // Build the transaction
        final long atomicQuantity = Math.round(
            quantity * Math.pow(10, divisibility));
        final long scaledMultiplier = 1_000_000;
        final MosaicIdDescriptor mosaicIdDescriptor =
            new MosaicIdDescriptor(
                new NamespaceIdDescriptor(mosaicNamespace.getBytes(UTF_8)),
                mosaicName.getBytes(UTF_8));
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV2Descriptor(
                    new Address(recipientAddress),
                    new Amount(scaledMultiplier),
                    null,
                    List.of(new SizePrefixedMosaicDescriptor(
                        new MosaicDescriptor(
                            mosaicIdDescriptor,
                            new Amount(atomicQuantity))))),
                signerKeyPair.getPublicKey(),
                0,
                2 * 60 * 60);

        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction, Map.of(mosaicId,
                new MosaicInformation(supply, divisibility)));
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) fee / 1_000_000);

        // Sign transaction and generate final payload
        final CryptoTypes.Signature signature = facade.signTransaction(
            signerKeyPair, transaction);
        final String jsonPayload = NemTransactionFactory.attachSignature(
            transaction, signature);
        System.out.println("Built transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        // Announce the transaction
        final String announceResult = announceTransaction(
            jsonPayload, "transaction");

        // Wait for confirmation
        if ("SUCCESS".equals(announceResult)) {
            final String transactionHash = facade.hashTransaction(
                transaction).toString();
            System.out.printf("Transaction hash: %s%n", transactionHash);
            waitForConfirmation(transactionHash, "transaction");
        } else {
            System.out.printf(
                "Transaction rejected: %s%n", announceResult);
        }

    }
}

Download source

コードの説明⚓︎

署名、アナウンス、承認待ちは XEM を送信する チュートリアルと同じなので、ここでは説明しません。 以下では、異なる手順だけを説明します。

アカウントをセットアップする⚓︎

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

RECIPIENT_ADDRESS = os.getenv(
    'RECIPIENT_ADDRESS',
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4')
const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new NemFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));

const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS ||
    'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';
        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));
        final String recipientAddress = System.getenv().getOrDefault(
            "RECIPIENT_ADDRESS",
            "TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4");

すべてのモザイク転送には、送信者 と 受取人 の 2 つのアカウントが関係します。

送信者 は、トランザクションに署名して手数料を支払い、送信するモザイクを保有する アカウント です。 秘密鍵は SIGNER_PRIVATE_KEY 環境変数から読み込みます。 指定されていない場合は、テスト用のキーをデフォルト値として使います。

受取人 はモザイクを受け取るアカウントです。 その アドレス は RECIPIENT_ADDRESS 環境変数から読み込みます。 指定されていない場合は、テスト用のアドレスをデフォルト値として使います。

デフォルトのテストアカウントには company:token があらかじめ資金提供されていますが、残高はチュートリアルの実行間で共有され、使い切る可能性があります。 残高不足で転送が失敗する場合は、SIGNER_PRIVATE_KEY をモザイクを保有するアカウントに設定してください。

モザイクをセットアップする⚓︎

MOSAIC_ID = os.getenv('MOSAIC_ID', 'company:token')
MOSAIC_NAMESPACE, MOSAIC_NAME = MOSAIC_ID.split(':')
QUANTITY = int(os.getenv('QUANTITY', '100'))
print(f'Sending mosaic {MOSAIC_ID}')
print(f'  Amount: {QUANTITY} units')
const MOSAIC_ID = process.env.MOSAIC_ID || 'company:token';
const [MOSAIC_NAMESPACE, MOSAIC_NAME] = MOSAIC_ID.split(':');
const QUANTITY = parseInt(process.env.QUANTITY || '100', 10);
console.log('Sending mosaic', MOSAIC_ID);
console.log(`  Amount: ${QUANTITY} units`);
        final String mosaicId = System.getenv().getOrDefault(
            "MOSAIC_ID", "company:token");
        final String[] mosaicIdParts = mosaicId.split(":");
        final String mosaicNamespace = mosaicIdParts[0];
        final String mosaicName = mosaicIdParts[1];
        final int quantity = Integer.parseInt(
            System.getenv().getOrDefault("QUANTITY", "100"));
        System.out.printf("Sending mosaic %s%n", mosaicId);
        System.out.printf("  Amount: %d units%n", quantity);

送信するモザイクは MOSAIC_ID で指定します。これは <namespace>:<mosaic_name> 形式の 完全修飾名 で、デフォルト値は company:token です。 設定すれば、署名アカウントが所有する他のモザイクをチュートリアルで指定できます。

QUANTITY は、送信するモザイクの量を 全体単位 で指定する値で、デフォルトは 100 です。

モザイク定義と供給量を取得する⚓︎

    # Fetch the mosaic's divisibility and supply
    definition_path = f'/mosaic/definition?mosaicId={MOSAIC_ID}'
    print(f'Fetching mosaic definition from {definition_path}')
    with urllib.request.urlopen(
        f'{NODE_URL}{definition_path}') as response:
        definition = json.loads(response.read().decode())
    properties = {
        prop['name']: prop['value']
        for prop in definition['properties']
    }
    divisibility = int(properties['divisibility'])

    supply_path = f'/mosaic/supply?mosaicId={MOSAIC_ID}'
    print(f'Fetching mosaic supply from {supply_path}')
    with urllib.request.urlopen(f'{NODE_URL}{supply_path}') as response:
        supply = json.loads(response.read().decode())['supply']
    print(f'  {MOSAIC_ID}: divisibility {divisibility}, supply {supply}')
    // Fetch the mosaic's divisibility and supply
    const definitionPath = `/mosaic/definition?mosaicId=${MOSAIC_ID}`;
    console.log('Fetching mosaic definition from', definitionPath);
    const definitionResponse =
        await fetch(`${NODE_URL}${definitionPath}`);
    const definition = await definitionResponse.json();
    const properties = Object.fromEntries(
        definition.properties.map(
            property => [property.name, property.value]));
    const divisibility = parseInt(properties.divisibility, 10);

    const supplyPath = `/mosaic/supply?mosaicId=${MOSAIC_ID}`;
    console.log('Fetching mosaic supply from', supplyPath);
    const supplyResponse = await fetch(`${NODE_URL}${supplyPath}`);
    const { supply } = await supplyResponse.json();
    console.log(`  ${MOSAIC_ID}: divisibility ${divisibility},`,
        `supply ${supply}`);
        // Fetch the mosaic's divisibility and supply
        final String definitionPath = String.format(
            "/mosaic/definition?mosaicId=%s", mosaicId);
        System.out.printf("Fetching mosaic definition from %s%n",
            definitionPath);
        final HttpRequest definitionRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + definitionPath)).GET().build();
        final HttpResponse<String> definitionResponse = HTTP_CLIENT.send(
            definitionRequest, BodyHandlers.ofString());
        final JsonNode definition = JSON_MAPPER.readTree(
            definitionResponse.body());
        int divisibility = 0;
        for (final JsonNode property : definition.get("properties")) {
            if ("divisibility".equals(property.get("name").asText()))
                divisibility = property.get("value").asInt();
        }
        final String supplyPath = String.format(
            "/mosaic/supply?mosaicId=%s", mosaicId);
        System.out.printf("Fetching mosaic supply from %s%n", supplyPath);
        final HttpRequest supplyRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + supplyPath)).GET().build();
        final HttpResponse<String> supplyResponse = HTTP_CLIENT.send(
            supplyRequest, BodyHandlers.ofString());
        final long supply = JSON_MAPPER.readTree(supplyResponse.body())
            .get("supply").asLong();
        System.out.printf("  %s: divisibility %d, supply %d%n",
            mosaicId, divisibility, supply);

モザイクの 可分性 を使って転送量を 原子単位 に変換し、現在の供給量とともに手数料を決定します。 トランザクションを構築する前に、両方をノードから取得します。

転送ごとにこれらの値を取得する必要はありません。 モザイクの可分性は作成時に固定され、供給量は供給量を可変として作成された場合だけ変化するため、アプリケーションは両方を一度取得してキャッシュし、必要に応じて供給量を更新できます。

トランザクションを構築する⚓︎

    # Build the transaction
    atomic_quantity = QUANTITY * (10 ** divisibility)
    multiplier = 1
    scaled_multiplier = multiplier * 1_000_000
    transaction = facade.create_transaction_from_descriptor({
        'type': 'transfer_transaction_v2',
        'recipient_address': RECIPIENT_ADDRESS,
        'amount': scaled_multiplier,
        'mosaics': [{
            'mosaic': {
                'mosaic_id': {
                    'namespace_id': {'name': MOSAIC_NAMESPACE},
                    'name': MOSAIC_NAME
                },
                'amount': atomic_quantity
            }
        }]
    }, signer_key_pair.public_key, 0, 2 * 60 * 60)
    // Build the transaction
    const atomicQuantity = QUANTITY * (10 ** divisibility);
    const multiplier = 1;
    const scaledMultiplier = multiplier * 1_000_000;
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV2Descriptor(
            new Address(RECIPIENT_ADDRESS),
            new models.Amount(BigInt(scaledMultiplier)),
            undefined,
            [new descriptors.SizePrefixedMosaicDescriptor(
                new descriptors.MosaicDescriptor(
                    new descriptors.MosaicIdDescriptor(
                        new descriptors.NamespaceIdDescriptor(
                            MOSAIC_NAMESPACE),
                        MOSAIC_NAME),
                    new models.Amount(BigInt(atomicQuantity))))]),
        signerKeyPair.publicKey,
        0n,
        2 * 60 * 60);
        // Build the transaction
        final long atomicQuantity = Math.round(
            quantity * Math.pow(10, divisibility));
        final long scaledMultiplier = 1_000_000;
        final MosaicIdDescriptor mosaicIdDescriptor =
            new MosaicIdDescriptor(
                new NamespaceIdDescriptor(mosaicNamespace.getBytes(UTF_8)),
                mosaicName.getBytes(UTF_8));
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV2Descriptor(
                    new Address(recipientAddress),
                    new Amount(scaledMultiplier),
                    null,
                    List.of(new SizePrefixedMosaicDescriptor(
                        new MosaicDescriptor(
                            mosaicIdDescriptor,
                            new Amount(atomicQuantity))))),
                signerKeyPair.getPublicKey(),
                0,
                2 * 60 * 60);

スニペットはまず、前の手順で取得した可分性を使って、QUANTITY を全体単位から原子単位へ変換します。

全体単位から原子単位へ

作成時に設定され 0~6 の範囲を取るモザイクの divisibility は、次の変換を定義します。 1 全体単位は 10divisibility 原子単位 に相当します。

ここで使う company:token モザイクの可分性は 0 なので、100 = 1 であり、QUANTITY 100 は 100 原子単位としてエンコードされます。

一方、可分性が 2 のモザイクでは、1 全体単位あたり 102 = 100 原子単位です。そのため同じ QUANTITY 100 は 10'000 原子単位としてエンコードされます。

次にスニペットは、最大 10 件を指定できる mosaics フィールドを含むトランザクションのディスクリプタから TransferTransactionV2 を作成します。 各エントリでは、モザイクと送信量を指定します。

  • モザイクを所有する ネームスペース。
  • モザイク名。
  • モザイクの 原子単位 で指定する数量(上で計算済み)。

モザイクを付加すると、最上位の amount は XEM の金額 ではなくなります。 列挙したすべてのモザイクに適用する乗数になり、1_000_000 は係数 1 を表します。 amount を 1_000_000 に設定すると、各モザイクをエントリに指定した数量で送信します。 ファサードは署名者を追加し、2 時間のデッドライン期間からタイムスタンプとデッドラインを導出します。

他のモザイクと一緒に XEM を送信する

最上位の amount は XEM を送るのではなく、乗数として機能します。 別のモザイクと同時に XEM を送るには、mosaics 配列に nem:xem エントリを追加します。 その数量は、送信する XEM の原子単位で指定します。

トランザクション手数料を計算する⚓︎

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(
        transaction,
        {MOSAIC_ID: {'supply': supply, 'divisibility': divisibility}})
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')
    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction, {
        [MOSAIC_ID]: { supply: BigInt(supply), divisibility }
    });
    transaction.fee = new models.Amount(fee);
    console.log(`  Transaction fee: ${Number(fee) / 1_000_000} XEM`);
        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction, Map.of(mosaicId,
                new MosaicInformation(supply, divisibility)));
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) fee / 1_000_000);

モザイク転送の手数料は、各モザイクの供給量、可分性、転送する数量によって決まります。 NEM の固定手数料表を手動で実装する代わりに、スニペットは SDK の ヘルパーを呼び出します。

ヘルパーは前の手順で構築したトランザクションから転送数量を直接読み取り、トランザクションに保存されない値なので、各モザイクの supply(全体単位)と divisibility を 2 つ目の引数として受け取ります。

返された手数料は、署名前に transaction.fee へ割り当てます。

計算の完全なルールについては、手数料 セクションを参照してください。

署名、アナウンス、承認待ち⚓︎

これらの手順は XEM を送信する 場合と同じなので、ここでは詳しく説明しません。

徴収手数料付きモザイク

一部のモザイクには 徴収手数料 があり、そのモザイクを転送するたびに、トランザクション手数料に加えて第三者アカウントへ追加手数料が支払われます。 送信者のアカウントは、徴収手数料用モザイク(転送するモザイクとは異なる場合があります)を支払えるだけ保有していなければ、トランザクションが拒否されます。

出力⚓︎

以下は、プログラムの実行時の出力例です。

Using node http://libertalia.nemtest.net:7890
Sending mosaic company:token
  Amount: 100 units
Fetching mosaic definition from /mosaic/definition?mosaicId=company:token
Fetching mosaic supply from /mosaic/supply?mosaicId=company:token
  company:token: divisibility 0, supply 1000000
  Transaction fee: 0.35 XEM
Built transaction:
{
  type: 257,
  version: 2,
  network: 152,
  timestamp: 353346165,
  signerPublicKey: '462EE976890916E54FA825D26BDD0235F5EB5B6A143C199AB0AE5EE9328E08CE',
  signature: 'C5BAAB71037317719398BA8643A7A3561AF0D69CEAC67757080E2625C4EAB3E0D7B960C850209CAD8E9C4EEA492FB75084AD9090DAE422624120D9C129642104',
  fee: '350000',
  deadline: 353353365,
  recipientAddress: '5442554C4541554732435A51495355523434324857413655414B47574958484441424A5649505334',
  amount: '1000000',
  mosaics: [
    {
      mosaic: {
        mosaicId: { namespaceId: { name: '636F6D70616E79' }, name: '746F6B656E' },
        amount: '100'
      }
    }
  ]
}
Announcing transaction to /transaction/announce
  Result: SUCCESS
Transaction hash: FAA9346884DF9AB9CEBD21D18242FEB24559F781F94148FD069165AECB751F65
Waiting for transaction confirmation from /transaction/get?hash=FAA9346884DF9AB9CEBD21D18242FEB24559F781F94148FD069165AECB751F65
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
  Transaction status: pending
transaction confirmed in block 649700

通常の XEM 転送と異なる部分に焦点を当てた、出力の要点は次のとおりです。

  • 定義と供給量(6 行目): 可分性(0)と現在の供給量(1000000)を持つ company:token モザイク。数量を原子単位へ変換し、手数料を計算するために取得します。
  • トランザクション手数料(16 行目): モザイクの供給量、可分性、転送量から導出した 350000 原子単位(0.35 XEM)。
  • 乗数としての amount(19 行目): モザイクを付加すると、amount は XEM の金額ではなく 1000000(係数 1)になります。
  • モザイク配列(20~27 行目): 転送に含まれるモザイク。ここでは数量 100 のエントリ 1 件です。 ネームスペースと名前はアドレスと同じように 16 進数でエンコードされるため、636F6D70616E79 と 746F6B656E は company と token にデコードされます。

ネットワーク側からトランザクションを確認するには、NEM テストネットエクスプローラー でトランザクションハッシュを検索できます。 ハッシュは Transaction hash: で始まる行に出力されます。

まとめ⚓︎

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

手順 関連ドキュメント
モザイクの可分性を取得する /mosaic/definition GET
モザイクの供給量を取得する /mosaic/supply GET
モザイク転送を構築する 、TransferTransactionV2
トランザクション手数料を計算する