コンテンツにスキップ

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

初級

ある アカウント から別のアカウントへ XEM を送信することは、NEM ブロックチェーンで最も基本的な操作です。他の種類の トランザクション も、同じ一般的なパターンに従います。

Transfer XEMAABBA->B1 XEM

このチュートリアルでは、2 つのアカウント間で 1 XEM を送信する 転送トランザクション を作成、署名、アナウンスし、承認されるまでトランザクションのステータスをポーリングする方法を説明します。

前提条件⚓︎

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

完全なコード⚓︎

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
from symbolchain.nem.Network import NetworkTimestamp

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

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

facade = NemFacade('testnet')

# Define the amount of XEM to transfer
xem = float(os.getenv('XEM_AMOUNT', '1'))
amount = round(xem * 1_000_000)


try:
    # Fetch current network time
    time_path = '/time-sync/network-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())
        network_time = response_json['receiveTimeStamp'] // 1000
        print(f'  Network time: {network_time} s since the nemesis block')

    # Derived fields from network time
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)

    # Build the transaction
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v2',
        'signer_public_key': signer_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': RECIPIENT_ADDRESS,
        'amount': amount
    })

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    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_path = '/transaction/announce'
    print(f'Announcing transaction to {announce_path}')
    announce_request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=json_payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    with urllib.request.urlopen(announce_request) as response:
        announce_result = json.loads(response.read().decode())
    print(f'  Result: {announce_result['message']}')

    # Wait for confirmation
    if 'SUCCESS' == announce_result['message']:
        status_path = (
            f'/transaction/get?hash={
                facade.hash_transaction(transaction)}')
        print(f'Waiting for confirmation from {status_path}')
        is_confirmed = False
        for attempt in range(120):
            try:
                with urllib.request.urlopen(
                    f'{NODE_URL}{status_path}'
                ) as response:
                    confirmed = json.loads(response.read().decode())
                    height = confirmed['meta']['height']
                    print(f'Transaction confirmed in block {height}')
                    is_confirmed = True
                    break
            except urllib.error.HTTPError:
                print('  Transaction status: pending')
            time.sleep(1)
        if not is_confirmed:
            print('Confirmation took too long.')
    else:
        print(f'Transaction rejected: {announce_result['message']}')

except urllib.error.URLError as e:
    print(e.reason)

Download source

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

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

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 facade = new NemFacade('testnet');

// Define the amount of XEM to transfer
const xem = parseFloat(process.env.XEM_AMOUNT || '1');
const amount = BigInt(Math.round(xem * 1_000_000));


try {
    // Fetch current network time
    const timePath = '/time-sync/network-time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
    console.log('  Network time:', networkTime,
        's since the nemesis block');

    // Derived fields from network time
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);

    // Build the transaction
    const transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v2',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        recipientAddress: RECIPIENT_ADDRESS,
        amount
    });

    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    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 });

    // Announce the transaction
    const announcePath = '/transaction/announce';
    console.log('Announcing transaction to', announcePath);
    const announceResponse = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    const announceResult = await announceResponse.json();
    console.log('  Result:', announceResult.message);

    // Wait for confirmation
    if ('SUCCESS' === announceResult.message) {
        const transactionHash = facade.hashTransaction(transaction)
            .toString();
        const statusPath = `/transaction/get?hash=${transactionHash}`;
        console.log('Waiting for confirmation from', statusPath);

        let isConfirmed = false;
        for (let attempt = 1; 120 >= attempt; ++attempt) {
            const response = await fetch(`${NODE_URL}${statusPath}`);

            if (response.ok) {
                const confirmed = await response.json();
                console.log('Transaction confirmed in block',
                    confirmed.meta.height);
                isConfirmed = true;
                break;
            }
            console.log('  Transaction status: pending');
            await new Promise(resolve => { setTimeout(resolve, 1000); });
        }
        if (!isConfirmed)
            console.warn('Confirmation took too long.');
    } else {
        console.log('Transaction rejected:', announceResult.message);
    }

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

