Saltar al contenido principal

Enviar y recibir pagos

Resumen de pagos

Un pago constituye la transferencia de un token de una cuenta a otra. En la red Stellar, un token puede ser un activo Stellar o un token de contrato personalizado que sigue el estándar de token SEP-41.

Hay dos formas principales en que se realizan los pagos en Stellar:

El enfoque que debes tomar depende del caso de uso:

  • Si deseas hacer un pago de un activo de Stellar entre dos cuentas Stellar, usa las operaciones de pago. Las tarifas de transacción son más económicas al usarlas comparado con las tarifas por invocar directamente el contrato de token del activo, conocido como el Contrato de Activo Stellar.
  • Si quieres realizar un pago de un activo Stellar entre una cuenta Stellar y una dirección de contrato, o entre dos direcciones de contrato, entonces debe usarse el contrato del activo. Las operaciones de pago de Stellar no pueden tener direcciones de contrato como origen o destino.
  • Si deseas realizar un pago con un token de contrato personalizado que no sea un activo Stellar pero que siga el estándar de token SEP-41, debes usar el contrato del token. Las operaciones de pago de Stellar solo se pueden usar para transferir activos Stellar.

Para aprender más sobre las diferencias entre activos Stellar y tokens de contrato, consulta la descripción general de Tokens.

información

En los siguientes ejemplos de código, se omite la comprobación adecuada de errores por brevedad. Sin embargo, debes siempre validar tus resultados, ya que hay muchas formas en que las solicitudes pueden fallar.

Ejemplo de uso de Pagos

Este ejemplo destaca el enfoque mencionado para realizar pagos usando operaciones y activos, y el uso del RPC de Stellar con los SDKs de cliente Stellar para ejecutar todas las acciones necesarias.

Enviar un Pago

Demostramos un pago de un activo en Stellar. Construiremos una transacción con una operación de pago para enviar 10 Lummens desde una cuenta emisora a una cuenta receptora, la firmaremos como la cuenta emisora y la enviaremos a la red.

  • Enviando la transacción al nodo público de testnet mantenido por SDF del servidor Stellar RPC.
  • Al enviar transacciones al servidor RPC, es posible que no recibas una respuesta debido a las condiciones de la red.
    • En tal situación, es imposible determinar el estado de tu transacción.
    • Se recomienda siempre guardar una transacción (o la transacción codificada en formato XDR) en una variable o base de datos y reenviarla si no conoces su estado.
    • La transacción en formato XDR serializado es idempotente, lo que significa que si la transacción ya fue aplicada con éxito al libro mayor, el RPC simplemente devolverá el resultado guardado y no intentará enviar la transacción de nuevo.
    • Solo en los casos donde el estado de la transacción sea desconocido (y por lo tanto tenga posibilidad de ser incluida en un ledger) se producirá un reenvío a la red.
Haz clic para ver el código de envío de pagos
// send_payment.js
// follow the https://github.com/stellar/js-stellar-sdk?tab=readme-ov-file#installation

import * as StellarSdk from "@stellar/stellar-sdk";
import fs from "fs";
const rpcServer = new StellarSdk.rpc.Server(
"https://soroban-testnet.stellar.org",
);

async function sendPayments() {
const { Keypair } = StellarSdk;
const senderKeyPair = Keypair.random();
const recipientKeyPair = Keypair.random();
let senderAccount;

try {
// Request airdrop for the sender account - this creates, funds and returns the Account object
senderAccount = await rpcServer.requestAirdrop(senderKeyPair.publicKey());
console.log("Sender Account funded with airdrop");
console.log("Sender Account ID:", senderAccount.accountId());
console.log("Sender Sequence number:", senderAccount.sequence.toString());

// Note - this persistence of sender id to a file on file system is only done for demo purposes.
// It shares the created sender account with the payment monitor example which will run next.
// Since this example is using file system it therefore is intended to run only on Node.
// In a real application the stellar js sdk can be used in browser or Node.
fs.writeFileSync("sender_public.key", senderAccount.accountId());

console.log("\n\n");

await rpcServer.requestAirdrop(recipientKeyPair.publicKey());
console.log("Recipient Account funded with airdrop");
console.log("Recipient Account ID:", recipientKeyPair.publicKey());
} catch (err) {
console.error("Airdrop / Account loading failed:", err);
return;
}

// Now call sendPayment with the funded account, in a loop of once every 30 seconds
while (true) {
console.log("\n\nSending payment...");
await sendPayment(
senderKeyPair,
senderAccount,
recipientKeyPair.publicKey(),
);
await new Promise((resolve) => setTimeout(resolve, 30000)); // wait for 30 seconds
}
}

