Skip to content

Sending XEM with a Transfer Transaction⚓︎

BEGINNER

Sending XEM from one account to another is the most basic action on the NEM blockchain, and every other type of transaction follows the same general pattern.

Transfer XEMAABBA->B1 XEM

This tutorial shows how to create, sign, and announce a transfer transaction that sends 1 XEM between two accounts, and then poll the transaction's status until it is confirmed.

Prerequisites⚓︎

Before you start, make sure to:

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

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


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



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


try:
    # Build the transaction
    transaction = facade.create_transaction_from_descriptor({
        'type': 'transfer_transaction_v2',
        'recipient_address': RECIPIENT_ADDRESS,
        'amount': amount
    }, signer_key_pair.public_key, 0, 2 * 60 * 60)

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

    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    announce_result = announce_transaction(json_payload, 'transaction')
    if 'SUCCESS' == announce_result:
        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);

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

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


// 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 {
    // Build the transaction
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV2Descriptor(
            new Address(RECIPIENT_ADDRESS),
            new models.Amount(amount)),
        signerKeyPair.publicKey,
        0n,
        2 * 60 * 60);

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

    const transactionHash = facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    const announceResult = await announceTransaction(
        jsonPayload, 'transaction');
    if ('SUCCESS' === announceResult)
        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 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 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.KeyPair;
import org.symbol.sdk.nem.NemTransactionFactory;
import org.symbol.sdk.nem.descriptors.TransferTransactionV2Descriptor;
import org.symbol.sdk.nem.models.Amount;
import org.symbol.sdk.nem.models.Transaction;

public final class TransferXem {
    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");

    // Helper method to announce a transaction
    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;
    }


    // Helper method to wait for transaction confirmation
    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 TransferXem().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");


        // Define the amount of XEM to transfer
        final double xem = Double.parseDouble(
            System.getenv().getOrDefault("XEM_AMOUNT", "1"));
        final long amount = Math.round(xem * 1_000_000);


        // Build the transaction
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV2Descriptor(
                    new Address(recipientAddress),
                    new Amount(amount),
                    null,
                    List.of()),
                signerKeyPair.getPublicKey(),
                0,
                2 * 60 * 60);

        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction);
        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()));

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

Download source

The whole code is wrapped in a single try block to provide simple error handling, but applications will probably want to use more fine-grained control.

Code Explanation⚓︎

Setting Up the Accounts⚓︎

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

Every transfer transaction involves two accounts: a sender and a recipient.

The sender is the account that signs the transaction and pays the fee. Its private key is loaded from the SIGNER_PRIVATE_KEY environment variable. If not provided, a test key is used as default.

The recipient is the account that receives the XEM. Its address is loaded from the RECIPIENT_ADDRESS environment variable. If not provided, a test address is used as default.

Defining the Transfer Amount⚓︎

# 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));
        // Define the amount of XEM to transfer
        final double xem = Double.parseDouble(
            System.getenv().getOrDefault("XEM_AMOUNT", "1"));
        final long amount = Math.round(xem * 1_000_000);

The snippet defines the transfer amount in the xem variable, loaded as a number from the XEM_AMOUNT environment variable. If not provided, a default of 1 XEM is used.

The transaction's amount field requires atomic units, not whole XEM. XEM has a divisibility of 6, so one XEM equals one million atomic units. The snippet derives amount by multiplying xem by 1'000'000.

Building the Transaction⚓︎

    # Build the transaction
    transaction = facade.create_transaction_from_descriptor({
        'type': 'transfer_transaction_v2',
        'recipient_address': RECIPIENT_ADDRESS,
        'amount': amount
    }, signer_key_pair.public_key, 0, 2 * 60 * 60)
    // Build the transaction
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.TransferTransactionV2Descriptor(
            new Address(RECIPIENT_ADDRESS),
            new models.Amount(amount)),
        signerKeyPair.publicKey,
        0n,
        2 * 60 * 60);
        // Build the transaction
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new TransferTransactionV2Descriptor(
                    new Address(recipientAddress),
                    new Amount(amount),
                    null,
                    List.of()),
                signerKeyPair.getPublicKey(),
                0,
                2 * 60 * 60);

The snippet calls the facade's creation method with the transaction's descriptor and the values common to every transaction: the signer's public key, an initial fee of zero, and a deadline duration of two hours. The SDK fills the signer, sets the timestamp from the local clock, and derives the deadline from that duration.

Network time

Every NEM transaction contains a timestamp and deadline, both expressed as the number of seconds since the NEM nemesis block. The deadline must be after the timestamp and no more than 24 hours later.

The facade uses the local system clock for these values. Applications that need to compare it with a node's clock can query /time-sync/network-time GET, but do not need to fetch network time before creating every transaction.