Download source

コード全体を 1 つの try ブロックでラップして簡単なエラー処理を行いますが、アプリケーションではより細かな制御が必要になる場合があります。

コードの説明⚓︎

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

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

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

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

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

送金額を定義する⚓︎

# Define the amount of XEM to transfer
xem = float(os.getenv('XEM_AMOUNT', '1'))
amount = round(xem * 1_000_000)
// Define the amount of XEM to transfer
const xem = parseFloat(process.env.XEM_AMOUNT || '1');
const amount = BigInt(Math.round(xem * 1_000_000));

スニペットでは、XEM_AMOUNT 環境変数から数値として読み込んだ送金額を、xem 変数に定義します。 指定されていない場合は、デフォルト値として 1 XEM を使います。

トランザクションの amount フィールドには、XEM 単位ではなく 原子単位 が必要です。 XEM の 可分性 は 6 なので、1 XEM は 100 万原子単位です。 スニペットでは xem に 1'000'000 を掛けて amount を導出します。

ネットワーク時刻を取得する⚓︎

    # Fetch current network time
    time_path = '/time-sync/network-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())
        network_time = response_json['receiveTimeStamp'] // 1000
        print(f'  Network time: {network_time} s since the nemesis block')

    # Derived fields from network time
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)
    // Fetch current network time
    const timePath = '/time-sync/network-time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
    console.log('  Network time:', networkTime,
        's since the nemesis block');

    // Derived fields from network time
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);

NEM のすべてのトランザクションには 2 つの時刻フィールドがあり、どちらも ネットワーク時刻 で表します。これは NEM のネメシスブロックからの経過秒数です。

  • timestamp: トランザクションが作成された時点。ここでは現在のネットワーク時刻を設定します。
  • deadline: トランザクションを破棄する前に、ネットワークが承認を試み続ける期間。 タイムスタンプより後の時間で、24 時間 以内でなければなりません。 範囲外の時間を指定した場合、ノードはトランザクションを拒否します。 この例では、範囲内であるタイムスタンプの 2 時間後に設定します。

送金のトランザクションを構築するには正確なネットワーク時刻が必要です。 /time-sync/network-time GET エンドポイントは、ノードの現在のネットワーク時刻を返します。 ノードはこの値をミリ秒で返すため、コードでは 1000 で割って、トランザクションが必要とする秒数を取得します。

ただし、アプリケーションがトランザクションごとにネットワーク時刻を照会する必要はありません。 一度取得した後、必要に応じてローカルシステムの時計を使って調整できます。 これにより、正確さと性能のバランスが取れます。

コードは秒数を SDK の クラスでラップして timestamp を取得し、 ヘルパーで deadline を導出します。

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

    # Build the transaction
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v2',
        'signer_public_key': signer_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': RECIPIENT_ADDRESS,
        'amount': amount
    })
    // Build the transaction
    const transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v2',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        recipientAddress: RECIPIENT_ADDRESS,
        amount
    });

スニペットは、転送トランザクションのプロパティを指定するディスクリプタを使って を呼び出します。

  • : このチュートリアルでは、現在の送金バージョンである TransferTransactionV2 を使用します。XEM と他の モザイク の両方を送信できます。 ここではモザイクを付加しないため、トランザクションは XEM だけを送信します。

  • : 署名者は手数料を支払うアカウントです。 転送トランザクションでは、送られる XEM の送信元でもあります。

  • : ネットワーク時刻の手順で計算した値。

  • : XEM を受け取るアドレス。

  • : 前の手順で計算した原子単位の数量。1 XEM の場合は 1_000_000 です。

モザイクまたはメッセージを送信する

TransferTransactionV2 は、XEM の代わりに他の モザイク を送信したり、メッセージを含めたりできます。それぞれの場合で手数料の計算方法は異なります。 詳細については、モザイクを送信するメッセージ付きで送信する チュートリアルを参照してください。

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

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')
    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log(`  Transaction fee: ${Number(fee) / 1_000_000} XEM`);