async function sendPayment(sender, senderAccount, recipient) {
// 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(senderAccount, {
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: recipient,
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(sender);

// 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 rpcServer.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}`,
);
}

console.log(
"Payment Transaction submitted, hash:",
sendTransactionResponse.hash,
);

// 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 rpcServer.pollTransaction(
sendTransactionResponse.hash,
);

// 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:
console.error("Transaction failed, status:", finalStatus.status);
if (finalStatus.resultXdr) {
console.error(
"Transaction Result XDR (decoded):",
JSON.stringify(finalStatus.resultXdr, null, 2),
);
}

throw new Error(`Transaction failed with status: ${finalStatus.status}`);
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! Committed on Ledger:", finalStatus.ledger);
break;
}
}

sendPayments().catch((error) => {
console.error("Error executing sendPayments function:", error);
process.exit(1);
});

Monitoreo de pagos como flujo de eventos

Aprovecha el último Protocolo 23 (Whisk) para capturar la actividad de pagos como eventos puros. Estos eventos pueden ser monitoreados eficazmente usando el método getEvents del servidor RPC, que proporciona filtrado del lado del servidor y detección en tiempo real de pagos.

El enfoque RPC ofrece varias ventajas:

  • Filtrado de temas de evento del lado del servidor: filtra eventos específicos usando patrones de temas
  • Sondeo eficiente: usa cursores y paginación para procesamiento confiable de eventos
  • Modelo de eventos unificado: soporta CAP-67, todos los movimientos de activos en la red se emiten como eventos con nombres de temas estandarizados para indicar el tipo de movimiento (p. ej., "transfer" para pagos)
  • Mejor manejo de errores: el sondeo HTTP sin estado en RPC es más resistente que protocolos de streaming persistentes como SSE o WebSockets

Demuestra monitoreo casi en tiempo real de transacciones de la red Stellar estableciendo un listener asíncrono para consumir eventos desde RPC y filtrarlos por tema solo para los pagos de ejemplo generados por nuestro script anterior.

Haz clic para ver el código de monitoreo de pagos
// monitor_payment.js
// follow the https://github.com/stellar/js-stellar-sdk?tab=readme-ov-file#installation

import * as StellarSdk from "@stellar/stellar-sdk";
import fs from "fs";

async function monitorPayments() {
// Initialize RPC server for testnet
const rpcServer = new StellarSdk.rpc.Server(
"https://soroban-testnet.stellar.org",
);

// One-time initialization - everything encapsulated here
const monitoredFromAccount = fs
.readFileSync("sender_public.key", "utf-8")
.trim();

// create our payment event topic filter values
const transferTopicFilter =
StellarSdk.xdr.ScVal.scvSymbol("transfer").toXDR("base64");
const fromTopicFilter = StellarSdk.nativeToScVal(monitoredFromAccount, {
type: "address",
}).toXDR("base64");

// Get starting ledger
const latestLedger = await rpcServer.getLatestLedger();
console.log(
`Starting payment monitoring from ledger ${latestLedger.sequence} and from account ${monitoredFromAccount}`,
);
let currentStartLedger = latestLedger.sequence;
let currentCursor;

while (true) {
// Query for payments from our monitored account
const eventsResponse = await rpcServer.getEvents({
startLedger: currentStartLedger,
cursor: currentCursor,
filters: [
{
type: "contract",
topics: [
[
transferTopicFilter,
fromTopicFilter,
"**", // filter will match on any 'to' address and any token(SAC or SEP-41)
],
],
},
],
limit: 10,
});

// Process any events found
console.log(`Found ${eventsResponse.events.length} payment(s):`);

for (const event of eventsResponse.events) {
console.log("\n--- Payment Received ---");
console.log(`Ledger: ${event.ledger}`);
console.log(`Transaction Hash: ${event.txHash}`);
console.log(`Closed At: ${event.ledgerClosedAt}`);

// Decode addresses from topics
try {
const fromAddress = StellarSdk.scValToNative(event.topic[1]);
const toAddress = StellarSdk.scValToNative(event.topic[2]);
console.log(`Transfer: ${fromAddress}${toAddress}`);
} catch (error) {
console.log(`From/To: Unable to decode addresses`, error);
continue;
}

// Decode transfer amount from the event data
try {
// Protocol 23+ unified events model per CAP-0067
// https://github.com/stellar/stellar-protocol/blob/master/core/cap-0067.md
// Event value field is scMap with {amount: i128, to_muxed_id: u64|bytes|string} (when muxed info present)
const amount = StellarSdk.scValToNative(event.value)["amount"];
console.log(
`Amount: ${amount.toString()} stroops (${(Number(amount) / 10000000).toFixed(7)} XLM)`,
);
} catch (error) {
console.log(`Failed to decode transfer amount:`, error);
}
// Decode asset from topics[3], this is only present in events from SAC tokens
if (event.topic.length > 3) {
const asset = StellarSdk.scValToNative(event.topic[3]);
console.log(`Asset: ${asset}`);
}
}

// Update cursor to drive next query
currentCursor = eventsResponse.cursor;
currentStartLedger = null;

// Wait 5 seconds before next iteration
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}

// Start the application
monitorPayments().catch((error) => {
console.error("Failed during payment monitoring:", error);
});

Ejecutando tu pipeline de pagos con RPC

  1. Ejecuta el script de envío de pagos en una terminal. Déjalo en ejecución, enviará un pago cada 30 segundos.
Haz clic para ver el comando para ejecutar los pagos
# js
node send_payment.js

# java
# JRE 17 or higher should be installed - https://adoptium.net/temurin/releases/?version=17&package=jdk
# download Stellar Java SDK jar, latest release on maven central
JAR_REPO='https://repo1.maven.org/maven2/network/lightsail/stellar-sdk'
JAR_VERSION="2.0.0" # replace with latest version if needed
JAR_FILE="stellar-sdk-$JAR_VERSION-uber.jar"
curl -fsSL -o "$JAR_FILE" "$JAR_REPO/$JAR_VERSION/$JAR_FILE"

javac -cp "$JAR_FILE" SendPaymentExample.java
java -cp ".:$JAR_FILE" SendPaymentExample

# go
mkdir -p payments_example/sender
cd payments_example
go mod init payments_example
go get github.com/stellar/go@latest
go get github.com/stellar/stellar-rpc@latest
# save sender.go code to sender/sender.go file
go build -o sender sender/sender.go
./sender
  1. Ejecuta el script de monitoreo de pagos en una terminal separada.
Haz clic para ver el comando para ejecutar el monitor de pagos
# js
node monitor_payments.js

# java
javac -cp "$JAR_FILE" MonitorPaymentExample.java
java -cp ".:$JAR_FILE" MonitorPaymentExample

# go
mkdir -p payments_example/monitor
# save monitor.go code to monitor/monitor.go file
go build -o monitor monitor/monitor.go
./monitor
  1. Observa los eventos de pago a medida que son detectados y mostrados en la consola.

Resumen

  • Los pagos mediante operaciones de transacción o invocaciones de contratos en la red Stellar pueden ser monitoreados aprovechando el modelo unificado de eventos desde el Protocolo 23 (Whisk) y el método getEvents del RPC.

    • Los pagos son eventos 'transfer': el modelo unificado de eventos asegura que tanto si un pago ocurrió a través de una operación o una invocación de contrato, se emitirá el mismo evento 'transfer'.
    • Tipo de Evento: "contract" indica que los pagos son un evento a nivel de aplicación y no un evento 'sistema'.
    • Modelo de temas de transferencia: un arreglo de cuatro temas, especificados en CAP-67 y resumidos:
      • topic[0] = El nombre del evento 'transfer' determina los siguientes 3 temas
      • topic[1] = Dirección del emisor de la transferencia
      • topic[2] = Dirección del receptor de la transferencia
      • topic[3] = Identificador del activo (presente solo para activos Stellar, a través de sus contratos integrados (SAC))
  • RPC proporciona un filtrado preciso de eventos de la red Stellar, incluyendo el modelo de temas de evento definido para eventos unificados en CAP-67. Ahorras tiempo usando las capacidades de filtrado del servidor RPC para enfocarte solo en procesar los eventos relevantes para tu aplicación, como los pagos de una cuenta específica en este ejemplo.

    • Comodines: el método getEvents del RPC permite usar "*" para coincidir con cualquier valor en una posición de tema
    • Filtrado del lado del servidor: getEvents del RPC aplica los filtros y sólo devuelve eventos que coinciden, reduciendo el ancho de banda
    • Paginación basada en cursores: el método getEvents del RPC recupera los datos de eventos de forma eficiente con un mecanismo de paginación

Guías en esta categoría: