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.
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.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.NemFacadeimportNemFacadefromsymbolchain.ncimportAmountfromsymbolchain.nem.FeeCalculatorimportcalculate_transaction_feeNODE_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')# Helper function to announce a transactiondefannounce_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')withurllib.request.urlopen(request)asannounce_response:result=json.loads(announce_response.read().decode())print(f' Result: {result["message"]}')returnresult['message']# Helper function to wait for transaction confirmationdefwait_for_confirmation(tx_hash,label):status_path=f'/transaction/get?hash={tx_hash}'print(f'Waiting for {label} confirmation from {status_path}')is_confirmed=Falsefor_inrange(120):try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asstatus_response:confirmed=json.loads(status_response.read().decode())height=confirmed['meta']['height']print(f'{label} confirmed in block {height}')is_confirmed=Truebreakexcepturllib.error.HTTPError:print(' Transaction status: pending')time.sleep(1)ifnotis_confirmed:print(f'{label} confirmation took too long.')# Define the amount of XEM to transferxem=float(os.getenv('XEM_AMOUNT','1'))amount=round(xem*1_000_000)try:# Build the transactiontransaction=facade.create_transaction_from_descriptor({'type':'transfer_transaction_v2','recipient_address':RECIPIENT_ADDRESS,'amount':amount},signer_key_pair.public_key,0,2*60*60)# Calculate and attach the transaction feefee=calculate_transaction_fee(transaction)transaction.fee=Amount(fee)print(f' Transaction fee: {fee/1_000_000} XEM')# 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_result=announce_transaction(json_payload,'transaction')if'SUCCESS'==announce_result:wait_for_confirmation(transaction_hash,'transaction')else:print(f'Transaction rejected: {announce_result}')exceptExceptionaserror:print(error)
import{PrivateKey}from'symbol-sdk';import{Address,NemFacade,calculateTransactionFee,descriptors,models}from'symbol-sdk/nem';constNODE_URL=process.env.NODE_URL||'http://libertalia.nemtest.net:7890';console.log('Using node',NODE_URL);constSIGNER_PRIVATE_KEY=process.env.SIGNER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constsignerKeyPair=newNemFacade.KeyPair(newPrivateKey(SIGNER_PRIVATE_KEY));constRECIPIENT_ADDRESS=process.env.RECIPIENT_ADDRESS||'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';constfacade=newNemFacade('testnet');// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){constannouncePath='/transaction/announce';console.log(`Announcing ${label} to ${announcePath}`);constresponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:payload});if(!response.ok)thrownewError(`HTTP ${response.status}`);constresult=awaitresponse.json();console.log(' Result:',result.message);returnresult.message;}// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){conststatusPath=`/transaction/get?hash=${transactionHash}`;console.log(`Waiting for ${label} confirmation from`,statusPath);letisConfirmed=false;for(letattempt=1;120>=attempt;++attempt){constresponse=awaitfetch(`${NODE_URL}${statusPath}`);if(!response.ok){console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}else{constconfirmed=awaitresponse.json();console.log(`${label} confirmed in block`,confirmed.meta.height);isConfirmed=true;break;}}if(!isConfirmed)console.warn(`${label} confirmation took too long.`);}// Define the amount of XEM to transferconstxem=parseFloat(process.env.XEM_AMOUNT||'1');constamount=BigInt(Math.round(xem*1_000_000));try{// Build the transactionconsttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV2Descriptor(newAddress(RECIPIENT_ADDRESS),newmodels.Amount(amount)),signerKeyPair.publicKey,0n,2*60*60);// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.Amount(fee);console.log(` Transaction fee: ${Number(fee)/1_000_000} XEM`);// 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);constannounceResult=awaitannounceTransaction(jsonPayload,'transaction');if('SUCCESS'===announceResult)awaitwaitForConfirmation(transactionHash,'transaction');elseconsole.log('Transaction rejected:',announceResult);}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
//JAVA 21+//DEPS org.symbol:symbol-sdk:3.3.3importjava.io.IOException;importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.net.http.HttpResponse.BodyHandlers;importjava.util.List;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.symbol.sdk.CryptoTypes;importorg.symbol.sdk.facade.NemFacade;importorg.symbol.sdk.nem.Address;importorg.symbol.sdk.nem.FeeCalculator;importorg.symbol.sdk.nem.KeyPair;importorg.symbol.sdk.nem.NemTransactionFactory;importorg.symbol.sdk.nem.descriptors.TransferTransactionV2Descriptor;importorg.symbol.sdk.nem.models.Amount;importorg.symbol.sdk.nem.models.Transaction;publicfinalclassTransferXem{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatestaticfinalHttpClientHTTP_CLIENT=HttpClient.newHttpClient();privatefinalStringnodeUrl=System.getenv().getOrDefault("NODE_URL","http://libertalia.nemtest.net:7890");privatefinalNemFacadefacade=newNemFacade("testnet");// Helper method to announce a transactionprivateStringannounceTransaction(finalStringpayload,finalStringlabel)throwsIOException,InterruptedException{finalStringannouncePath="/transaction/announce";System.out.printf("Announcing %s to %s%n",label,announcePath);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+announcePath)).header("Content-Type","application/json").POST(HttpRequest.BodyPublishers.ofString(payload)).build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100)thrownewIOException("HTTP "+response.statusCode());finalStringresult=JSON_MAPPER.readTree(response.body()).get("message").asText();System.out.printf(" Result: %s%n",result);returnresult;}// Helper method to wait for transaction confirmationprivatevoidwaitForConfirmation(finalStringtransactionHash,finalStringlabel)throwsIOException,InterruptedException{finalStringstatusPath=String.format("/transaction/get?hash=%s",transactionHash);System.out.printf("Waiting for %s confirmation from %s%n",label,statusPath);booleanisConfirmed=false;for(intattempt=1;120>=attempt;++attempt){finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+statusPath)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100){System.out.println(" Transaction status: pending");Thread.sleep(1000);}else{finalJsonNodeconfirmed=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);}publicstaticvoidmain(finalString[]args){try{newTransferXem().run();}catch(finalExceptionex){System.out.println(null==ex.getMessage()?ex.toString():ex.getMessage());}}privatevoidrun()throwsIOException,InterruptedException{System.out.printf("Using node %s%n",nodeUrl);finalStringsignerPrivateKey=System.getenv().getOrDefault("SIGNER_PRIVATE_KEY","0".repeat(64));finalKeyPairsignerKeyPair=newKeyPair(newCryptoTypes.PrivateKey(signerPrivateKey));finalStringrecipientAddress=System.getenv().getOrDefault("RECIPIENT_ADDRESS","TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4");// Define the amount of XEM to transferfinaldoublexem=Double.parseDouble(System.getenv().getOrDefault("XEM_AMOUNT","1"));finallongamount=Math.round(xem*1_000_000);// Build the transactionfinalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newTransferTransactionV2Descriptor(newAddress(recipientAddress),newAmount(amount),null,List.of()),signerKeyPair.getPublicKey(),0,2*60*60);// Calculate and attach the transaction feefinallongfee=FeeCalculator.calculateTransactionFee(transaction);transaction.setFee(newAmount(fee));System.out.printf(" Transaction fee: %s XEM%n",(double)fee/1_000_000);// Sign transaction and generate final payloadfinalCryptoTypes.Signaturesignature=facade.signTransaction(signerKeyPair,transaction);finalStringjsonPayload=NemTransactionFactory.attachSignature(transaction,signature);System.out.println("Built transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));finalStringtransactionHash=facade.hashTransaction(transaction).toString();System.out.printf("Transaction hash: %s%n",transactionHash);finalStringannounceResult=announceTransaction(jsonPayload,"transaction");if("SUCCESS".equals(announceResult))waitForConfirmation(transactionHash,"transaction");elseSystem.out.printf("Transaction rejected: %s%n",announceResult);}}
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.
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.
// Define the amount of XEM to transferfinaldoublexem=Double.parseDouble(System.getenv().getOrDefault("XEM_AMOUNT","1"));finallongamount=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.
# Build the transactiontransaction=facade.create_transaction_from_descriptor({'type':'transfer_transaction_v2','recipient_address':RECIPIENT_ADDRESS,'amount':amount},signer_key_pair.public_key,0,2*60*60)
// Build the transactionconsttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV2Descriptor(newAddress(RECIPIENT_ADDRESS),newmodels.Amount(amount)),signerKeyPair.publicKey,0n,2*60*60);
// Build the transactionfinalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newTransferTransactionV2Descriptor(newAddress(recipientAddress),newAmount(amount),null,List.of()),signerKeyPair.getPublicKey(),0,2*60*60);
The snippet calls the facade's creation method with the transaction's descriptor and the values common to every
transaction: the signer's public key, an initial fee of zero, and a deadline duration of two hours.
The SDK fills the signer, sets the timestamp from the local clock, and derives the deadline from that duration.
Network time
Every NEM transaction contains a timestamp and deadline, both expressed as the number of seconds since the NEM
nemesis block. The deadline must be after the timestamp and no more than
24 hours later.
The facade uses the local system clock for these values. Applications that need to compare it with a node's clock
can query /time-sync/network-timeGET, but do not need to fetch network time before creating every transaction.
The descriptor supplies the transfer-specific properties, in this order:
Transaction type: 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 address that will receive the XEM.
: The atomic-unit value computed earlier. For 1 XEM, this is 1_000_000.
The signer passed separately to pays the fee and supplies the
transferred XEM.
// Calculate and attach the transaction feefinallongfee=FeeCalculator.calculateTransactionFee(transaction);transaction.setFee(newAmount(fee));System.out.printf(" Transaction fee: %s XEM%n",(double)fee/1_000_000);
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.
# 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=NemTransactionFactory.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.
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.
# Helper function to announce a transactiondefannounce_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')withurllib.request.urlopen(request)asannounce_response:result=json.loads(announce_response.read().decode())print(f' Result: {result["message"]}')returnresult['message']
// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){constannouncePath='/transaction/announce';console.log(`Announcing ${label} to ${announcePath}`);constresponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:payload});if(!response.ok)thrownewError(`HTTP ${response.status}`);constresult=awaitresponse.json();console.log(' Result:',result.message);returnresult.message;}
// Helper method to announce a transactionprivateStringannounceTransaction(finalStringpayload,finalStringlabel)throwsIOException,InterruptedException{finalStringannouncePath="/transaction/announce";System.out.printf("Announcing %s to %s%n",label,announcePath);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+announcePath)).header("Content-Type","application/json").POST(HttpRequest.BodyPublishers.ofString(payload)).build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100)thrownewIOException("HTTP "+response.statusCode());finalStringresult=JSON_MAPPER.readTree(response.body()).get("message").asText();System.out.printf(" Result: %s%n",result);returnresult;}
The helper receives the JSON payload and a human-readable label.
The label is only used in log messages, making the same helper reusable in tutorials that announce several
transactions.
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.
# Helper function to wait for transaction confirmationdefwait_for_confirmation(tx_hash,label):status_path=f'/transaction/get?hash={tx_hash}'print(f'Waiting for {label} confirmation from {status_path}')is_confirmed=Falsefor_inrange(120):try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asstatus_response:confirmed=json.loads(status_response.read().decode())height=confirmed['meta']['height']print(f'{label} confirmed in block {height}')is_confirmed=Truebreakexcepturllib.error.HTTPError:print(' Transaction status: pending')time.sleep(1)ifnotis_confirmed:print(f'{label} confirmation took too long.')
// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){conststatusPath=`/transaction/get?hash=${transactionHash}`;console.log(`Waiting for ${label} confirmation from`,statusPath);letisConfirmed=false;for(letattempt=1;120>=attempt;++attempt){constresponse=awaitfetch(`${NODE_URL}${statusPath}`);if(!response.ok){console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}else{constconfirmed=awaitresponse.json();console.log(`${label} confirmed in block`,confirmed.meta.height);isConfirmed=true;break;}}if(!isConfirmed)console.warn(`${label} confirmation took too long.`);}
// Helper method to wait for transaction confirmationprivatevoidwaitForConfirmation(finalStringtransactionHash,finalStringlabel)throwsIOException,InterruptedException{finalStringstatusPath=String.format("/transaction/get?hash=%s",transactionHash);System.out.printf("Waiting for %s confirmation from %s%n",label,statusPath);booleanisConfirmed=false;for(intattempt=1;120>=attempt;++attempt){finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+statusPath)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100){System.out.println(" Transaction status: pending");Thread.sleep(1000);}else{finalJsonNodeconfirmed=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);}
The snippet above repeatedly queries the /transaction/getGET endpoint using the hash of the announced transaction.
Like the announcement helper, it receives a label so its output remains clear when a tutorial announces several
transactions.
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 HTTP 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.
Signer public key (line 9): The account that signs the transaction and sends the XEM.
Transaction fee (line 11): 50000 atomic units (0.05 XEM), the fee for sending the default amount of 1 XEM.
Recipient address (line 13): 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 14): 1000000 atomic units, equal to 1 XEM.
No mosaics (line 15): An empty mosaics array means the transaction sends XEM only.
Transaction hash (line 17): The hash that uniquely identifies the transaction on the network.
Announcement result (line 19): A result of SUCCESS means the node accepted the transaction into the unconfirmed
pool.
Confirmation (line 33): 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 starts with Transaction hash:.