Creating a Transfer Transaction⚓︎
Transfer transactions are the most basic type of Symbol transaction. They allow sending XYM or any other type of mosaic from one account to another.
This tutorial shows how to create, sign, and announce a transfer transaction, and then poll the transaction's status
until it is confirmed.
Required transaction parameters, such as the current time and fees, are fetched from the network to use the most
up-to-date values.
You should have completed the Hello World tutorial to understand how to run the tutorials.
Full Code⚓︎
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
|
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⚓︎
Fetching Network Time⚓︎
Transactions on Symbol must include a deadline, which defines how long the network should attempt to confirm the transaction before discarding it. Deadlines are expressed in absolute network time, so the first step is to fetch the current network time from a node.
What is the network time?
Symbol defines time as the number of seconds elapsed since the creation of its first block, known as the Nemesis block (or Genesis block, for the rest of blockchains).
All transaction deadlines and timestamps are calculated relative to this origin.
If you want to display timestamps in a more human-friendly way such as UTC, you need to add the timestamp of the Nemesis block, which you can retrieve from the network properties:
properties_path = '/network/properties'
print(f'Fetching network properties from {properties_path}')
with urllib.request.urlopen(f'{NODE_URL}{properties_path}') as response:
response_json = json.loads(response.read().decode())
epoch_adjustment = datetime.datetime.fromtimestamp(
int(response_json['network']['epochAdjustment'].rstrip('s')))
print(f' Nemesis timestamp: {epoch_adjustment}')
const propertiesPath = '/network/properties';
console.log('Fetching network properties from', propertiesPath);
const propertiesResponse = await fetch(`${NODE_URL}${propertiesPath}`);
const propertiesJSON = await propertiesResponse.json();
const epochAdjustment = new Date(parseInt(
propertiesJSON['network']['epochAdjustment']) * 1000);
console.log(' Nemesis timestamp:', epochAdjustment);
If a transaction's deadline is earlier than the current network time or more than two hours in the future,
the transaction will be rejected.
To avoid this, you need to know the current network time before constructing the transaction, using the
/node/time
endpoint.
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.
Fetching Recommended Fees⚓︎
Transactions on Symbol must pay a fee to incentivize nodes to include them in blocks. If the fee is too low, no node may include the transaction. If it is too high, the sender wastes funds. In addition, each node may enforce a minimum fee threshold for incoming transactions.
The optimal fee depends on the current state of the network,
particularly the number of transactions being submitted and the fees they are offering.
To support fee estimation, Symbol provides the
/network/fees/transaction
endpoint that returns a recommended fee multiplier based on recent transaction activity.
The final fee is calculated by multiplying the recommended multiplier by the transaction's size in bytes. This ensures that larger transactions pay proportionally more while smaller ones remain cost-effective.
Although applications can use a fixed fee for simplicity, it is more efficient to follow the network recommendation. As with network time, there is no need to query the multiplier for every transaction, but it should be refreshed regularly.
The snippet above takes the greater of the recommended multiplier (medianFeeMultiplier
) and the node's minimum
multiplier (minFeeMultiplier
), and stores it for later use once the transaction size is known.
Building the Transaction⚓︎
Use a
TransferTransactionV1Descriptor
to create this transaction in a type-safe way.
See the Typed Descriptors tutorial for details.
All required transaction properties must be provided when building the transfer transaction. The snippet includes the following fields:
-
Type: Transfer transactions use the type
transfer_transaction_v1
. -
Signer public key: The signer is the account that will pay the fee. In a transfer transaction, it is also the source of the transferred mosaics.
-
Deadline: This value is set to two hours after the current network time, which is the maximum allowed deadline.
-
Recipient address: In this example, the recipient is the same as the sender, which is useful for demonstration but not terribly practical.
-
Mosaics: This is an array, because a transfer transaction can send multiple mosaics at once. Each entry includes a mosaic ID and an absolute amount.
In the example, the mosaic ID for XYM is obtained using its alias,
symbol.xym
, which is easier to remember than the full hexadecimal ID.Absolute amounts depend on the mosaic's divisibility. For XYM, the divisibility is 6, so 1 XYM must be expressed as
1_000_000
.
Note that the fee field is not set in the descriptor. Instead, the fee is calculated after the transaction is built, using the previously obtained multiplier and the transaction's size in bytes.
Signing and Serializing⚓︎
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.
symbolchain.facade.SymbolFacade.SymbolFacade.sign_transaction
returns a signature encoded as a
hexadecimal string.
symbolchain.symbol.TransactionFactory.TransactionFactory.attach_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⚓︎
Announcing a transaction is a simple PUT
request to the
/transactions
endpoint
of any Symbol API node.
As long as the payload is correctly formed, the request will succeed with an HTTP 200 response.
However, this response does not indicate that the transaction is valid or accepted by the network. Validation, fee checks, and other rules are applied asynchronously after the transaction is received.
To confirm that the transaction is actually accepted and included in a block, its status must be monitored separately, as shown in the next step.
Waiting for Confirmation⚓︎
Note
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.
A production-grade application should use WebSockets to receive confirmation events directly from the node. This provides a simpler and more responsive solution without the overhead of repeated API calls.
In addition, the logic for checking transaction status is reusable. It can be moved into a utility function or module, since it is needed after announcing every transaction.
The snippet above repeatedly queries the
/transactionStatus
endpoint using the hash of the submitted transaction.
The response may take one of several forms:
- An HTTP error, indicating that the node has not yet started processing the transaction.
- A valid JSON object containing the transaction status.
If the status group is confirmed
, the transaction has been accepted and included in a block.
If the status group is failed
, the transaction has been rejected, for example, due to insufficient funds.
In any other case, the code waits one second and tries again, up to a maximum of 60 times.
Note that the code performs the first wait before performing the first status check. This gives the node some time to begin processing the transaction after it is announced.
Output⚓︎
The output shown below corresponds to a typical run of the program.
Using node https://001-sai-dual.symboltest.net:3001
Fetching current network time from /node/time
Network time: 78235462065n ms since nemesis
Fetching recommended fees from /network/fees/transaction
Fee multiplier: 100
Built transaction:
{
signature: '728D968E14F50EBB2496B560721E938629D6B4C1522B4A22DD659507B469C0EC5125485EBD38D48FBF351FF9DEC9CF3AFD7A5AFC5E945087E53173589B0B6B08',
signerPublicKey: '87DA603E7BE5656C45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC49FCBC8',
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"}
Waiting for confirmation from /transactionStatus/260CD293E05C2853A967874BCF67FAB36FD331CE14925CA611B3877B99BB325D
Transaction status: unknown | Cause: Not Found
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: confirmed
Transaction confirmed in 5 seconds
The number of status checks before confirmation can vary based on network conditions,
and the initial unknown
status may or may not appear,
depending on how quickly the node begins processing the transaction.
To see the transaction from the network's perspective, you can visit the
Symbol Testnet Explorer and search for the transaction hash.
The hash is printed in the line that says Waiting for confirmation from /transactionStatus/...
.
You should see the transaction move through the confirmation process in real time.
Alternatively, you can search for the signer_public_key
to view the transaction in the history of the signer account.
Conclusion⚓︎
This tutorial showed how to:
-
Create a transaction using the
TransactionFactory.create
method, and providing deadline and fee information obtained from the/node/time
and/network/fees/transaction
endpoints. -
Sign the transaction using the
SymbolFacade.sign_transaction
andTransactionFactory.attach_signature
methods. -
Announce the transaction using the
/transactions
endpoint. -
Confirm the transaction by polling the
/transactionStatus
endpoint.
Other transaction types follow the same general process.