Skip to content

Creating Transactions Manually⚓︎

INTERMEDIATE

Most tutorials create transactions from descriptors using . This is the recommended approach: in languages that support typed descriptors, it provides type safety and better editor support, while also calculating the deadline and transaction fees for you.

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

The example mirrors the Transfer Transaction tutorial, but replaces descriptor-based transaction creation with manual field assignment, including explicit deadline and fee handling.

The remaining steps are briefly summarized. For full details, refer to the Transfer Transaction tutorial.

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.SymbolFacade import SymbolFacade
from symbolchain.sc import Amount
from symbolchain.symbol.FeeCalculator import calculate_transaction_fee
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id
from symbolchain.symbol.Network import NetworkTimestamp

NODE_URL = os.getenv('NODE_URL', 'https://reference.symboltest.net:3001')

print(f'Using node {NODE_URL}')


# Helper function to announce a transaction
def announce_transaction(payload, label):
    print(f'Announcing {label} to /transactions')
    request = urllib.request.Request(
        f'{NODE_URL}/transactions',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(request) as announce_response:
        print(f'  Response: {announce_response.read().decode()}')


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{tx_hash}'
            with urllib.request.urlopen(url) as confirm_response:
                status = json.loads(confirm_response.read().decode())
                print(f'  Transaction status: {status["group"]}')
                if status['group'] == 'confirmed':
                    print(f'{label} confirmed in {attempt} seconds')
                    return
                if status['group'] == 'failed':
                    raise RuntimeError(
                        f'{label} failed: {status["code"]}')
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise TimeoutError(f'{label} not confirmed after 60 seconds')


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

facade = SymbolFacade('testnet')

try:
    # Fetch current network time
    time_path = '/node/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())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')

    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')

    # Build the transaction
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'recipient_address':
            facade.network.public_key_to_address(
                signer_key_pair.public_key),
        'mosaics': [{
            'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
            'amount': 1_000_000  # 1 XYM
        }]
    })
    transaction.fee = Amount(
        calculate_transaction_fee(transaction, fee_multiplier))

    # 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_transaction(json_payload, 'transaction')

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

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

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    NetworkTimestamp,
    SymbolFacade,
    calculateTransactionFee,
    generateMosaicAliasId,
    models
} from 'symbol-sdk/symbol';

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log('Using node', NODE_URL);

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    console.log(`Announcing ${label} to /transactions`);
    const response = await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log('  Response:', await response.text());
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    console.log(`Waiting for ${label} confirmation...`);
    for (let attempt = 0; 60 > attempt; attempt++) {
        await new Promise(resolve => { setTimeout(resolve, 1000); });
        const response = await fetch(
            `${NODE_URL}/transactionStatus/${transactionHash}`);
        if (!response.ok) {
            if (404 === response.status) {
                console.log('  Transaction status: unknown');
                continue;
            }
            throw new Error(`HTTP ${response.status}`);
        }
        const status = await response.json();
        console.log('  Transaction status:', status.group);
        if ('confirmed' === status.group) {
            console.log(`${label} confirmed in`, attempt, 'seconds');
            return;
        }
        if ('failed' === status.group)
            throw new Error(`${label} failed: ${status.code}`);
    }
    throw new Error(`${label} not confirmed after 60 seconds`);
}


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

const facade = new SymbolFacade('testnet');

try {
    // Fetch current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');

    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);

    // Build the transaction
    const transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        recipientAddress: facade.network.publicKeyToAddress(
            signerKeyPair.publicKey).toString(),
        mosaics: [{
            mosaicId: generateMosaicAliasId('symbol.xym'),
            amount: 1_000_000n // 1 XYM
        }]
    });
    transaction.fee = new models.Amount(
        calculateTransactionFee(transaction, feeMultiplier));

    // 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
    await announceTransaction(jsonPayload, 'transaction');

    // Wait for confirmation
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    await waitForConfirmation(transactionHash, 'transaction');

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

Download source

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

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
import org.symbol.sdk.symbol.FeeCalculator;
import org.symbol.sdk.symbol.IdGenerator;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.NetworkTimestamp;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.models.Amount;
import org.symbol.sdk.symbol.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", "https://reference.symboltest.net:3001");

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

    private void announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to /transactions%n", label);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + "/transactions"))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        System.out.printf("  Response: %s%n", response.body());
    }

    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        for (int attempt = 0; 60 > attempt; ++attempt) {
            Thread.sleep(1000);
            final String statusPath =
                "/transactionStatus/" + transactionHash;
            final HttpRequest statusRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> statusResponse = HTTP_CLIENT
                .send(statusRequest, BodyHandlers.ofString());
            if (404 == statusResponse.statusCode()) {
                System.out.println("  Transaction status: unknown");
                continue;
            }
            if (2 != statusResponse.statusCode() / 100)
                throw new IOException(
                    "HTTP " + statusResponse.statusCode());

            final JsonNode status =
                JSON_MAPPER.readTree(statusResponse.body());
            final String group = status.get("group").asText();
            System.out.printf("  Transaction status: %s%n", group);
            if ("confirmed".equals(group)) {
                System.out.printf("%s confirmed in %d seconds%n",
                    label, attempt);
                return;
            }
            if ("failed".equals(group))
                throw new IOException(String.format("%s failed: %s",
                    label, status.get("code").asText()));
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", 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));

        // Fetch current network time
        final String timePath = "/node/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 NetworkTimestamp timestamp = new NetworkTimestamp(
            timeJSON.get("communicationTimestamps")
                .get("receiveTimestamp").asLong());
        System.out.printf("  Network time: %d ms since nemesis%n",
            timestamp.timestamp);

        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJSON = JSON_MAPPER.readTree(feeResponse.body());
        final long medianMultiplier =
            feeJSON.get("medianFeeMultiplier").asLong();
        final long minimumMultiplier =
            feeJSON.get("minFeeMultiplier").asLong();
        final long feeMultiplier = Math.max(
            medianMultiplier, minimumMultiplier);
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

        // Build the transaction
        final Transaction transaction = facade.transactionFactory.create(
            Map.of(
                "type", "transfer_transaction_v1",
                "signerPublicKey", signerKeyPair.getPublicKey().toString(),
                "deadline", timestamp.addHours(2).timestamp,
                "recipientAddress", facade.network.publicKeyToAddress(
                    signerKeyPair.getPublicKey()).toString(),
                "mosaics", List.of(Map.of(
                    "mosaicId", IdGenerator.generateMosaicAliasId(
                        "symbol.xym"),
                    "amount", 1_000_000L // 1 XYM
                ))
            ));
        transaction.setFee(new Amount(
            FeeCalculator.calculateTransactionFee(
                transaction, feeMultiplier)));

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

        // Announce the transaction
        announceTransaction(jsonPayload, "transaction");

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

Download source

Code Explanation⚓︎

Setting Up an Account⚓︎

SIGNER_PRIVATE_KEY = os.getenv('SIGNER_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
signer_key_pair = SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))
const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const signerKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(SIGNER_PRIVATE_KEY));
        final String signerPrivateKey = System.getenv().getOrDefault(
            "SIGNER_PRIVATE_KEY", "0".repeat(64));
        final KeyPair signerKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(signerPrivateKey));

The signer account is loaded from the SIGNER_PRIVATE_KEY environment variable. If not provided, a test key is used as default.

Fetching Network Time⚓︎

    # Fetch current network time
    time_path = '/node/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())
        receive_timestamp = (
            response_json['communicationTimestamps']['receiveTimestamp'])
        timestamp = NetworkTimestamp(int(receive_timestamp))
        print(f'  Network time: {timestamp.timestamp} ms since nemesis')
    // Fetch current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');
        // Fetch current network time
        final String timePath = "/node/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 NetworkTimestamp timestamp = new NetworkTimestamp(
            timeJSON.get("communicationTimestamps")
                .get("receiveTimestamp").asLong());
        System.out.printf("  Network time: %d ms since nemesis%n",
            timestamp.timestamp);

Manual transaction creation requires an absolute deadline expressed in network time. Network time is measured in milliseconds since the nemesis block.

When using descriptor-based transaction creation, the SDK accepts a deadline duration in seconds from now instead so fetching the current network time is not necessary.

The snippet fetches the current network time from /node/time GET and stores it so the transaction deadline can be set later. Applications do not need to query network time before every transaction: it can be fetched once and then adjusted using the local system clock.

Deadline checks

If a transaction's deadline is earlier than the current network time or too far in the future, the transaction is rejected.

    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')
    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);
        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJSON = JSON_MAPPER.readTree(feeResponse.body());
        final long medianMultiplier =
            feeJSON.get("medianFeeMultiplier").asLong();
        final long minimumMultiplier =
            feeJSON.get("minFeeMultiplier").asLong();
        final long feeMultiplier = Math.max(
            medianMultiplier, minimumMultiplier);
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

Transactions on Symbol must pay a fee to incentivize nodes to include them in blocks. The snippet fetches the recommended fee multiplier from /network/fees/transaction GET and stores it for use after the transaction is created.