The descriptor supplies the transfer-specific properties, in this order:

  • Transaction type: This tutorial uses TransferTransactionV2, the current transfer version, which can carry both XEM and other mosaics. No mosaics are attached here, so the transaction sends XEM only.

  • : The address that will receive the XEM.

  • : The atomic-unit value computed earlier. For 1 XEM, this is 1_000_000.

The signer passed separately to pays the fee and supplies the transferred XEM.

Sending a mosaic or a message

A TransferTransactionV2 can also carry other mosaics instead of XEM, or include a message, with the fee calculated differently in each case. See the Transfer Mosaics and Transfer with a Message tutorials.

Calculating the Transaction Fee⚓︎

    # 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`);
        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction);
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) fee / 1_000_000);

Every transaction pays a fee to the harvester account that includes it in a block.

Rather than implement NEM's fixed fee schedule by hand, the snippet calls the SDK's helper, which reads the XEM amount directly from the transaction built in the previous step.

The returned fee is assigned to transaction.fee before signing. The fee starts at 0.05 XEM for small amounts and grows with the XEM sent, up to a cap of 1.25 XEM.

Signing and Serializing⚓︎

    # 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 });
        // 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()));

Once the transaction is created, it must be signed with the signing account's private key. Signing ensures the transaction is authentic and authorized by the sender.

returns a signature. adds the signature to the transaction and serializes it into a JSON payload ready to be submitted directly to a node for announcement.

Announcing the Transaction⚓︎

# 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 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 method to announce a transaction
    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;
    }

The helper receives the JSON payload and a human-readable label. The label is only used in log messages, making the same helper reusable in tutorials that announce several transactions.

The signed payload is submitted to the /transaction/announce POST endpoint of any NEM node.

The node validates the transaction as soon as it is announced and reports the outcome in the response. A result of SUCCESS means the transaction passed this first check and was added to the unconfirmed pool. Any other result means the node did not accept it, and the response message explains why, for example that the account does not hold enough XEM to cover the amount and the fee.

Do not rely on unconfirmed transactions

A SUCCESS result only means the transaction reached the unconfirmed pool. It is not yet guaranteed to be included in a block. Wait until it is confirmed, and ideally past the rewrite limit, before relying on it.

Waiting for Confirmation⚓︎

# 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.')
// 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.`);
}
    // Helper method to wait for transaction confirmation
    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);
    }

The snippet above repeatedly queries the /transaction/get GET endpoint using the hash of the announced transaction. Like the announcement helper, it receives a label so its output remains clear when a tutorial announces several transactions.

Polling vs WebSockets

This step uses polling to check whether the transaction has been confirmed. Polling is used here for illustration purposes, but it is not the recommended approach for real applications.

WebSockets provide a more responsive solution without the overhead of repeated API calls.

While the transaction is still unconfirmed, the endpoint responds with an HTTP error, and the code waits one second before retrying, for up to 120 attempts (about two minutes).

Once the transaction is included in a block, the endpoint returns it together with the block height, and the loop ends.

NEM produces a block roughly once per minute, so confirmation usually takes from a few seconds to a couple of minutes.

Output⚓︎

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

Using node http://libertalia.nemtest.net:7890
  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: []
}
Transaction hash: 436A43BDD3CE1F0A5A5EFC40A9027D3732D59AB3C507818A1B436EC7B32BF099
Announcing transaction to /transaction/announce
  Result: SUCCESS
Waiting for transaction 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

Some highlights from the output:

  • Signer public key (line 9): The account that signs the transaction and sends the XEM.

  • Transaction fee (line 11): 50000 atomic units (0.05 XEM), the fee for sending the default amount of 1 XEM.

  • Recipient address (line 13): The account that receives the XEM. This is the same RECIPIENT_ADDRESS, but it looks different because NEM's transaction format encodes each character of its Base32 text as an ASCII code in hexadecimal, so 5442... decodes back to TBUL... (54 is T, 42 is B, and so on).

  • Transfer amount (line 14): 1000000 atomic units, equal to 1 XEM.

  • No mosaics (line 15): An empty mosaics array means the transaction sends XEM only.

  • Transaction hash (line 17): The hash that uniquely identifies the transaction on the network.

  • Announcement result (line 19): A result of SUCCESS means the node accepted the transaction into the unconfirmed pool.

  • Confirmation (line 33): The transaction is included in block 626588.

The number of pending checks depends on how soon the next block is harvested, so it varies between runs.

To see the transaction from the network's perspective, you can search for the transaction hash on the NEM testnet explorer. The hash is printed in the line that starts with Transaction hash:.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Build the transaction , TransferTransactionV2
Calculate the transaction fee
Sign the transaction
Announce the transaction /transaction/announce POST
Wait for confirmation /transaction/get GET

Most other NEM transaction types are created, signed, and announced in the same way.