Skip to content

Creating Transactions Manually⚓︎

INTERMEDIATE

Most tutorials create transactions from descriptors through the facade. This is the recommended approach: typed descriptors provide compile-time or type-checking support and better editor assistance in languages that support them, and the facade fills the signer, timestamp, and deadline from a duration.

For completeness, this tutorial shows the lower-level alternative: creating a transaction manually with the transaction factory.

The example mirrors the Transfer XEM tutorial, but replaces descriptor-based creation with manual field assignment. It explicitly fetches network time, assigns the timestamp and absolute deadline, and calculates the NEM transaction fee.

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

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

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

// 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 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 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 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.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.FeeCalculator;
import org.symbol.sdk.nem.KeyPair;
import org.symbol.sdk.nem.NemTransactionFactory;
import org.symbol.sdk.nem.NetworkTimestamp;
import org.symbol.sdk.nem.models.Amount;
import org.symbol.sdk.nem.models.Transaction;

public final class ManualTransactionCreation {
    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 ManualTransactionCreation().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);

        // Fetch current network time
        final String timePath = "/time-sync/network-time";
        System.out.printf("Fetching current network time from %s%n",
            timePath);
        final HttpRequest timeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + timePath)).GET().build();
        final HttpResponse<String> timeResponse = HTTP_CLIENT.send(
            timeRequest, BodyHandlers.ofString());
        final JsonNode timeJson = JSON_MAPPER.readTree(
            timeResponse.body());
        final long networkTime =
            timeJson.get("receiveTimeStamp").asLong() / 1000;
        System.out.printf(
            "  Network time: %d s since the nemesis block%n", networkTime);
        final NetworkTimestamp timestamp = new NetworkTimestamp(
            networkTime);
        final long deadline = timestamp.addHours(2).timestamp;

        // Build the transaction
        final Transaction transaction = facade.transactionFactory.create(
            Map.of(
                "type", "transfer_transaction_v2",
                "signerPublicKey",
                    signerKeyPair.getPublicKey().toString(),
                "timestamp", timestamp.timestamp,
                "deadline", deadline,
                "recipientAddress", recipientAddress,
                "amount", amount));

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

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

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

The signer and recipient are configured in the same way as in the Transfer XEM tutorial.

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 amount is expressed in atomic units. XEM has a divisibility of 6, so the default transfer of 1 XEM is 1_000_000 atomic units.

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);
        // Fetch current network time
        final String timePath = "/time-sync/network-time";
        System.out.printf("Fetching current network time from %s%n",
            timePath);
        final HttpRequest timeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + timePath)).GET().build();
        final HttpResponse<String> timeResponse = HTTP_CLIENT.send(
            timeRequest, BodyHandlers.ofString());
        final JsonNode timeJson = JSON_MAPPER.readTree(
            timeResponse.body());
        final long networkTime =
            timeJson.get("receiveTimeStamp").asLong() / 1000;
        System.out.printf(
            "  Network time: %d s since the nemesis block%n", networkTime);
        final NetworkTimestamp timestamp = new NetworkTimestamp(
            networkTime);
        final long deadline = timestamp.addHours(2).timestamp;

Manual transaction creation requires an explicit timestamp and an absolute deadline, both expressed as the number of seconds since the NEM nemesis block.

The snippet fetches the current network time from /time-sync/network-time GET. The endpoint returns milliseconds, so the value is divided by 1000 before it is wrapped in . The deadline is set two hours after the timestamp.

Descriptor-based facade creation instead accepts a deadline duration in seconds and derives both values using the local clock, so it does not require this request.

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
    });
        // Build the transaction
        final Transaction transaction = facade.transactionFactory.create(
            Map.of(
                "type", "transfer_transaction_v2",
                "signerPublicKey",
                    signerKeyPair.getPublicKey().toString(),
                "timestamp", timestamp.timestamp,
                "deadline", deadline,
                "recipientAddress", recipientAddress,
                "amount", amount));

The transaction is created with , which accepts a plain descriptor dictionary or object. Unlike the facade creation method, this lower-level factory does not fill common transaction fields.

The descriptor contains:

  • : TransferTransactionV2, the current transfer transaction version.
  • : The account that signs the transaction, pays the fee, and sends the XEM.
  • : The current network-time timestamp fetched in the previous step.
  • : The absolute network-time deadline, two hours after the timestamp.
  • : The address that receives the XEM.
  • : The amount to transfer in atomic units.

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

The low-level factory does not calculate NEM's fixed-schedule fee. The snippet calls after construction and assigns the result to transaction.fee before signing.

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

The transaction is signed with . The signature is attached with , producing a JSON payload ready for announcement.

Announcing the Transaction⚓︎

    # Announce the transaction
    announce_result = announce_transaction(json_payload, 'transaction')
    // Announce the transaction
    const announceResult = await announceTransaction(
        jsonPayload, 'transaction');
        // Announce the transaction
        final String announceResult = announceTransaction(
            jsonPayload, "transaction");

The signed payload is submitted to /transaction/announce POST. A SUCCESS response means the transaction entered the unconfirmed pool.

Waiting for Confirmation⚓︎

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

The transaction is polled through /transaction/get GET until it is included in a block or the two-minute retry limit is reached.

Output⚓︎

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
Transaction hash: 436A43BDD3CE1F0A5A5EFC40A9027D3732D59AB3C507818A1B436EC7B32BF099
Waiting for transaction confirmation from /transaction/get?hash=436A43BDD3CE1F0A5A5EFC40A9027D3732D59AB3C507818A1B436EC7B32BF099
  Transaction status: pending
  Transaction status: pending
transaction confirmed in block 626588

The first two highlighted lines show the explicit network-time request. The output also shows the calculated fee, the manually assigned signer and absolute deadline, the announcement result, and the transaction hash used for confirmation.

Conclusion⚓︎

This tutorial showed how to create a transaction manually:

Step Related documentation
Fetch network time /time-sync/network-time GET,
Build the transaction , TransferTransactionV2
Calculate the fee
Sign and serialize
Announce and confirm /transaction/announce POST
/transaction/get GET