Building the Transaction⚓︎

    # Build the transaction
    transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v1',
        'signer_public_key': signer_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'recipient_address':
            facade.network.public_key_to_address(
                signer_key_pair.public_key),
        'mosaics': [{
            'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
            'amount': 1_000_000  # 1 XYM
        }]
    })
    transaction.fee = Amount(
        calculate_transaction_fee(transaction, fee_multiplier))
    // Build the transaction
    const transaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v1',
        signerPublicKey: signerKeyPair.publicKey.toString(),
        deadline: timestamp.addHours(2).timestamp,
        recipientAddress: facade.network.publicKeyToAddress(
            signerKeyPair.publicKey).toString(),
        mosaics: [{
            mosaicId: generateMosaicAliasId('symbol.xym'),
            amount: 1_000_000n // 1 XYM
        }]
    });
    transaction.fee = new models.Amount(
        calculateTransactionFee(transaction, feeMultiplier));
        // Build the transaction
        final Transaction transaction = facade.transactionFactory.create(
            Map.of(
                "type", "transfer_transaction_v1",
                "signerPublicKey", signerKeyPair.getPublicKey().toString(),
                "deadline", timestamp.addHours(2).timestamp,
                "recipientAddress", facade.network.publicKeyToAddress(
                    signerKeyPair.getPublicKey()).toString(),
                "mosaics", List.of(Map.of(
                    "mosaicId", IdGenerator.generateMosaicAliasId(
                        "symbol.xym"),
                    "amount", 1_000_000L // 1 XYM
                ))
            ));
        transaction.setFee(new Amount(
            FeeCalculator.calculateTransactionFee(
                transaction, feeMultiplier)));

The transaction is created with , which accepts a plain descriptor object. Unlike , the manual factory does not fill in common transaction fields or calculate the fee.

The descriptor passed to contains:

  • : Use transfer_transaction_v1.
  • : The account that signs the transaction and pays the fee. In a transfer transaction, it is also the source of the transferred mosaics.
  • : The absolute deadline in network time.
  • : In this example, the recipient is the same as the sender.
  • : The mosaics to send. The example sends 1 XYM, expressed as 1_000_000 atomic units because XYM has divisibility 6.

After the transaction is created, its size is known. The final fee is calculated using , which multiplies that size by the recommended fee multiplier, and assigned to the transaction's fee field.

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 = SymbolTransactionFactory
            .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 then attached with , producing the JSON payload that can be announced to a node.

Announcing the Transaction⚓︎

    # Announce the transaction
    announce_transaction(json_payload, 'transaction')
    // Announce the transaction
    await announceTransaction(jsonPayload, 'transaction');
        // Announce the transaction
        announceTransaction(jsonPayload, "transaction");

The transaction is announced by sending the signed payload to /transactions PUT.

Waiting for Confirmation⚓︎

    # Wait for confirmation
    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    wait_for_confirmation(transaction_hash, 'transaction')
    // Wait for confirmation
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    await waitForConfirmation(transactionHash, 'transaction');
        // Wait for confirmation
        final String transactionHash =
            facade.hashTransaction(transaction).toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);
        waitForConfirmation(transactionHash, "transaction");

After announcement, the transaction status is monitored using /transactionStatus/{hash} GET until it is confirmed or fails.

Output⚓︎

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

Using node https://reference.symboltest.net:3001
Fetching current network time from /node/time
  Network time: 78235462065 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Built transaction:
{
  signature: '728D968E14F50EBB2496B560721E938629D6B4C1522B4A22DD659507B469C0EC5125485EBD38D48FBF351FF9DEC9CF3AFD7A5AFC5E945087E53173589B0B6B08',
  signerPublicKey: '87DA603E7BE5656C45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC8',
  version: 1,
  network: 152,
  type: 16724,
  fee: '17600',
  deadline: '78242662065',
  recipientAddress: '98F96BD2F803DE1EE39AACFC53A246F4F7A46901A5D0A53E',
  mosaics: [ { mosaicId: '16666583871264174062', amount: '1000000' } ],
  message: ''
}
Announcing transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Transaction hash: 260CD293E05C2853A967874BCF67FAB36FD331CE14925CA611B3877B99BB325D
Waiting for transaction confirmation...
  Transaction status: unknown
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
transaction confirmed in 5 seconds

Key points in the output:

  • Line 2: The code explicitly fetches the current network time.
  • Line 4: The code fetches the recommended fee multiplier.
  • Line 8 (signature): The signature is already attached before the transaction is printed.
  • Line 13 (fee): The fee was calculated after the transaction was created.
  • Line 14 (deadline): The deadline is an absolute network-time timestamp.
  • Line 19: The signed payload is announced to the network.
  • Line 21: The transaction hash can be used to look up the transaction in the Symbol Testnet Explorer.

Conclusion⚓︎

This tutorial showed how to create a transaction manually:

Step Related documentation
Fetch network time /node/time GET
Fetch recommended fees /network/fees/transaction GET
Build the transaction
Calculate the fee
Sign and serialize
Announce and confirm /transactions PUT
/transactionStatus/{hash} GET