Saltar al contenido principal

Enviar y recibir pagos

La mayor parte del tiempo, estarás enviando dinero a alguien más que tiene su propia cuenta. Sin embargo, para este tutorial, necesitarás una segunda cuenta para realizar transacciones. Así que antes de continuar, sigue los pasos descritos en Crear una Cuenta para hacer dos cuentas: una para enviar y una para recibir. También usaremos tanto las operaciones de pago como las invocaciones de contratos inteligentes para realizar pagos. Para más detalles sobre los diferentes tipos de transacciones, consulta la documentación de Operaciones y Transacciones.

Acerca de Operaciones y Transacciones

Hay dos formas de realizar pagos en Stellar. Una opción es usar las operaciones relacionadas con pagos de Stellar, y la otra es invocar una función en el contrato del token; la opción que deberías tomar depende del caso de uso.

Si quieres hacer un pago de un activo Stellar entre dos cuentas Stellar, utiliza las operaciones de pago. Las tarifas de transacción son más económicas cuando usas estas operaciones en comparación con las que invocan el contrato de activo Stellar (o SAC).

Si quieres hacer un pago de un activo Stellar entre una cuenta Stellar y una dirección de contrato, o entre dos direcciones de contrato, debes usar el SAC. Las operaciones relacionadas con pagos de Stellar no pueden tener direcciones de contrato como origen o destino.

Finalmente, si quieres hacer un pago de un token de contrato (no un activo Stellar), debes usar el contrato del token. Las operaciones de pago de Stellar solo pueden usarse para transferir activos Stellar.

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

Además, hay dos opciones principales para interactuar con la red Stellar: la API Horizon y Stellar RPC. En general, deberías usar RPC salvo que necesites características que solo Horizon provea. Si tu caso de uso se limita a hacer pagos, RPC debería ser suficiente. De todas formas, cada ejemplo de pago se desglosará para mostrar cómo realizar dichos pagos con cualquiera de las dos opciones.

información

En los siguientes ejemplos de código, se omite la verificación de errores adecuada por brevedad. Sin embargo, debes siempre validar tus resultados, ya que hay muchas formas en que las solicitudes pueden fallar. Debes consultar la guía sobre Manejo de Errores para obtener consejos sobre estrategias de gestión de errores.

Usando la Operación de Pago

Enviar un Pago

Al enviar pagos en Stellar, construyes una transacción con una operación de pago, la firmas y la envías a la red. Stellar almacena y comunica los datos de transacciones en un formato binario llamado XDR, que está optimizado para el rendimiento de la red pero no es legible para el ojo humano. Por suerte, la API Horizon, Stellar RPC y los SDKs de Stellar convierten los XDRs en formatos más amigables. Así es como podrías enviar 10 lumens a una cuenta:

Usando Stellar RPC

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;
}

Las diferencias clave al usar RPC en lugar de Horizon:

  1. Envío instantáneo: rpc.sendTransaction() devuelve inmediatamente con un estado "PENDING"
  2. Requiere sondeo activo: Debes sondear con rpc.pollTransaction() para obtener el resultado final
  3. Configuración del sondeo: Puedes personalizar los intervalos del sondeo y los intentos de reintento
  4. Manejo de estado: Verifica los estados SUCCESS, FAILED o NOT_FOUND en el resultado final

Usando la API Horizon

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);
}

Desglose paso a paso

¿Qué exactamente sucedió allí? Desglosemoslo.

  1. Confirma que la ID de cuenta (también conocida como la clave pública) a la que envías realmente existe cargando los datos de la cuenta asociada desde la red Stellar. Está bien omitir este paso, pero te da la oportunidad de evitar hacer una transacción que inevitablemente fallará.
// 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;
}
  1. Carga los datos de la cuenta desde la que estás enviando. Una cuenta solo puede realizar una transacción a la vez y tiene algo llamado un número de secuencia, que ayuda a Stellar a verificar el orden de las transacciones. El número de secuencia de una transacción debe coincidir con el número de secuencia de la cuenta, así que necesitas obtener el número de secuencia actual de la cuenta de la red.
// 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 */

El SDK incrementará automáticamente el número de secuencia de la cuenta cuando construyas una transacción, así que no necesitarás recuperar esta información nuevamente si deseas realizar una segunda transacción.

  1. Empieza a construir una transacción. Esto requiere un objeto de cuenta, no solo una ID de cuenta, porque incrementará el número de secuencia de la cuenta.
const transaction = new StellarSdk.TransactionBuilder(sourceAccount);
  1. Agrega la operación de pago a la cuenta. Ten en cuenta que necesitas especificar el tipo de activo que estás enviando: la moneda de la red Stellar es el lumen, pero puedes enviar cualquier activo emitido en la red. Cubriré el envío de activos no lumen abajo. Por ahora, sin embargo, nos mantendremos en lumens, que se llaman activos “nativos” en el SDK:
.addOperation(StellarSdk.Operation.payment({
destination: destinationId,
asset: StellarSdk.Asset.native(),
amount: "10"
}))

También debes notar que la cantidad es una cadena en lugar de un número. Al trabajar con fracciones extremadamente pequeñas o grandes valores, la matemática de punto flotante puede introducir pequeñas inexactitudes. Dado que no todos los sistemas tienen una forma nativa de representar con precisión decimales extremadamente pequeños o grandes, Stellar utiliza cadenas como una forma confiable de representar la cantidad exacta en cualquier sistema.

  1. Opcionalmente, puedes agregar tus propios metadatos, llamados memo, a una transacción. Stellar no hace nada con estos datos, pero puedes usarlos para cualquier propósito que desees. Muchos exchanges requieren memos para transacciones entrantes porque utilizan una sola cuenta Stellar para todos sus usuarios y dependen del memo para diferenciar entre cuentas internas de usuarios.
.addMemo(StellarSdk.Memo.text('Test Transaction'))
  1. Ahora que la transacción tiene todos los datos que necesita, debes firmarla criptográficamente utilizando tu clave secreta. Esto demuestra que los datos realmente provinieron de ti y no de alguien que te imita.
transaction.sign(sourceKeys);
  1. Y finalmente, ¡envíala a la red Stellar!
// 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);
}

En este ejemplo, estamos enviando la transacción a la instancia pública de Horizon mantenida por SDF (la API Stellar) o a un servidor Stellar RPC. Al enviar transacciones a un servidor Horizon o RPC — que es lo que la mayoría hace — es posible que no recibas respuesta del servidor debido a bugs, condiciones de red, etc. En tal situación, es imposible determinar el estado de tu transacción.

Por eso deberías siempre guardar una transacción construida (o una transacción codificada en formato XDR) en una variable o base de datos y reenviarla si no conoces su estado. Si la transacción ya se aplicó exitosamente al ledger, tanto Horizon como RPC devolverán simplemente el resultado guardado y no intentarán enviar la transacción otra vez. Solo en casos donde el estado de la transacción es desconocido (y por ende tiene posibilidad de ser incluida en un ledger) se realizará un reenvío a la red.

Recibir un Pago

En realidad, no necesitas hacer nada para recibir pagos en una cuenta Stellar: si un pagador realiza una transacción exitosa para enviarte activos, esos activos se agregarán automáticamente a tu cuenta.

Sin embargo, es posible que desees estar atento a los pagos entrantes. Un programa simple que vigila la red en busca de pagos e imprime cada uno podría verse así:

Usando Stellar RPC

información

Con el próximo lanzamiento del Protocolo 23, los pagos nativos para activos también emitirán eventos de contratos inteligentes, lo que permitirá usar Stellar RPC para transmitir pagos de activos nativos. El proceso será similar al ejemplo bajo la sección Contrato de Activo Stellar (SAC) -> Recibir un Pago. Por ahora, puedes usar la API Horizon para transmitir pagos de activos nativos.

Usando Horizon

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
}

Hay dos partes principales en este programa. Primero, creas una consulta para pagos que involucran una cuenta dada. Como la mayoría de las consultas en Stellar, esto podría devolver un gran número de elementos, así que la API devuelve tokens de paginación, que puedes usar más tarde para comenzar tu consulta desde el mismo punto donde la dejaste anteriormente. En el ejemplo anterior, las funciones para guardar y cargar tokens de paginación se dejaron en blanco, pero en una aplicación real, querrías guardar los tokens de paginación en un archivo o base de datos para poder continuar donde te quedaste en caso de que el programa se bloquee o el usuario lo cierre.

var payments = server.payments().forAccount(accountId);
var lastToken = loadLastPagingToken();
if (lastToken) {
payments.cursor(lastToken);
}

En segundo lugar, los resultados de la consulta se transmiten. Esta es la forma más fácil de vigilar los pagos u otras transacciones. Cada pago existente se envía a través de la transmisión, uno por uno. Una vez que se han enviado todos los pagos existentes, la transmisión se mantiene abierta y se envían nuevos pagos a medida que se realizan.

Pruébalo: Ejecuta este programa y luego, en otra ventana, crea y envía un pago. Deberías ver que este programa registra el pago.

payments.stream({
onmessage: function (payment) {
// handle a payment
},
});

También puedes solicitar pagos en grupos o páginas. Una vez que hayas procesado cada página de pagos, necesitarás solicitar la siguiente hasta que no quede ninguna.

payments.call().then(function handlePage(paymentsPage) {
paymentsPage.records.forEach(function (payment) {
// handle a payment
});
return paymentsPage.next().then(handlePage);
});

Pagos con Stellar Asset Contract (SAC)

Al enviar pagos usando Stellar Asset Contracts, invocas funciones de contratos inteligentes en lugar de usar operaciones de pago. Los pagos SAC utilizan la operación invokeHostFunction para llamar métodos del contrato como transfer, que proporciona la misma funcionalidad de pago pero mediante lógica de contrato. Este enfoque ofrece flexibilidad y programabilidad adicionales mientras mantiene compatibilidad con el sistema de activos de Stellar.

En el ejemplo siguiente, usamos SAC como interfaz de contrato inteligente para el activo nativo, por lo que el resultado final es el mismo que si se hubiera ejecutado una operación de pago nativa. Sin embargo, el mismo proceso puede aplicarse para cualquier transferencia de contrato inteligente que involucre tokens de contrato estandarizados que sigan la interfaz del token de contrato. Esto convierte a los pagos SAC en un enfoque unificado para manejar tanto activos nativos como tokens de contrato mediante una interfaz consistente basada en contratos.

Así es como podrías enviar 10 lumens usando un SAC:

información

Para usar el ejemplo de RPC a continuación, primero debes generar las bindings del contrato para que el cliente pueda usarse apropiadamente. Esto puede lograrse mediante la CLI Stellar.

Ejemplo: Generar las bindings de typescript para el contrato sac de un activo dado:

 stellar contract bindings typescript --network=testnet --contract-id=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC --output-dir=./bindings

Enviar un Pago

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;
}

Desglose paso a paso

¿Qué ocurrió exactamente en el ejemplo de pago SAC? Vamos a desglosarlo:

  1. Importa el cliente generado del contrato desde las bindings producidas por la CLI. Este cliente provee métodos tipados para interactuar con Stellar Asset Contract.
import { Client } from "sac";
  1. Inicializa el cliente SAC con detalles de conexión y una función personalizada de firma. El cliente necesita el endpoint RPC, la frase de red, el ID del contrato para el activo y la clave pública de tu cuenta. Se puede proporcionar una función de firma para manejar automáticamente la firma de transacciones al invocar métodos del contrato con este cliente.
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(),
};
},
});
  1. Inicia la transferencia llamando al método transfer del contrato. Esto devuelve una transacción ensamblada lista para firmar y enviar.
xlmClient.transfer({
to: destinationId,
amount: BigInt(10_0000000), // Amount in stroops (1 XLM = 10,000,000 stroops)
from: sourceKeys.publicKey(),
});
  1. Firma y envía la transacción usando signAndSend(). El cliente gestiona la simulación, la firma (usando la función provista), el envío a la red y el sondeo del resultado final.
try {
const result = await assembledTx.signAndSend();
console.log("Transaction successful:", result.getTransactionResponse?.txHash);
} catch (error) {
console.error("Error during transaction processing:", error);
throw error;
}

Recibir un Pago

No necesitas hacer nada especial para recibir pagos SAC en una cuenta Stellar: si un pagador realiza una transferencia exitosa mediante contrato, esos activos se añadirán automáticamente a tu cuenta.

Sin embargo, puede que quieras monitorear los pagos SAC entrantes. Como los pagos SAC generan eventos de contrato en lugar de operaciones de pago tradicionales, necesitarás observar eventos del contrato en vez de usar flujos de pagos de Horizon. Aquí se muestra cómo puedes monitorear transferencias de contratos inteligentes entrantes:

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();

Desglose paso a paso

¿Qué ocurrió exactamente en el ejemplo de recepción de pago SAC? Vamos a desglosarlo:

  1. Configura la conexión RPC y obtén el ID del contrato para el activo que quieres monitorear. Para XLM nativo, usamos el ID de contrato incorporado.
const rpc = new Server("https://soroban-testnet.stellar.org");
const contractId = Asset.native().contractId(Networks.TESTNET);
const monitoredAddress = new Address(
"GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5",
);
  1. Inicializa el sistema de sondeo obteniendo la última secuencia del ledger como punto de partida para monitorear eventos.
if (!lastLedgerStart) {
const latestLedger = await rpc.getLatestLedger();
lastLedgerStart = latestLedger.sequence;
}
  1. Consulta eventos de contratos usando filtros específicos para eventos de transferencia. El filtro apunta al ID del contrato y escucha eventos de "transfer" donde tu dirección es la destinataria. El arreglo topics define la estructura del evento: el primer elemento es el nombre del evento ("transfer"), el segundo es el emisor (comodín "" para cualquier), el tercero es el receptor (tu dirección monitoreada), y el cuarto es el activo (comodín "" para cualquier activo).
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,
});
  1. Procesa los eventos extrayendo detalles de transferencia de los topics y valores del evento, luego continúa sondeando nuevos eventos después de una breve espera.
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);

Diferencias clave con el monitoreo clásico de pagos:

  • Basado en eventos: Monitorea eventos de contratos en lugar de operaciones de pago
  • Solo RPC: Usa exclusivamente endpoints RPC, sin soporte Horizon
  • Filtrado por topics: Usa topics de eventos para filtrar patrones específicos de transferencia
  • Sondeo activo: Requiere sondeo continuo en vez de transmisión
  • Estructura de eventos: Extrae datos de pagos desde topics y valores de eventos de contrato en lugar de campos de operación

Transaccionar en Otras Monedas

Una de las cosas sorprendentes de la red Stellar es que puedes crear, mantener, enviar, recibir e intercambiar cualquier tipo de activo. Muchas organizaciones emiten activos en Stellar que representan monedas del mundo real, como dólares estadounidenses o nairas nigerianas, o criptomonedas como bitcoin o ether.

Cada uno de estos activos redimibles — anclado en la jerga Stellar — es esencialmente un crédito emitido por una cuenta particular que representa las reservas que esas cuentas tienen fuera de la red. Por eso los activos en el ejemplo anterior tenían tanto un código como un emisor: el emisor es la clave pública de la cuenta que creó el activo, una cuenta propiedad de la organización que finalmente honra el crédito que representa ese activo. Además, con la introducción de contratos inteligentes, los activos clásicos pueden también ser representados por un contract id (Interfaz SAC)) que permite interacciones más complejas e interoperabilidad con aplicaciones de contratos inteligentes.

Otros tokens personalizados, desarrollados completamente con contratos inteligentes, también son representados por su propio contract id y deberían ofrecer una interfaz similar para la interacción al seguir la interfaz de token.