すべてのトランザクションは、ブロックに含める ハーベスターアカウント に手数料を支払います。

NEM の 固定手数料表 を手動で実装する代わりに、スニペットは SDK の ヘルパーを呼び出します。このヘルパーは、前の手順で構築したトランザクションから XEM の金額を直接読み取ります。

返された手数料は、署名前に transaction.fee へ割り当てます。 手数料は少額の場合 0.05 XEM から始まり、送信する XEM に応じて増加し、最大 1.25 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))
    // 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 });

トランザクションを作成したら、署名アカウントの秘密鍵で署名する必要があります。 署名により、トランザクションが本物であり、送信者によって承認されたことを保証します。

署名 を返します。 は署名をトランザクションに追加し、ノードへ直接送信してアナウンスできる JSON ペイロードにシリアライズします。

トランザクションをアナウンスする⚓︎

    # Announce the transaction
    announce_path = '/transaction/announce'
    print(f'Announcing transaction to {announce_path}')
    announce_request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=json_payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    with urllib.request.urlopen(announce_request) as response:
        announce_result = json.loads(response.read().decode())
    print(f'  Result: {announce_result['message']}')
    // Announce the transaction
    const announcePath = '/transaction/announce';
    console.log('Announcing transaction to', announcePath);
    const announceResponse = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: jsonPayload
    });
    const announceResult = await announceResponse.json();
    console.log('  Result:', announceResult.message);

署名済みペイロードは、任意の NEM ノード/transaction/announce POST エンドポイントに送信します。

ノードはアナウンスされるとすぐにトランザクションを検証し、結果をレスポンスで返します。 SUCCESS はトランザクションが最初のチェックに合格し、未承認トランザクションプール に追加されたことを意味します。 それ以外の結果はノードが受け付けなかったことを意味し、例えばアカウントが金額と手数料をカバーできる XEM を保有していないなど、その理由をレスポンスメッセージで説明します。

未承認トランザクションを信頼しないでください

SUCCESS の結果は、トランザクションが未承認プールに到達したことだけを意味します。 ブロックに含まれることはまだ保証されていません。 承認 されるまで、できれば 書き換え制限 を過ぎるまで待ってから信頼してください。

承認を待つ⚓︎

    # Wait for confirmation
    if 'SUCCESS' == announce_result['message']:
        status_path = (
            f'/transaction/get?hash={
                facade.hash_transaction(transaction)}')
        print(f'Waiting for confirmation from {status_path}')
        is_confirmed = False
        for attempt in range(120):
            try:
                with urllib.request.urlopen(
                    f'{NODE_URL}{status_path}'
                ) as response:
                    confirmed = json.loads(response.read().decode())
                    height = confirmed['meta']['height']
                    print(f'Transaction confirmed in block {height}')
                    is_confirmed = True
                    break
            except urllib.error.HTTPError:
                print('  Transaction status: pending')
            time.sleep(1)
        if not is_confirmed:
            print('Confirmation took too long.')
    else:
        print(f'Transaction rejected: {announce_result['message']}')
    // Wait for confirmation
    if ('SUCCESS' === announceResult.message) {
        const transactionHash = facade.hashTransaction(transaction)
            .toString();
        const statusPath = `/transaction/get?hash=${transactionHash}`;
        console.log('Waiting for confirmation from', statusPath);

        let isConfirmed = false;
        for (let attempt = 1; 120 >= attempt; ++attempt) {
            const response = await fetch(`${NODE_URL}${statusPath}`);

            if (response.ok) {
                const confirmed = await response.json();
                console.log('Transaction confirmed in block',
                    confirmed.meta.height);
                isConfirmed = true;
                break;
            }
            console.log('  Transaction status: pending');
            await new Promise(resolve => { setTimeout(resolve, 1000); });
        }
        if (!isConfirmed)
            console.warn('Confirmation took too long.');
    } else {
        console.log('Transaction rejected:', announceResult.message);
    }

上記のスニペットは、アナウンスしたトランザクションのハッシュを使って /transaction/get GET エンドポイントを繰り返し照会します。

ポーリングと WebSocket

この手順ではポーリングでトランザクションが承認されたか確認します。 ポーリングは説明のために使用していますが、実際のアプリケーションに推奨される方法ではありません。

WebSocket を使えば、API を繰り返し呼び出すオーバーヘッドなしに、より応答性の高い解決策を実現できます。

トランザクションが未承認の間、エンドポイントはエラーを返します。コードは 1 秒待ってから再試行し、最大 120 回(約 2 分)繰り返します。

トランザクションがブロックに含まれると、エンドポイントはブロックの高さとともにトランザクションを返し、ループは終了します。

NEM はおよそ 1 分に 1 ブロックを生成するため、通常、承認には数秒から数分かかります。

出力⚓︎

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

Using node http://libertalia.nemtest.net:7890
Fetching current network time from /time-sync/network-time
  Network time: 351947283 s since the nemesis block
  Transaction fee: 0.05 XEM
Built transaction:
{
  type: 257,
  version: 2,
  network: 152,
  timestamp: 351947283,
  signerPublicKey: '462EE976890916E54FA825D26BDD0235F5EB5B6A143C199AB0AE5EE9328E08CE',
  signature: '17FF7D37A63F3CFC43C790300A7B7F1C8A2A2B1C5D64546007C7D02C771516EE8872A6BBAE414486DC2D4750BBAACB41B3140D45CC319F51B7CBC1D524545C06',
  fee: '50000',
  deadline: 351954483,
  recipientAddress: '5442554C4541554732435A51495355523434324857413655414B47574958484441424A5649505334',
  amount: '1000000',
  mosaics: []
}
Announcing transaction to /transaction/announce
  Result: SUCCESS
Waiting for confirmation from /transaction/get?hash=436A43BDD3CE1F0A5A5EFC40A9027D3732D59AB3C507818A1B436EC7B32BF099
  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 status: pending
  Transaction status: pending
Transaction confirmed in block 626588

出力の要点は次のとおりです。

  • 署名者の公開鍵(11 行目): トランザクションに署名して XEM を送るアカウント。
  • トランザクション手数料(13 行目): 50000 原子単位(0.05 XEM)。デフォルトの 1 XEM を送る手数料です。
  • 受取人アドレス(15 行目): XEM を受け取るアカウント。 これは同じ RECIPIENT_ADDRESS ですが、NEM のトランザクション形式では Base32 テキストの各文字を 16 進数の ASCII コードとしてエンコードするため、見た目が異なります。そのため 5442...TBUL... にデコードされます(54T42B など)。
  • 転送額(16 行目): 1000000 原子単位で、1 XEM に相当します。
  • モザイクなし(17 行目): モザイク配列が空なので、トランザクションは XEM だけを送信します。
  • アナウンス結果(20 行目): SUCCESS はノードがトランザクションを未承認プールに受け入れたことを意味します。
  • トランザクションハッシュ(21 行目): ネットワーク上でトランザクションを一意に識別するハッシュ。
  • 承認(34 行目): トランザクションがブロック 626588 に含まれています。

pending のチェック回数は、次のブロックがハーベスティングされるまでの時間によって変わるため、実行ごとに異なります。

ネットワーク側からトランザクションを確認するには、NEM テストネットエクスプローラー でトランザクションハッシュを検索できます。 ハッシュは Waiting for confirmation from /transaction/get?hash=... と表示される行に出力されます。

まとめ⚓︎

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

手順 関連ドキュメント
ネットワーク時刻を取得する /time-sync/network-time GET
トランザクションを構築する TransferTransactionV2
トランザクション手数料を計算する
トランザクションに署名する
トランザクションをアナウンスする /transaction/announce POST
承認を待つ /transaction/get GET

他のほとんどの NEM トランザクションタイプも、同じ方法で作成、署名、アナウンスします。