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⚓︎

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

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

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

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.

Fetching Network Time⚓︎

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

Every NEM transaction contains two time fields, both expressed in network time, the number of seconds since the NEM nemesis block:

  • timestamp: The moment the transaction is created, set here to the current network time.
  • deadline: How long the network keeps trying to confirm the transaction before discarding it. It must be after the timestamp and no more than 24 hours later. Otherwise, the node rejects the transaction. This example sets it two hours after the timestamp, well within the limit.

Building a transfer therefore needs an accurate network time. The /time-sync/network-time GET endpoint reports the node's current network time. The node returns this value in milliseconds, so the code divides it by 1000 to obtain the seconds that transactions expect.

However, applications do not need to query the network time before every transaction. It can be fetched once and then adjusted using the local system clock when needed. This provides a good balance between accuracy and performance.

The code wraps the seconds value in the SDK's class to obtain the timestamp, and derives the deadline from it with the helper.

Building the Transaction⚓︎

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

The snippet calls with a descriptor that supplies the transfer transaction's properties:

  • : 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 signer is the account that will pay the fee. In a transfer transaction, it is also the source of the transferred XEM.

  • and : The values computed in the network time step.

  • : The address that will receive the XEM.

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

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

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

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⚓︎

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

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⚓︎

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

The snippet above repeatedly queries the /transaction/get GET endpoint using the hash of the announced transaction.

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

Some highlights from the output:

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

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

  • Recipient address (line 15): 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 16): 1000000 atomic units, equal to 1 XEM.

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

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

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

  • Confirmation (line 34): 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 says Waiting for confirmation from /transaction/get?hash=....

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Obtain the network time /time-sync/network-time GET,
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.