Saltar al contenido principal

Saldos reclamables

Los saldos reclamables fueron introducidos en CAP-0023 y se usan para dividir un pago en dos partes.

  • Parte 1: la cuenta que envía crea un pago, o ClaimableBalanceEntry, usando la operación Create Claimable Balance
  • Parte 2: la(s) cuenta(s) destinataria(s), o reclamante(s), acepta(n) el ClaimableBalanceEntry usando la operación Claim Claimable Balance

Los saldos reclamables permiten que una cuenta envíe un pago a otra cuenta que no necesariamente está preparada para recibir el pago. Pueden usarse cuando envías un activo no nativo a una cuenta que aún no ha establecido una trustline, lo cual puede ser útil para anchors que integran nuevos usuarios. El reclamante debe establecer una trustline para el activo antes de poder reclamar el saldo reclamable; de lo contrario, la reclamación resultará en un error op_no_trust.

Es importante notar que si un saldo reclamable no es reclamado, permanece en el ledger para siempre, ocupando espacio y, en última instancia, haciendo que la red sea menos eficiente. Por esta razón, es buena idea poner una de tus propias cuentas como reclamante de un saldo reclamable. Así puedes aceptar tu propio saldo reclamable si es necesario, liberando espacio en la red.

Cada ClaimableBalanceEntry es una entrada del ledger, y cada reclamante en esa entrada incrementa el balance mínimo de la cuenta fuente en una reserva base.

Una vez que un ClaimableBalanceEntry ha sido reclamado, es eliminado.

Operaciones

Crear saldo reclamable

Para parámetros básicos, consulta la entrada Crear saldo reclamable en nuestra sección Lista de operaciones.

Parámetros adicionales

Claim_Predicate_ Reclamante — un objeto que contiene tanto la cuenta destino que puede reclamar el ClaimableBalanceEntry como un ClaimPredicate que debe evaluarse como verdadero para que la reclamación tenga éxito.

Un ClaimPredicate es una estructura de datos recursiva que puede usarse para construir condicionales complejos usando diferentes ClaimPredicateTypes. A continuación, algunos ejemplos con el prefijo Claim_Predicate_ removido para mayor legibilidad. Ten en cuenta que los SDKs esperan que las marcas de tiempo Unix estén expresadas en segundos.

  • Puede reclamar en cualquier momento - UNCONDITIONAL
  • Puede reclamar si el tiempo de cierre del ledger, incluyendo la reclamación, es antes de X segundos + el tiempo de cierre del ledger en el que se creó el ClaimableBalanceEntry - BEFORE_RELATIVE_TIME(X)
  • Puede reclamar si el tiempo de cierre del ledger incluyendo la reclamación es antes de X (marca de tiempo Unix) - BEFORE_ABSOLUTE_TIME(X)
  • Puede reclamar si el tiempo de cierre del ledger, incluyendo la reclamación, es en o después de X segundos + el tiempo de cierre del ledger en el que se creó el ClaimableBalanceEntry - NOT(BEFORE_RELATIVE_TIME(X))
  • Puede reclamar si el tiempo de cierre del ledger, incluyendo la reclamación, es en o después de X (marca de tiempo Unix) - NOT(BEFORE_ABSOLUTE_TIME(X))
  • Puede reclamar entre las marcas de tiempo Unix X e Y (dado que X < Y) - AND(NOT(BEFORE_ABSOLUTE_TIME(X)), BEFORE_ABSOLUTE_TIME(Y))
  • Puede reclamar fuera de las marcas de tiempo Unix X e Y (dado que X < Y) - OR(BEFORE_ABSOLUTE_TIME(X), NOT(BEFORE_ABSOLUTE_TIME(Y))

ClaimableBalanceID ClaimableBalanceID es una unión con un posible tipo (CLAIMABLE_BALANCE_ID_TYPE_V0). Contiene un hash SHA-256 del OperationID para Saldos Reclamables.

Una operación Crear saldo reclamable exitosa devolverá un ID de Balance, que es requerido al reclamar el ClaimableBalanceEntry con la operación Claim Claimable Balance.

Reclamar saldo reclamable

Para parámetros básicos, consulta la entrada Reclamar saldo reclamable en nuestra sección Lista de operaciones.

Esta operación cargará el ClaimableBalanceEntry que corresponde al Balance ID y luego buscará la cuenta fuente de esta operación en la lista de reclamantes de la entrada. Si se encuentra una coincidencia en el reclamante, y el ClaimPredicate evalúa a verdadero, entonces el ClaimableBalanceEntry puede ser reclamado. El saldo en la entrada será transferido a la cuenta fuente si no hay problemas de límite o trustline (para activos no nativos), lo que significa que el reclamante debe establecer una trustline para el activo antes de reclamarlo.

Recuperar saldo reclamable

Esta operación recupera un saldo reclamable, devolviendo el activo a la cuenta emisora, quemándolo. Debes recuperar el saldo reclamable completo, no solo una parte. Una vez que un saldo reclamable ha sido reclamado, usa la operación regular de recuperación para recuperarlo.

Las recuperaciones de saldos reclamables requieren el ID del saldo reclamable.

Aprende más sobre recuperaciones en nuestra Guía de recuperaciones.

Ejemplo

El código a continuación demuestra, mediante los SDKs de JavaScript y Go, cómo una cuenta (Cuenta A) crea un ClaimableBalanceEntry con dos reclamantes: Cuenta A (ella misma) y Cuenta B (otro destinatario).

Cada una de estas cuentas solo puede reclamar el saldo bajo condiciones únicas. La Cuenta B tiene un minuto completo para reclamar el saldo antes de que la Cuenta A pueda recuperarlo para sí misma.

Nota: en general no existe un mecanismo de recuperación para un saldo reclamable — si ninguno de los predicados puede cumplirse, el saldo no puede recuperarse. El ejemplo de recuperación a continuación funciona como una red de seguridad para esta situación.

Haz clic aquí para ver funciones auxiliares
func fundAccount(rpcClient *client.Client, address string) error {
ctx := context.Background()

// Use GetNetwork method from client
networkResp, err := rpcClient.GetNetwork(ctx)
if err != nil {
return err
}

if networkResp.FriendbotURL != "" {
friendbotURL := networkResp.FriendbotURL + "?addr=" + url.QueryEscape(address)
resp, err := http.Post(friendbotURL, "application/x-www-form-urlencoded", nil)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
return fmt.Errorf("friendbot failed with status: %d", resp.StatusCode)
}
return nil
}

return fmt.Errorf("friendbot not configured for network - %s", networkResp.Passphrase)
}

