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, optionally including a message.
This tutorial shows how to create, sign, and announce a transfer transaction, and then poll the transaction's status
until it is confirmed.
The recommended fee multiplier is fetched from the network so the SDK can calculate an appropriate transaction fee.
This tutorial is used to explain the basic concepts of transaction creation and announcement.
The rest of tutorials refer to this one to explain these basic techniques.
Alternative Manual Transaction Creation
This tutorial creates transactions from descriptors, which is the most convenient and type-safe method.
For an alternative, lower-level, manual method, see the
Manual Transaction Creation tutorial.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.symbol.IdGeneratorimportgenerate_mosaic_alias_idNODE_URL=os.getenv('NODE_URL','https://reference.symboltest.net:3001')print(f'Using node {NODE_URL}')SIGNER_PRIVATE_KEY=os.getenv('SIGNER_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000000')signer_key_pair=SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))facade=SymbolFacade('testnet')# Helper function to announce a transactiondefannounce_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')withurllib.request.urlopen(request)asannounce_response:print(f' Response: {announce_response.read().decode()}')# Helper function to wait for transaction confirmationdefwait_for_confirmation(tx_hash,label):print(f'Waiting for {label} confirmation...')forattemptinrange(60):time.sleep(1)try:url=f'{NODE_URL}/transactionStatus/{tx_hash}'withurllib.request.urlopen(url)asconfirm_response:status=json.loads(confirm_response.read().decode())print(f' Transaction status: {status["group"]}')ifstatus['group']=='confirmed':print(f'{label} confirmed in {attempt} seconds')returnifstatus['group']=='failed':raiseRuntimeError(f'{label} failed: {status["code"]}')excepturllib.error.HTTPError:print(' Transaction status: unknown')raiseTimeoutError(f'{label} not confirmed after 60 seconds')try:# Fetch recommended feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse: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 transactiontransaction=facade.create_transaction_from_descriptor({'type':'transfer_transaction_v1','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}]},signer_key_pair.public_key,fee_multiplier,2*60*60)# Sign transaction and generate final payloadsignature=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_transaction(json_payload,'transaction')wait_for_confirmation(transaction_hash,'transaction')excepturllib.error.URLErrorase:print(e.reason)
import{PrivateKey}from'symbol-sdk';import{SymbolFacade,descriptors,generateMosaicAliasId,models}from'symbol-sdk/symbol';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);constSIGNER_PRIVATE_KEY=process.env.SIGNER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constsignerKeyPair=newSymbolFacade.KeyPair(newPrivateKey(SIGNER_PRIVATE_KEY));constfacade=newSymbolFacade('testnet');// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){console.log(`Announcing ${label} to /transactions`);constresponse=awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:payload});console.log(' Response:',awaitresponse.text());}// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){console.log(`Waiting for ${label} confirmation...`);for(letattempt=0;60>attempt;attempt++){awaitnewPromise(resolve=>{setTimeout(resolve,1000);});constresponse=awaitfetch(`${NODE_URL}/transactionStatus/${transactionHash}`);if(!response.ok){if(404===response.status){console.log(' Transaction status: unknown');continue;}thrownewError(`HTTP ${response.status}`);}conststatus=awaitresponse.json();console.log(' Transaction status:',status.group);if('confirmed'===status.group){console.log(`${label} confirmed in`,attempt,'seconds');return;}if('failed'===status.group)thrownewError(`${label} failed: ${status.code}`);}thrownewError(`${label} not confirmed after 60 seconds`);}try{// Fetch recommended feesconstfeePath='/network/fees/transaction';console.log('Fetching recommended fees from',feePath);constfeeResponse=awaitfetch(`${NODE_URL}${feePath}`);constfeeJSON=awaitfeeResponse.json();constmedianMultiplier=feeJSON.medianFeeMultiplier;constminimumMultiplier=feeJSON.minFeeMultiplier;constfeeMultiplier=Math.max(medianMultiplier,minimumMultiplier);console.log(' Fee multiplier:',feeMultiplier);// Build the transactionconsttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV1Descriptor(facade.network.publicKeyToAddress(signerKeyPair.publicKey),[newdescriptors.UnresolvedMosaicDescriptor(generateMosaicAliasId('symbol.xym'),newmodels.Amount(1_000_000n))// 1 XYM],undefined),signerKeyPair.publicKey,feeMultiplier,2*60*60);// Sign transaction and generate final payloadconstsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);console.log('Built transaction:');console.dir(transaction.toJson(),{colors:true});consttransactionHash=facade.hashTransaction(transaction).toString();console.log('Transaction hash:',transactionHash);awaitannounceTransaction(jsonPayload,'transaction');awaitwaitForConfirmation(transactionHash,'transaction');}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
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.
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/transactionGET
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.
When creating transactions from descriptors as done in all tutorials, this operation is performed by the SDK.
If you create transactions manually using , you need to calculate the final fee
yourself.
Although applications can use a fixed fee for simplicity, it is more efficient to follow the network recommendation.
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 network's recommended multiplier (medianFeeMultiplier) and the
minimum multiplier (minFeeMultiplier) required by the node where the transactions will be sent.
The result is stored for later use once the transaction size is known.
The transfer transaction is created from the transaction's descriptor.
On typed languages like Java or JavaScript, these descriptors are also typed, so there are less chances of using
the wrong parameter when building or using them.
On untyped languages like Python, descriptors are generic objects which must contain the right fields for each
transaction type.
The descriptor contains the transaction-specific fields, while the creation method receives the common fields used
to finish the transaction.
// Build the transactionfinalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newTransferTransactionV1Descriptor(facade.network.publicKeyToAddress(signerKeyPair.getPublicKey()),List.of(newUnresolvedMosaicDescriptor(newUnresolvedMosaicId(IdGenerator.generateMosaicAliasId("symbol.xym")),newAmount(1_000_000))),// 1 XYMnull),signerKeyPair.getPublicKey(),feeMultiplier,2*60*60);
receives:
The transaction's descriptor: Defines TransferTransactionV1 and the transfer fields described below.
The 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.
The fee multiplier: Used to calculate the transaction fee.
The deadline duration: Set to two hours from the current time.
Deadlines and 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 network time, measured from the nemesis block.
If a transaction's deadline is earlier than the current network time or more than six hours in the future,
the transaction will be rejected.
When creating transactions from descriptors, the SDK takes care of network time and accepts a relative deadline
duration in seconds from now.
If you create transactions manually using , you need to provide the absolute
deadline yourself, as shown in the Manual Transaction Creation tutorial.
The transaction's descriptor contains:
: 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 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.
Amounts are expressed in atomic units, which depend on the mosaic's divisibility.
For XYM, the divisibility is 6, so 1 XYM must be expressed as 1_000_000.
The descriptor does not include common transaction fields such as the signer public key, deadline, or fee.
fills them in, taking care of network time for the relative
deadline and calculating the fee from the fee multiplier.
# Sign transaction and generate final payloadsignature=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 payloadconstsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);console.log('Built transaction:');console.dir(transaction.toJson(),{colors:true});
// Sign transaction and generate final payloadfinalCryptoTypes.Signaturesignature=facade.signTransaction(signerKeyPair,transaction);finalStringjsonPayload=SymbolTransactionFactory.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.
# Helper function to announce a transactiondefannounce_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')withurllib.request.urlopen(request)asannounce_response:print(f' Response: {announce_response.read().decode()}')
// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){console.log(`Announcing ${label} to /transactions`);constresponse=awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:payload});console.log(' Response:',awaitresponse.text());}
// Helper method to announce a transactionprivatevoidannounceTransaction(finalStringpayload,finalStringlabel)throwsIOException,InterruptedException{System.out.printf("Announcing %s to /transactions%n",label);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+"/transactions")).header("Content-Type","application/json").PUT(HttpRequest.BodyPublishers.ofString(payload)).build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());System.out.printf(" Response: %s%n",response.body());}
The helper receives the JSON payload and a human-readable label.
The label is only used in log messages, which makes the same helper reusable in tutorials that announce several
transactions.
Announcing a transaction is a simple request to the /transactionsPUT 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.
# Helper function to wait for transaction confirmationdefwait_for_confirmation(tx_hash,label):print(f'Waiting for {label} confirmation...')forattemptinrange(60):time.sleep(1)try:url=f'{NODE_URL}/transactionStatus/{tx_hash}'withurllib.request.urlopen(url)asconfirm_response:status=json.loads(confirm_response.read().decode())print(f' Transaction status: {status["group"]}')ifstatus['group']=='confirmed':print(f'{label} confirmed in {attempt} seconds')returnifstatus['group']=='failed':raiseRuntimeError(f'{label} failed: {status["code"]}')excepturllib.error.HTTPError:print(' Transaction status: unknown')raiseTimeoutError(f'{label} not confirmed after 60 seconds')
// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){console.log(`Waiting for ${label} confirmation...`);for(letattempt=0;60>attempt;attempt++){awaitnewPromise(resolve=>{setTimeout(resolve,1000);});constresponse=awaitfetch(`${NODE_URL}/transactionStatus/${transactionHash}`);if(!response.ok){if(404===response.status){console.log(' Transaction status: unknown');continue;}thrownewError(`HTTP ${response.status}`);}conststatus=awaitresponse.json();console.log(' Transaction status:',status.group);if('confirmed'===status.group){console.log(`${label} confirmed in`,attempt,'seconds');return;}if('failed'===status.group)thrownewError(`${label} failed: ${status.code}`);}thrownewError(`${label} not confirmed after 60 seconds`);}
// Helper method to wait for transaction confirmationprivatevoidwaitForConfirmation(finalStringtransactionHash,finalStringlabel)throwsIOException,InterruptedException{System.out.printf("Waiting for %s confirmation...%n",label);for(intattempt=0;60>attempt;++attempt){Thread.sleep(1000);finalStringstatusPath="/transactionStatus/"+transactionHash;finalHttpRequeststatusRequest=HttpRequest.newBuilder(URI.create(nodeUrl+statusPath)).GET().build();finalHttpResponse<String>statusResponse=HTTP_CLIENT.send(statusRequest,BodyHandlers.ofString());if(404==statusResponse.statusCode()){System.out.println(" Transaction status: unknown");continue;}if(2!=statusResponse.statusCode()/100)thrownewIOException("HTTP "+statusResponse.statusCode());finalJsonNodestatus=JSON_MAPPER.readTree(statusResponse.body());finalStringgroup=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))thrownewIOException(String.format("%s failed: %s",label,status.get("code").asText()));}thrownewIOException(String.format("%s not confirmed after 60 seconds",label));}
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.
WebSockets provide a more responsive solution without the overhead of repeated API calls.
In addition, the logic for checking transaction status is reusable.
This tutorial defines it as a helper because it is needed after announcing almost every transaction.
The snippet above repeatedly queries the /transactionStatus/{hash}GET endpoint using the hash of the submitted
transaction.
Like the announcement helper, it receives a label so its output remains clear when a tutorial announces several
transactions.
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.
Using node https://reference.symboltest.net:3001
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: ''
}
Transaction hash: 260CD293E05C2853A967874BCF67FAB36FD331CE14925CA611B3877B99BB325D
Announcing transaction to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
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
Some highlights from the output:
Fee multiplier (line 3): The recommended multiplier fetched from the network, used together with the
transaction size to compute the fee.
Signer public key (line 7): The account that signs the transaction and sends the mosaics.
Transaction fee (line 11): 17600 atomic units (0.0176 XYM), derived from the fee multiplier and the
transaction's size in bytes.
Recipient address (line 13): The account that receives the mosaics.
It looks different from the address used in the code because the transaction format encodes it in its raw
hexadecimal form rather than the Base32 text.
Mosaics (line 14): The assets transferred.
Here, 1000000 atomic units of the mosaic aliased by symbol.xym (XYM), equal to 1 XYM.
Announcement response (line 19): The node accepted the payload.
This does not yet mean the transaction is valid or included in a block.
Confirmed status (line 26): The transaction has been accepted and included in a block.
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 starts with Transaction hash:.
You should see the transaction move through the confirmation process in real time.
Alternatively, you can search for the signerPublicKey to view the transaction in the history of the signer account.