Send and receive payments
Most of the time, you’ll be sending money to someone else who has their own account. For this tutorial, however, you'll need a second account to transact with. So before proceeding, follow the steps outlined in Create an Account to make two accounts: one for sending and one for receiving. We'll also be using both payment operations as well as smart contract invocations to perform payments. For more details about the different types of transactions, see the Operations and Transactions documentation.
About Operations and Transactions
There are two ways to make payments on Stellar. One approach is to use Stellar's payment-related operations, and the other is to invoke a function on the token's contract, and the approach you should take depends on the use case.
If you want to make a payment of a Stellar asset between two Stellar accounts, use the payment operations. Transaction fees are cheaper when using them compared to those invoking the Stellar asset contract (or SAC).
If you want to make a payment of a Stellar asset between a Stellar account and a contract address, or between two contract addresses, you must use the SAC. Stellar's payment-related operations cannot have contract addresses as their source or destination.
Finally, if you want to make a payment of a contract token (not a Stellar asset), you must use the token's contract. Stellar's payment operations can only be used to transfer Stellar assets.
To learn more about the differences between Stellar assets and contract tokens, see the Tokens overview.
Additionally, there are two main options to interact with the Stellar network: the Horizon API and Stellar RPC. Generally speaking, you should use RPC unless you need features that only Horizon provides. If your use case is limited to making payments, RPC should be sufficient. Regardless, each payment example will be broken down to show how to perform such payments with either option.
In the following code samples, proper error checking is omitted for brevity. However, you should always validate your results, as there are many ways that requests can fail. You should refer to the guide on Error Handling for tips on error management strategies.
Using the Payment Operation
Send a Payment
When sending payments on Stellar, you build a transaction with a payment operation, sign it, and submit it to the network. Stellar stores and communicates transaction data in a binary format called XDR, which is optimized for network performance but unreadable to the human eye. Luckily, Horizon API, Stellar RPC, and the Stellar SDKs convert XDRs into friendlier formats. Here’s how you might send 10 lumens to an account:
Using the Stellar RPC
- JavaScript
import * as StellarSdk from "stellar-sdk";
// Initialize Soroban RPC server for testnet
const rpc = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org");
// Initalize the source account's secret key and destination account ID.
// The source account is the one that will send the payment, and the destination account
// is the one that will receive the payment.
const sourceKeys = StellarSdk.Keypair.fromSecret(
"SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"
);
const destinationId =
"GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5";
// First, check to make sure that the destination account exists.
try {
await rpc.getAccount(destinationId);
} catch (error) {
console.error("Error checking destination account:", error);
throw error;
}
// Now we also load the source account to build the transaction.
let sourceAccount: StellarSdk.Account;
try {
sourceAccount = await rpc.getAccount(sourceKeys.publicKey());
} catch (error) {
console.error("Error checking source account:", error);
throw error;
}
// The next step is to parametrize and build the transaction object:
// Using the source account we just loaded we begin to assemble the transaction.
// We set the fee to the base fee, which is 100 stroops (0.00001 XLM).
// We also set the network passphrase to TESTNET.
const transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
// We then add a payment operation to the transaction oject.
// This operation will send 10 XLM to the destination account.
// Obs.: Not specifying a explicit source account here means that the
// operation will use the source account of the whole transaction, which we specified above.
.addOperation(
StellarSdk.Operation.payment({
destination: destinationId,
asset: StellarSdk.Asset.native(),
amount: "10",
})
)
// We include an optional memo which oftentimes is used to identify the transaction
// when working with pooled accounts or to facilitate reconciliation.
.addMemo(StellarSdk.Memo.id("1234567890"))
// Finally, we set a timeout for the transaction.
// This means that the transaction will not be valid anymore after 180 seconds.
.setTimeout(180)
.build();
// We sign the transaction with the source account's secret key.
transaction.sign(sourceKeys);
// Now we can send the transaction to the network.
// The sendTransaction method immediately returns a reply with the transaction hash
// and the status "PENDING". This means the transaction was received and is being processed.
const sendTransactionResponse = await rpc.sendTransaction(transaction);
// Here we check the status of the transaction as there are
// a possible outcomes after sending a transaction that would have
// to be handled accordingly, such as "DUPLICATE" or "TRY_AGAIN_LATER".
if (sendTransactionResponse.status !== "PENDING") {
throw new Error(
`Failed to send transaction, status: ${sendTransactionResponse.status}`
);
}
// Here we poll the transaction status to await for its final result.
// We can use the transaction hash to poll the transaction status later.
const finalStatus = await rpc.pollTransaction(sendTransactionResponse.hash, {
sleepStrategy: (_iter: number) => 500,
attempts: 5,
});
// The pollTransaction method will return the final status of the transaction
// after the specified number of attempts or when the transaction is finalized.
// We then check the final status of the transaction and handle it accordingly.
switch (finalStatus.status) {
case StellarSdk.rpc.Api.GetTransactionStatus.FAILED:
case StellarSdk.rpc.Api.GetTransactionStatus.NOT_FOUND:
throw new Error(`Transaction failed with status: ${finalStatus.status}`);
case StellarSdk.rpc.Api.GetTransactionStatus.SUCCESS:
console.log("Success! Results:", finalStatus);
break;
}
The key differences when using RPC instead of Horizon:
- Instant submission:
rpc.sendTransaction()
returns immediately with a "PENDING" status - Active polling required: You must poll
rpc.pollTransaction()
to get the final result - Polling configuration: You can customize polling intervals and retry attempts
- Status handling: Check for SUCCESS, FAILED, or NOT_FOUND status in the final result
Using the Horizon API
- JavaScript
- Java
- Go
- Python
import * as StellarSdk from "stellar-sdk";
// Initialize Horizon server for testnet
const server = new StellarSdk.Horizon.Server(
"https://horizon-testnet.stellar.org"
);
// Initalize the source account's secret key and destination account ID.
// The source account is the one that will send the payment, and the destination account
// is the one that will receive the payment.
const sourceKeys = StellarSdk.Keypair.fromSecret(
"SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"
);
const destinationId =
"GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5";
// First, check to make sure that the destination account exists.
try {
await server.loadAccount(destinationId);
} catch (error) {
console.error("Error checking destination account:", error);
throw error;
}
// Now we also load the source account to build the transaction.
let sourceAccount: StellarSdk.Account;
try {
sourceAccount = await server.loadAccount(sourceKeys.publicKey());
} catch (error) {
console.error("Error checking source account:", error);
throw error;
}
// The next step is to parametrize and build the transaction object:
// Using the source account we just loaded we begin to assemble the transaction.
// We set the fee to the base fee, which is 100 stroops (0.00001 XLM).
// We also set the network passphrase to TESTNET.
const transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
// We then add a payment operation to the transaction oject.
// This operation will send 10 XLM to the destination account.
// Obs.: Not specifying a explicit source account here means that the
// operation will use the source account of the whole transaction, which we specified above.
.addOperation(
StellarSdk.Operation.payment({
destination: destinationId,
asset: StellarSdk.Asset.native(),
amount: "10",
})
)
// We include an optional memo which oftentimes is used to identify the transaction
// when working with pooled accounts or to facilitate reconciliation.
.addMemo(StellarSdk.Memo.id("1234567890"))
// Finally, we set a timeout for the transaction.
// This means that the transaction will not be valid anymore after 180 seconds.
.setTimeout(180)
.build();
// We sign the transaction with the source account's secret key.
transaction.sign(sourceKeys);
// Now we can send the transaction to the network.
// The sendTransaction method returns a promise that resolves with the transaction result.
// The result will contain the transaction hash and other details.
try {
const result = await server.submitTransaction(transaction);
console.log("Success! Results:", result);
} catch (error) {
console.error("Something went wrong!", error);
}
Server server = new Server("https://horizon-testnet.stellar.org");
KeyPair source = KeyPair.fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4");
KeyPair destination = KeyPair.fromAccountId("GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5");
// First, check to make sure that the destination account exists.
// You could skip this, but if the account does not exist, you will be charged
// the transaction fee when the transaction fails.
// It will throw HttpResponseException if account does not exist or there was another error.
server.accounts().account(destination.getAccountId());
// If there was no error, load up-to-date information on your account.
AccountResponse sourceAccount = server.accounts().account(source.getAccountId());
// Start building the transaction.
Transaction transaction = new Transaction.Builder(sourceAccount, Network.TESTNET)
.addOperation(new PaymentOperation.Builder(destination.getAccountId(), new AssetTypeNative(), "10").build())
// A memo allows you to add your own metadata to a transaction. It's
// optional and does not affect how Stellar treats the transaction.
.addMemo(Memo.text("Test Transaction"))
// Wait a maximum of three minutes for the transaction
.setTimeout(180)
// Set the amount of lumens you're willing to pay per operation to submit your transaction
.setBaseFee(Transaction.MIN_BASE_FEE)
.build();
// Sign the transaction to prove you are actually the person sending it.
transaction.sign(source);
// And finally, send it off to Stellar!
try {
SubmitTransactionResponse response = server.submitTransaction(transaction);
System.out.println("Success!");
System.out.println(response);
} catch (Exception e) {
System.out.println("Something went wrong!");
System.out.println(e.getMessage());
// If the result is unknown (no response body, timeout etc.) we simply resubmit
// already built transaction:
// SubmitTransactionResponse response = server.submitTransaction(transaction);
}
package main
import (
"github.com/stellar/go/keypair"
"github.com/stellar/go/network"
"github.com/stellar/go/txnbuild"
"github.com/stellar/go/clients/horizonclient"
"fmt"
)
func main () {
source := "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"
destination := "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5"
client := horizonclient.DefaultTestNetClient
// Make sure destination account exists
destAccountRequest := horizonclient.AccountRequest{AccountID: destination}
destinationAccount, err := client.AccountDetail(destAccountRequest)
if err != nil {
panic(err)
}
fmt.Println("Destination Account", destinationAccount)
// Load the source account
sourceKP := keypair.MustParseFull(source)
sourceAccountRequest := horizonclient.AccountRequest{AccountID: sourceKP.Address()}
sourceAccount, err := client.AccountDetail(sourceAccountRequest)
if err != nil {
panic(err)
}
// Build transaction
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: &sourceAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
Preconditions: txnbuild.Preconditions{
TimeBounds: txnbuild.NewInfiniteTimeout(), // Use a real timeout in production!
},
Operations: []txnbuild.Operation{
&txnbuild.Payment{
Destination: destination,
Amount: "10",
Asset: txnbuild.NativeAsset{},
},
},
},
)
if err != nil {
panic(err)
}
// Sign the transaction to prove you are actually the person sending it.
tx, err = tx.Sign(network.TestNetworkPassphrase, sourceKP)
if err != nil {
panic(err)
}
// And finally, send it off to Stellar!
resp, err := horizonclient.DefaultTestNetClient.SubmitTransaction(tx)
if err != nil {
panic(err)
}
fmt.Println("Successful Transaction:")
fmt.Println("Ledger:", resp.Ledger)
fmt.Println("Hash:", resp.Hash)
}
from stellar_sdk import Asset, Keypair, Network, Server, TransactionBuilder
from stellar_sdk.exceptions import NotFoundError, BadResponseError, BadRequestError
server = Server("https://horizon-testnet.stellar.org")
source_key = Keypair.from_secret("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4")
destination_id = "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5"
# First, check to make sure that the destination account exists.
# You could skip this, but if the account does not exist, you will be charged
# the transaction fee when the transaction fails.
try:
server.load_account(destination_id)
except NotFoundError:
# If the account is not found, surface an error message for logging.
raise Exception("The destination account does not exist!")
# If there was no error, load up-to-date information on your account.
source_account = server.load_account(source_key.public_key)
# Let's fetch base_fee from network
base_fee = server.fetch_base_fee()
# Start building the transaction.
transaction = (
TransactionBuilder(
source_account=source_account,
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
base_fee=base_fee,
)
# Because Stellar allows transaction in many currencies, you must specify the asset type.
# Here we are sending Lumens.
.append_payment_op(destination=destination_id, asset=Asset.native(), amount="10")
# A memo allows you to add your own metadata to a transaction. It's
# optional and does not affect how Stellar treats the transaction.
.add_text_memo("Test Transaction")
# Wait a maximum of three minutes for the transaction
.set_timeout(10)
.build()
)
# Sign the transaction to prove you are actually the person sending it.
transaction.sign(source_key)
try:
# And finally, send it off to Stellar!
response = server.submit_transaction(transaction)
print(f"Response: {response}")
except (BadRequestError, BadResponseError) as err:
print(f"Something went wrong!\n{err}")
Step-by-Step Breakdown
What exactly happened there? Let’s break it down.
- Confirm that the account ID (aka the public key) you are sending to actually exists by loading the associated account data from the Stellar network. It's okay to skip this step, but it gives you an opportunity to avoid making a transaction that will inevitably fail.
- JavaScript
- Java
- Go
- Python
// Using RPC
try {
const destinationAccount = await rpc.getAccount(destinationId);
/* validate the account */
} catch (error) {
console.error("Error checking the destination account:", error);
throw error;
}
// Using Horizon
try {
const destinationAccount = await server.loadAccount(destinationId);
/* validate the account */
} catch (error) {
console.error("Error checking the destination account:", error);
throw error;
}
server.accounts().account(destination.getAccountId());
destAccountRequest := horizonclient.AccountRequest{AccountID: destination}
destinationAccount, err := client.AccountDetail(destAccountRequest)
if err != nil {
panic(err)
}
fmt.Println("Destination Account", destinationAccount)
server.load_account(destination_id)
- Load data for the account you are sending from. An account can only perform one transaction at a time and has something called a sequence number, which helps Stellar verify the order of transactions. A transaction’s sequence number needs to match the account’s sequence number, so you need to get the account’s current sequence number from the network.
- JavaScript
- Java
- Go
- Python
// Using RPC
let sourceAccount: StellarSdk.Account;
try {
sourceAccount = await rpc.getAccount(sourceKeys.publicKey());
/* validate the account */
} catch (error) {
console.error("Error checking source account:", error);
throw error;
}
/* use sourceAccount to build transaction */
// Using Horizon
let sourceAccount: StellarSdk.Account;
try {
sourceAccount = await server.loadAccount(sourceKeys.publicKey());
/* validate the account */
} catch (error) {
console.error("Error checking source account:", error);
throw error;
}
/* use sourceAccount to build transaction */
AccountResponse sourceAccount = server.accounts().account(source.getAccountId());
sourceKP := keypair.MustParseFull(source)
sourceAccountRequest := horizonclient.AccountRequest{AccountID: sourceKP.Address()}
sourceAccount, err := client.AccountDetail(sourceAccountRequest)
if err != nil {
panic(err)
}
source_account = server.load_account(source_key.public_key)
The SDK will automatically increment the account’s sequence number when you build a transaction, so you won’t need to retrieve this information again if you want to perform a second transaction.
- Start building a transaction. This requires an account object, not just an account ID, because it will increment the account’s sequence number.
- JavaScript
- Java
- Go
- Python
const transaction = new StellarSdk.TransactionBuilder(sourceAccount);
Transaction transaction = new Transaction.Builder(sourceAccount, Network.TESTNET)
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: &sourceAccount,
IncrementSequenceNum: true,
BaseFee: MinBaseFee,
Preconditions: txnbuild.Preconditions{
TimeBounds: txnbuild.NewInfiniteTimeout(), // Use a real timeout in production!
},
...
},
)
if err != nil {
panic(err)
}
transaction = TransactionBuilder(
source_account=source_account,
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
base_fee=base_fee
)
- Add the payment operation to the account. Note that you need to specify the type of asset you are sending: Stellar’s network currency is the lumen, but you can send any asset issued on the network. We'll cover sending non-lumen assets below. For now, though, we’ll stick to lumens, which are called “native” assets in the SDK:
- JavaScript
- Java
- Go
- Python
.addOperation(StellarSdk.Operation.payment({
destination: destinationId,
asset: StellarSdk.Asset.native(),
amount: "10"
}))
.addOperation(new PaymentOperation.Builder(destination.getAccountId(), new AssetTypeNative(), "10").build())
Operations: []txnbuild.Operation{
&txnbuild.Payment{
Destination: destination,
Amount: "10",
Asset: txnbuild.NativeAsset{},
},
},
.append_payment_op(destination=destination_id, asset=Asset.native(), amount="10")
You should also note that the amount is a string rather than a number. When working with extremely small fractions or large values, floating point math can introduce small inaccuracies. Since not all systems have a native way to accurately represent extremely small or large decimals, Stellar uses strings as a reliable way to represent the exact amount across any system.
- Optionally, you can add your own metadata, called a memo, to a transaction. Stellar doesn’t do anything with this data, but you can use it for any purpose you’d like. Many exchanges require memos for incoming transactions because they use a single Stellar account for all their users and rely on the memo to differentiate between internal user accounts.
- JavaScript
- Java
- Go
- Python
.addMemo(StellarSdk.Memo.text('Test Transaction'))
.addMemo(Memo.text("Test Transaction"));
Memo: txnbuild.MemoText("Test Transaction")
.add_text_memo("Test Transaction")
- Now that the transaction has all the data it needs, you have to cryptographically sign it using your secret key. This proves that the data actually came from you and not someone impersonating you.
- JavaScript
- Java
- Go
- Python
transaction.sign(sourceKeys);
transaction.sign(source);
tx, err = tx.Sign(network.TestNetworkPassphrase, sourceKP)
if err != nil {
panic(err)
}
transaction.sign(source_key)
- And finally, submit it to the Stellar network!
- JavaScript
- Java
- Go
- Python
// Using RPC - requires polling for final status
let sendTransactionResponse: StellarSdk.rpc.Api.SendTransactionResponse;
try {
sendTransactionResponse = await rpc.sendTransaction(transaction);
if (sendTransactionResponse.status !== "PENDING") {
throw sendTransactionResponse;
}
} catch (error) {
console.error("Error sending transaction:", error);
throw error;
}
try {
const finalStatus = await rpc.pollTransaction(sendTransactionResponse.hash, {
sleepStrategy: (_iter: number) => 500,
attempts: 5,
});
switch (finalStatus.status) {
case StellarSdk.rpc.Api.GetTransactionStatus.FAILED:
case StellarSdk.rpc.Api.GetTransactionStatus.NOT_FOUND:
throw new Error(`Transaction failed with status: ${finalStatus.status}`);
case StellarSdk.rpc.Api.GetTransactionStatus.SUCCESS:
console.log("Success! Results:", finalStatus);
break;
}
} catch (error) {
console.error("Error polling transaction status:", error);
throw error;
}
// Using Horizon - returns final status immediately
try {
const result = await server.submitTransaction(transaction);
console.log("Success! Results:", result);
} catch (error) {
console.error("Something went wrong!", error);
}
server.submitTransaction(transaction);
resp, err := horizonclient.DefaultTestNetClient.SubmitTransaction(tx)
if err != nil {
panic(err)
}
server.submit_transaction(transaction)
In this example, we're submitting the transaction to either the SDF-maintained public testnet instance of Horizon (the Stellar API) or to a Stellar RPC server. When submitting transactions to either a Horizon server or RPC server — which is what most people do — it's possible that you will not receive a response from the server due to a bug, network conditions, etc. In such a situation it's impossible to determine the status of your transaction.
That's why you should always save a built transaction (or transaction encoded in XDR format) in a variable or a database and resubmit it if you don't know its status. If the transaction has already been successfully applied to the ledger, both Horizon and RPC will simply return the saved result and not attempt to submit the transaction again. Only in cases where a transaction's status is unknown (and thus will have a chance of being included into a ledger) will a resubmission to the network occur.
Receive a Payment
You don’t actually need to do anything to receive payments into a Stellar account: if a payer makes a successful transaction to send assets to you, those assets will automatically be added to your account.
However, you may want to keep an eye out for incoming payments. A simple program that watches the network for payments and prints each one might look like:
Using the Stellar RPC
With the upcoming release of Protocol 23, native payments for assets will also emit smart contract events, which will allow for the use the Stellar RPC to stream payments for native assets. The process will then be similar to how the example under section Stellar Asset Contract (SAC) -> Receive a Payment works. For now, you can use the Horizon API to stream payments for native assets.
Using Horizon
- JavaScript
- Java
- Go
- Python
var StellarSdk = require("stellar-sdk");
var server = new StellarSdk.Horizon.Server(
"https://horizon-testnet.stellar.org",
);
var accountId = "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF";
// Create an API call to query payments involving the account.
var payments = server.payments().forAccount(accountId);
// If some payments have already been handled, start the results from the
// last seen payment. (See below in `handlePayment` where it gets saved.)
var lastToken = loadLastPagingToken();
if (lastToken) {
payments.cursor(lastToken);
}
// `stream` will send each recorded payment, one by one, then keep the
// connection open and continue to send you new payments as they occur.
payments.stream({
onmessage: function (payment) {
// Record the paging token so we can start from here next time.
savePagingToken(payment.paging_token);
// The payments stream includes both sent and received payments. We only
// want to process received payments here.
if (payment.to !== accountId) {
return;
}
// In Stellar’s API, Lumens are referred to as the “native” type. Other
// asset types have more detailed information.
var asset;
if (payment.asset_type === "native") {
asset = "lumens";
} else {
asset = payment.asset_code + ":" + payment.asset_issuer;
}
console.log(payment.amount + " " + asset + " from " + payment.from);
},
onerror: function (error) {
console.error("Error in payment stream");
},
});
function savePagingToken(token) {
// In most cases, you should save this to a local database or file so that
// you can load it next time you stream new payments.
}
function loadLastPagingToken() {
// Get the last paging token from a local database or file
}
Server server = new Server("https://horizon-testnet.stellar.org");
KeyPair account = KeyPair.fromAccountId("GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF");
// Create an API call to query payments involving the account.
PaymentsRequestBuilder paymentsRequest = server.payments().forAccount(account.getAccountId());
// If some payments have already been handled, start the results from the
// last seen payment. (See below in `handlePayment` where it gets saved.)
String lastToken = loadLastPagingToken();
if (lastToken != null) {
paymentsRequest.cursor(lastToken);
}
// `stream` will send each recorded payment, one by one, then keep the
// connection open and continue to send you new payments as they occur.
paymentsRequest.stream(new EventListener<OperationResponse>() {
@Override
public void onEvent(OperationResponse payment) {
// Record the paging token so we can start from here next time.
savePagingToken(payment.getPagingToken());
// The payments stream includes both sent and received payments. We only
// want to process received payments here.
if (payment instanceof PaymentOperationResponse) {
if (!((PaymentOperationResponse) payment).getTo().equals(account.getAccountId()) {
return;
}
String amount = ((PaymentOperationResponse) payment).getAmount();
Asset asset = ((PaymentOperationResponse) payment).getAsset();
String assetName;
if (asset.equals(new AssetTypeNative())) {
assetName = "lumens";
} else {
StringBuilder assetNameBuilder = new StringBuilder();
assetNameBuilder.append(((AssetTypeCreditAlphaNum) asset).getCode());
assetNameBuilder.append(":");
assetNameBuilder.append(((AssetTypeCreditAlphaNum) asset).getIssuer());
assetName = assetNameBuilder.toString();
}
StringBuilder output = new StringBuilder();
output.append(amount);
output.append(" ");
output.append(assetName);
output.append(" from ");
output.append(((PaymentOperationResponse) payment).getFrom());
System.out.println(output.toString());
}
}
@Override
public void onFailure(Optional<Throwable> optional, Optional<Integer> optional1) {
}
});
package main
import (
"context"
"fmt"
"time"
"github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/protocols/horizon/operations"
)
func main() {
client := horizonclient.DefaultTestNetClient
opRequest := horizonclient.OperationRequest{ForAccount: "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF", Cursor: "now"}
ctx, cancel := context.WithCancel(context.Background())
go func() {
// Stop streaming after 60 seconds.
time.Sleep(60 * time.Second)
cancel()
}()
printHandler := func(op operations.Operation) {
fmt.Println(op)
}
err := client.StreamPayments(ctx, opRequest, printHandler)
if err != nil {
fmt.Println(err)
}
}
from stellar_sdk import Server
def load_last_paging_token():
# Get the last paging token from a local database or file
return "now"
def save_paging_token(paging_token):
# In most cases, you should save this to a local database or file so that
# you can load it next time you stream new payments.
pass
server = Server("https://horizon-testnet.stellar.org")
account_id = "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF"
# Create an API call to query payments involving the account.
payments = server.payments().for_account(account_id)
# If some payments have already been handled, start the results from the
# last seen payment. (See below in `handle_payment` where it gets saved.)
last_token = load_last_paging_token()
if last_token:
payments.cursor(last_token)
# `stream` will send each recorded payment, one by one, then keep the
# connection open and continue to send you new payments as they occur.
for payment in payments.stream():
# Record the paging token so we can start from here next time.
save_paging_token(payment["paging_token"])
# We only process `payment`, ignore `create_account` and `account_merge`.
if payment["type"] != "payment":
continue
# The payments stream includes both sent and received payments. We
# only want to process received payments here.
if payment['to'] != account_id:
continue
# In Stellar’s API, Lumens are referred to as the “native” type. Other
# asset types have more detailed information.
if payment["asset_type"] == "native":
asset = "Lumens"
else:
asset = f"{payment['asset_code']}:{payment['asset_issuer']}"
print(f"{payment['amount']} {asset} from {payment['from']}")
There are two main parts to this program. First, you create a query for payments involving a given account. Like most queries in Stellar, this could return a huge number of items, so the API returns paging tokens, which you can use later to start your query from the same point where you previously left off. In the example above, the functions to save and load paging tokens are left blank, but in a real application, you’d want to save the paging tokens to a file or database so you can pick up where you left off in case the program crashes or the user closes it.
- JavaScript
- Java
- Go
- Python
var payments = server.payments().forAccount(accountId);
var lastToken = loadLastPagingToken();
if (lastToken) {
payments.cursor(lastToken);
}
PaymentsRequestBuilder paymentsRequest = server.payments().forAccount(account.getAccountId());
String lastToken = loadLastPagingToken();
if (lastToken != null) {
paymentsRequest.cursor(lastToken);
}
client := horizonclient.DefaultTestNetClient
opRequest := horizonclient.OperationRequest{ForAccount: "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF", Cursor: "now"}
payments = server.payments().for_account(account_id)
last_token = load_last_paging_token()
if last_token:
payments.cursor(last_token)
Second, the results of the query are streamed. This is the easiest way to watch for payments or other transactions. Each existing payment is sent through the stream, one by one. Once all existing payments have been sent, the stream stays open and new payments are sent as they are made.
Try it out: Run this program, and then, in another window, create and submit a payment. You should see this program log the payment.
- JavaScript
- Java
- Go
- Python
payments.stream({
onmessage: function (payment) {
// handle a payment
},
});
paymentsRequest.stream(new EventListener<OperationResponse>() {
@Override
public void onEvent(OperationResponse payment) {
// Handle a payment
}
});
ctx, cancel := context.WithCancel(context.Background())
go func() {
// Stop streaming after 60 seconds.
time.Sleep(60 * time.Second)
cancel()
}()
printHandler := func(op operations.Operation) {
fmt.Println(op)
}
err := client.StreamPayments(ctx, opRequest, printHandler)
if err != nil {
fmt.Println(err)
}
for payment in payments.stream():
# handle a payment
You can also request payments in groups or pages. Once you’ve processed each page of payments, you’ll need to request the next one until there are none left.
- JavaScript
- Java
- Python
payments.call().then(function handlePage(paymentsPage) {
paymentsPage.records.forEach(function (payment) {
// handle a payment
});
return paymentsPage.next().then(handlePage);
});
Page<OperationResponse> page = payments.execute();
for (OperationResponse operation : page.getRecords()) {
// handle a payment
}
page = page.getNextPage();
payments_current = payments.call()
payments_next = payments.next()
Stellar Asset Contract (SAC) Payments
When sending payments using Stellar Asset Contracts, you invoke smart contract functions instead of using payment operations. SAC payments utilize the invokeHostFunction
operation to call contract methods like transfer
, which provides the same payment functionality but through contract logic. This approach offers additional flexibility and programmability while maintaining compatibility with Stellar's asset system.
In the example below, we're using SAC as a smart contract interface for the native asset, so the end result is the same as if a native payment operation had been executed. However, the same process can be applied for any smart contract transfer involving standardized contract tokens that follow the contract token interface. This makes SAC payments a unified approach for handling both native assets and contract tokens through a consistent contract-based interface.
Here's how you might send 10 lumens using a SAC:
To use the RPC example below you should first generate the contract bindings so the client can be used accordingly. This can be achieved through the Stellar CLI.
E.g.: Generating the typescript bindings for the sac
contract of a given asset:
stellar contract bindings typescript --network=testnet --contract-id=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC --output-dir=./bindings
Send a Payment
- JavaScript
import {
Account,
Asset,
Keypair,
Networks,
TransactionBuilder,
} from "stellar-sdk";
import { Client } from "sac";
import { Server } from "stellar-sdk/rpc";
// Initialize Soroban RPC server for testnet
const rpc = new Server("https://soroban-testnet.stellar.org");
// Initalize the source account's secret key and destination account ID.
// The source account is the one that will send the payment, and the destination account
// is the one that will receive the payment.
const sourceKeys = Keypair.fromSecret(
"SAMAJBEGN2743SLFDSBSVRCTQ7AC33XFZHWJVCJ2UMOIVH4MUJ7WWHEJ",
);
const destinationId =
"GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5";
// First, check to make sure that the destination account exists.
try {
await rpc.getAccount(destinationId);
} catch (error) {
console.error("Error checking destination account:", error);
throw error;
}
// Now we also load the source account to build the transaction.
try {
await rpc.getAccount(sourceKeys.publicKey());
} catch (error) {
console.error("Error checking source account:", error);
throw error;
}
// First, check to make sure that the destination account exists.
// You could skip this, but if the account does not exist, you will be charged
// the transaction fee when the transaction fails.
const xlmClient = new Client({
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: Networks.TESTNET,
contractId: Asset.native().contractId(Networks.TESTNET),
publicKey: sourceKeys.publicKey(),
signTransaction: async (txXdr) => {
const tx = await TransactionBuilder.fromXDR(txXdr, Networks.TESTNET);
tx.sign(sourceKeys);
return { signedTxXdr: tx.toXDR(), signerAddress: sourceKeys.publicKey() };
},
});
// Now, using the client, we assemble a soroban transaction to invoke the transfer
// function of the native asset contract. The ciente will automatically
// bvundle the operation and simulate the transaction before providing us
// with an assembled transaction object that we can sign and send.
let assembledTx;
try {
assembledTx = await xlmClient.transfer({
to: destinationId,
amount: BigInt(10_0000000), // Amount in stroops (1 XLM = 10,000,000 stroops)
from: sourceKeys.publicKey(),
});
} catch (error) {
console.error("Error assembling the transaction:", error);
throw error;
}
// The assembled transaction is ready to be signed and sent. It already includes
// a function that allows us to perform the signing and sending in one step. It will
// use the signTransaction function we provided to the client to sign the transaction or
// alternatively, receive a custom one just for this transaction.
try {
const result = await assembledTx.signAndSend();
console.log("Transaction successful:", result.getTransactionResponse?.txHash);
} catch (error) {
console.error("Error during transaction processing:", error);
throw error;
}
Step-by-Step Breakdown
What exactly happened in the SAC payment example? Let's break it down:
- Import the generated contract client from the CLI-produced bindings. This client provides typed methods for interacting with the Stellar Asset Contract.
- JavaScript
import { Client } from "sac";
- Initialize the SAC client with connection details and a custom signing function. The client needs the RPC endpoint, network passphrase, contract ID for the asset, and your account's public key. A signing function can also be provided to automatically handle transaction signing when invoking contract methods with this client.
- JavaScript
const xlmClient = new Client({
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: Networks.TESTNET,
contractId: Asset.native().contractId(Networks.TESTNET),
publicKey: sourceKeys.publicKey(),
signTransaction: async (txXdr) => {
const tx = await TransactionBuilder.fromXDR(txXdr, Networks.TESTNET);
tx.sign(sourceKeys);
return {
signedTxXdr: tx.toXDR(),
signerAddress: sourceKeys.publicKey(),
};
},
});
- Initiate the transfer by calling the contract's
transfer
method. This returns an assembled transaction ready for signing and submission.
- JavaScript
xlmClient.transfer({
to: destinationId,
amount: BigInt(10_0000000), // Amount in stroops (1 XLM = 10,000,000 stroops)
from: sourceKeys.publicKey(),
});
- Sign and submit the transaction using
signAndSend()
. The client handles simulation, signing (using your provided function), submission to the network, and polling for the final result.
- JavaScript
try {
const result = await assembledTx.signAndSend();
console.log("Transaction successful:", result.getTransactionResponse?.txHash);
} catch (error) {
console.error("Error during transaction processing:", error);
throw error;
}
Receive a Payment
You don't need to do anything special to receive SAC payments into a Stellar account: if a payer makes a successful contract transfer to you, those assets will automatically be added to your account.
However, you may want to monitor for incoming SAC payments. Since SAC payments generate contract events rather than traditional payment operations, you'll need to watch for contract events instead of using Horizon's payment streams. Here's how you can monitor for incoming Smart Contract transfers:
- JavaScript
import { Server } from "stellar-sdk/rpc";
import { xdr, Asset, Networks, Address } from "stellar-sdk";
// Initialize Soroban RPC server for testnet
const rpc = new Server("https://soroban-testnet.stellar.org");
// Get the native XLM contract ID for testnet
const contractId = Asset.native().contractId(Networks.TESTNET);
// The address we want to monitor for incoming payments
const monitoredAddress = new Address(
"GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5"
);
// Paging state for event polling (similar to useSubscription hook)
let lastLedgerStart: number | undefined;
let pagingToken: string | undefined;
async function pollForTransfers() {
// Set starting ledger if not set (get the latest ledger as starting point)
if (!lastLedgerStart) {
const latestLedger = await rpc.getLatestLedger();
lastLedgerStart = latestLedger.sequence;
}
console.log(`> Monitoring transfers for ledger: ${lastLedgerStart}`);
// Get events for "transfer" topic from the native asset contract
const response = await rpc.getEvents({
startLedger: !pagingToken ? lastLedgerStart : undefined,
cursor: pagingToken,
filters: [
{
contractIds: [contractId],
// Filter for transfer events to the monitored address
// Using wildcards (*) to match any sender and asset
// Event structure: ["transfer", fromAddress, toAddress, assetName]
topics: [
[
xdr.ScVal.scvSymbol("transfer").toXDR("base64"),
"*",
monitoredAddress.toScVal().toXDR("base64"),
"*",
],
],
type: "contract",
},
],
limit: 10,
});
// Update paging tokens for next poll
pagingToken = undefined;
if (response.latestLedger) {
lastLedgerStart = response.latestLedger;
}
// Process events and check for payments to our monitored address
if (response.events) {
response.events.forEach((event) => {
try {
const topics = event.topic;
console.log(
`Processing event: ${event.txHash} at ledger ${event.ledger}`
);
if (topics && topics.length >= 3) {
// Extract recipient address from event topics
const toAddress = Address.fromScAddress(
topics[2].address()
).toString();
// Check if the payment is to our monitored address
if (toAddress === monitoredAddress.toString()) {
console.log("PAYMENT RECEIVED!");
console.log(` Transaction: ${event.txHash}`);
console.log(` Ledger: ${event.ledger}`);
console.log(
` Sender: ${Address.fromScAddress(
topics[1].address()
).toString()}`
);
console.log(` Amount: ${event.value.i128().lo().toBigInt()}`);
}
}
} catch (error) {
console.error("Error processing event:", error);
} finally {
// Update paging token for next poll
pagingToken = event.pagingToken;
}
});
}
// Continue polling after 4 seconds
setTimeout(pollForTransfers, 4000);
}
// Start monitoring for payment events
console.log(`Starting payment monitor for: ${monitoredAddress}`);
pollForTransfers();
Step-by-Step Breakdown
What exactly happened in the SAC receive payment example? Let's break it down:
- Set up the RPC connection and get the contract ID for the asset you want to monitor. For native XLM, we use the built-in contract ID.
- JavaScript
const rpc = new Server("https://soroban-testnet.stellar.org");
const contractId = Asset.native().contractId(Networks.TESTNET);
const monitoredAddress = new Address(
"GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5",
);
- Initialize the polling system by getting the latest ledger sequence as a starting point for monitoring events.
- JavaScript
if (!lastLedgerStart) {
const latestLedger = await rpc.getLatestLedger();
lastLedgerStart = latestLedger.sequence;
}
- Query contract events using specific filters for transfer events. The filter targets the contract ID and listens for "transfer" events where your address is the recipient. The topics array defines the event structure: the first element is the event name ("transfer"), the second is the sender (wildcard "" to match any), the third is the recipient (your monitored address), and the fourth is the asset (wildcard "" for any asset).
- JavaScript
const response = await rpc.getEvents({
startLedger: !pagingToken ? lastLedgerStart : undefined,
cursor: pagingToken,
filters: [
{
contractIds: [contractId],
// Topics filter structure: ["event_name", "from_address", "to_address", "asset"]
// Using wildcards (*) allows matching any value in that position
topics: [
[
xdr.ScVal.scvSymbol("transfer").toXDR("base64"), // Event name: "transfer"
"*", // From: any address
monitoredAddress.toScVal().toXDR("base64"), // To: our monitored address
"*", // Asset: any asset
],
],
type: "contract",
},
],
limit: 10,
});
- Process the events by extracting transfer details from the event topics and values, then continue polling for new events after a short delay.
- JavaScript
response.events.forEach((event) => {
const topics = event.topic;
if (topics && topics.length >= 3) {
const toAddress = Address.fromScAddress(topics[2].address()).toString();
if (toAddress === monitoredAddress.toString()) {
console.log("PAYMENT RECEIVED!");
console.log(`Amount: ${event.value.i128().lo().toBigInt()}`);
}
}
});
// Continue polling
setTimeout(pollForTransfers, 4000);
Key Differences from Classic Payment Monitoring:
- Event-based: Monitors contract events instead of payment operations
- RPC Only: Uses RPC endpoints exclusively, no Horizon support
- Topic Filtering: Uses event topics to filter for specific transfer patterns
- Active Polling: Requires continuous polling rather than streaming
- Event Structure: Extracts payment data from contract event topics and values rather than operation fields
Transacting in Other Currencies
One of the amazing things about the Stellar network is that you can create, hold, send, receive, and trade any type of asset. Many organizations issue assets on Stellar that represent real-world currencies such as US dollars or Nigerian naira or other cryptocurrencies such as bitcoin or ether.
Each of these redeemable assets — anchored in the Stellar vernacular — is essentially a credit issued by a particular account that represents reserves those accounts hold outside the network. That's why the assets in the example above had both a code
and an issuer
: the issuer
is the public key of the account that created the asset, an account owned by the organization that ultimately honors the credit that asset represents. Additionally, with the introduction of smart contracts, the classic assets can also be represented by a contract id
(SAC Interface)) which allows for more complex interactions and interoperability with the smart contract applications.
Other custom tokens, built entirely with smart contracts are also represented by their own contract id
and should offer a similar interface for interaction when following the token interface.
Guides in this category:
📄️ Create an Account
Follow this step-by-step guide to learn account basics and how to create a blockchain account on Stellar, which involves creating a keypair and funding it.
📄️ Send and receive payments
Learn to send payments and watch for received payments on the Stellar network
📄️ Follow received payments
Watch for incoming payments using Horizon
📄️ Add support for smart contracts
Considerations for integrating with Stellar’s smart contracts specifically for wallets and exchanges
📄️ Automating Testnet and Futurenet reset data
Learn how to automate Testnet and Futurenet reset data on Stellar