func panicIf(err error) {
if err != nil {
log.Fatal(err)
}
}
import * as StellarSdk from "@stellar/stellar-sdk";

/**
* Creates a claimable balance on Stellar testnet
* A claimable balance allows splitting a payment into two parts:
* 1. Sender creates the claimable balance
* 2. Recipient(s) can claim it later
*/
async function createClaimableBalance() {
// Connect to Stellar testnet RPC server
const server = new StellarSdk.rpc.Server(
"https://soroban-testnet.stellar.org",
);

const A = StellarSdk.Keypair.random();
const B = StellarSdk.Keypair.random();

console.log(
`Account A... public key: ${A.publicKey()}, secret: ${A.secret()}`,
);
console.log(
`Account B... public key: ${B.publicKey()}, secret: ${B.secret()}`,
);

try {
// Fund the source account using testnet's built-in airdrop
await server.requestAirdrop(A.publicKey());

// Load the funded account to get current sequence number
const aAccount = await server.getAccount(A.publicKey());
console.log(`Account sequence: ${aAccount.sequenceNumber()}`);

// Create a claimable balance with our two above-described conditions.
let soon = Math.ceil(Date.now() / 1000 + 60); // .now() is in ms
let bCanClaim = StellarSdk.Claimant.predicateBeforeRelativeTime("60");
let aCanReclaim = StellarSdk.Claimant.predicateNot(
StellarSdk.Claimant.predicateBeforeAbsoluteTime(soon.toString()),
);

// Create claimable balance operation
const claimableBalanceOp = StellarSdk.Operation.createClaimableBalance({
claimants: [
new StellarSdk.Claimant(B.publicKey(), bCanClaim),
new StellarSdk.Claimant(A.publicKey(), aCanReclaim),
],
asset: StellarSdk.Asset.native(),
amount: "420",
});

// Build the transaction
console.log(`Building transaction...`);
const transaction = new StellarSdk.TransactionBuilder(aAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(claimableBalanceOp)
.setTimeout(180)
.build();

/*
Claimable BalanceIds are predictable and can be derived from the Sha256 hash of the operation that creates them.
*/
const predictableBalanceId = transaction.getClaimableBalanceId(0);

// Sign the transaction with source account
transaction.sign(A);

// Submit transaction to the network
console.log(`Submitting transaction...`);
const response = await server.sendTransaction(transaction);

// Poll for transaction completion (RPC is asynchronous)
console.log(`Polling for result...`);
const finalResponse = await server.pollTransaction(response.hash);

if (finalResponse.status === "SUCCESS") {
// Extract claimable balance ID from transaction result
const txResult = finalResponse.resultXdr;
const results = txResult.result().results();
const operationResult = results[0].value().createClaimableBalanceResult();
const balanceId = operationResult.balanceId().toXDR("hex");

console.log(`Balance ID (from txResult): ${balanceId}`);
console.log(
`Predictable Balance ID (obtained before txSubmission): ${predictableBalanceId}`,
);
if (balanceId === predictableBalanceId) {
console.log(`Balance ID from txResult matches the predictable ID`);
} else {
console.log(
` Balance ID from txResult does NOT match the predictable ID`,
);
}
} else {
console.log(`Transaction failed: ${finalResponse.status}`);
}
} catch (error) {
console.error(`Error: ${error.message}`);
}
}

// Run the function
createClaimableBalance();

En este punto, el ClaimableBalanceEntry existe en el libro mayor, pero necesitaremos su ID de saldo para reclamarlo. Puedes llamar al endpoint getLedgerEntries del RPC para hacer esto.

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

// Replace with your actual Claimable Balance ID
// Format: 72 hex characters (includes ClaimableBalanceId type + hash)
const BALANCE_ID =
"00000000db1108ff108a807150d02b8672d9a8c0e808bff918cdbe5c7605e63a7f565df5";

/**
* Fetches and displays claimable balance details using Stellar RPC
*/
async function fetchClaimableBalance(balanceId) {
const server = new StellarSdk.rpc.Server(
"https://soroban-testnet.stellar.org",
);

try {
console.log(`Looking up balance ID: ${balanceId}`);

// Parse the claimable balance ID from hex XDR
const claimableBalanceId = StellarSdk.xdr.ClaimableBalanceId.fromXDR(
balanceId,
"hex",
);

// Create ledger key for the claimable balance entry
const ledgerKey = StellarSdk.xdr.LedgerKey.claimableBalance(
new StellarSdk.xdr.LedgerKeyClaimableBalance({
balanceId: claimableBalanceId,
}),
);

console.log(`Fetching from RPC server...`);

// Use SDK's getLedgerEntries method with XDR object array
const response = await server.getLedgerEntries(ledgerKey);

if (response.entries && response.entries.length > 0) {
const claimableBalance = response.entries[0].val.claimableBalance();

const asset = StellarSdk.Asset.fromOperation(claimableBalance.asset());

console.log(`Found claimable balance`);
console.log(`Amount: ${claimableBalance.amount().toString()}`);
console.log(`Asset: ${asset.toString()} `);
// Show claimant details
console.log(`\nClaimants:`);
claimableBalance.claimants().forEach((claimant, index) => {
const destination = claimant.v0().destination().ed25519();
console.log(
` ${index + 1}. ${StellarSdk.StrKey.encodeEd25519PublicKey(
destination,
)}`,
);
});
} else {
console.log(`Claimable balance not found`);
}
} catch (error) {
console.error(`Error: ${error.message}`);
}
}

fetchClaimableBalance(BALANCE_ID);

Con el ID de Claimable Balance obtenido, tanto la Cuenta B como la A pueden enviar una reclamación, dependiendo de qué predicado se cumpla. Aquí asumiremos que ha pasado un minuto, así que la Cuenta A simplemente reclama de nuevo la entrada de saldo.

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

// Replace with your claimable balance ID
const BALANCE_ID =
"0000000067a94da6c5d487fa09fc93c558ca91f6338413d3152d2a17771353f7c4111e11";

// Replace with the secret key of one of the claimants
const CLAIMANT_SECRET =
"SDJLAUDIHMDO6PAIVVVYH5IFIE5QMZOOBHO37NLF43335ULECK6EURVJ";

/**
* Claims a claimable balance
*/
async function claimClaimableBalance(balanceId, claimantSecret) {
const server = new StellarSdk.rpc.Server(
"https://soroban-testnet.stellar.org",
);

try {
console.log(`Claiming balance ID: ${balanceId}`);
// Create keypair from claimant's secret key
const claimantKeypair = StellarSdk.Keypair.fromSecret(claimantSecret);

// Load the claiming account
const claimantAccount = await server.getAccount(
claimantKeypair.publicKey(),
);

// Convert balance ID to proper format for the operation
const claimableBalanceId = StellarSdk.xdr.ClaimableBalanceId.fromXDR(
balanceId,
"hex",
);
const balanceIdHex = claimableBalanceId.toXDR("hex");

// Create claim operation
const claimOperation = StellarSdk.Operation.claimClaimableBalance({
balanceId: balanceIdHex,
});

// Build and sign transaction
const transaction = new StellarSdk.TransactionBuilder(claimantAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(claimOperation)
.setTimeout(180)
.build();

transaction.sign(claimantKeypair);

// Submit and poll for completion
const response = await server.sendTransaction(transaction);
const finalResponse = await server.pollTransaction(response.hash);

if (finalResponse.status === "SUCCESS") {
console.log(`Claimable balance claimed successfully`);
console.log(`Transaction hash: ${response.hash}`);
} else {
console.log(`Transaction failed: ${finalResponse.status}`);
}
} catch (error) {
console.error(`Error: ${error.message}`);
}
}

claimClaimableBalance(BALANCE_ID, CLAIMANT_SECRET);

¡Y eso es todo! Como optamos por la ruta de recuperación, la Cuenta A debería tener el mismo saldo con el que empezó (menos las tarifas), y la Cuenta B debería permanecer sin cambios.

Guías en esta categoría: