Skip to content

Please submit any typos you come across to GitHub issues.


Claimable Balance

Claimable Balances can be used to “split up” a payment into two parts, which allows the sending to only depend on the sending account, and the receipt to only depend on the receiving account. An account can initiate the “send” by creating a ClaimableBalanceEntry with Create Claimable Balance, and then that entry can be claimed by the claimants specified on the ClaimableBalanceEntry at a later time with Claim Claimable Balance.

Relevant operations

Create Claimable Balance

Parameters

  1. Asset - Asset that will be held in the ClaimableBalanceEntry in the form asset_code:issuing_address or native (for XLM).

  2. Amount - Amount of Asset stored in the ClaimableBalanceEntry.

  3. List of Claimants - A Claimant is an object that holds both the destination account that can claim the ClaimableBalanceEntry, and a ClaimPredicate that must evaluate to true for the claim to succeed. A ClaimPredicate is a recursive data structure that can be used to construct complex conditionals using different ClaimPredicateTypes. Here are some examples (The types have had the CLAIM_PREDICATE_ prefix removed for readability) -

    • Can claim at anytime - UNCONDITIONAL
    • Can claim if the close time of the ledger including the claim is before X seconds + the ledger close time in which the ClaimableBalanceEntry was created - BEFORE_RELATIVE_TIME(X)
    • Can claim if the close time of the ledger including the claim is before X (Unix timestamp) - BEFORE_ABSOLUTE_TIME(X)
    • Can claim if the close time of the ledger including the claim is at or after X seconds + the ledger close time in which the ClaimableBalanceEntry was created - NOT(BEFORE_RELATIVE_TIME(X))
    • Can claim if the close time of the ledger including the claim is at or after X (Unix timestamp) - NOT(BEFORE_ABSOLUTE_TIME(X))
    • Can claim between X and Y Unix timestamps (given X < Y) - AND(NOT(BEFORE_ABSOLUTE_TIME(X)), BEFORE_ABSOLUTE_TIME(Y))
    • Can claim outside X and Y Unix timestamps (given X < Y) - OR(BEFORE_ABSOLUTE_TIME(X), NOT(BEFORE_ABSOLUTE_TIME(Y))

Note that the SDKs expect the Unix timestamps to be expressed in seconds.

Operation Information

This operation will move Amount of Asset from the operation source account into a new ClaimableBalanceEntry.

Note that the baseReserve requirement for a ClaimableBalanceEntry is dependant on the number of Claimants. The minimum balance of the account will increase by # of Claimants * baseReserve.

BalanceID

A successful Create Claimable Balance operation will return a balanceID, which is the required parameter when actually claiming the newly-created entry via the Claim Claimable Balance operation, below. See ClaimableBalanceID for more information.

Claim Claimable Balance

Parameters

  1. BalanceID - The ID of the ClaimableBalanceEntry being claimed.

Operation Information

This operation will load the ClaimableBalanceEntry that corresponds to the BalanceID, and then search for the source account of this operation in the list of Claimants on the entry. If a match on the Claimant is found, and the ClaimPredicate evaluates to true, then the ClaimableBalanceEntry can be claimed. The balance on the entry will be moved to the source account if there are no limit or trustline issues (for non-native assets).

Once a ClaimableBalanceEntry has been claimed, it will be deleted.

Example

The below code demonstrates via both the JavaScript and Go SDKs how an account (“Account A”) can create a ClaimableBalanceEntry with two Claimants: Account A (itself) and “Account B” (another recipient).

Each of these accounts can only claim the balance under certain, individual conditions. Namely, Account B has a full minute to claim the balance, after which Account A can “reclaim” the balance back for itself.

It’s worth emphasizing that there is no “recovery” mechanism for a claimable balance in general: if none of the predicates can be fulfilled, the balance cannot be recovered. The “reclaim” paradigm below acts as a safety net for this situation.

const sdk = require("stellar-sdk");

async function main() {
  let server = new sdk.Server("https://horizon-testnet.stellar.org");

  let A = sdk.Keypair.fromSecret("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ");
  let B = sdk.Keypair.fromPublicKey("GAS4V4O2B7DW5T7IQRPEEVCRXMDZESKISR7DVIGKZQYYV3OSQ5SH5LVP");

  // NOTE: Proper error checks are omitted for brevity; always validate things!

  let aAccount = await server.loadAccount(A.publicKey()).catch(function (err) {
    console.error(`Failed to load ${A.publicKey()}: ${err}`)
  })
  if (!aAccount) { return }

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

  // Create the operation and submit it in a transaction.
  let claimableBalanceEntry = sdk.Operation.createClaimableBalance({
    claimants: [
      new sdk.Claimant(B.publicKey(), bCanClaim),
      new sdk.Claimant(A.publicKey(), aCanReclaim)
    ],
    asset: sdk.Asset.native(),
    amount: "420",
  });

  let tx = new sdk.TransactionBuilder(aAccount, {fee: sdk.BASE_FEE})
    .addOperation(claimableBalanceEntry)
    .setNetworkPassphrase(sdk.Networks.TESTNET)
    .setTimeout(180)
    .build();

  tx.sign(A);
  let txResponse = await server.submitTransaction(tx).then(function() {
    console.log("Claimable balance created!");
  }).catch(function (err) {
    console.error(`Tx submission failed: ${err}`)
  });
}

At this point, the ClaimableBalanceEntry exists in the ledger, but we’ll need its Balance ID to claim it. This can be acquired in a number of ways:

  1. the submitter of the entry (Account A in this case) can retrieve the balance ID prior to submitting the transaction;
  2. the submitter parses the XDR of the transaction result’s operations; or
  3. someone queries the list of claimable balances (filtered accordingly, if necessary).

Either party could also check the /effects of the transaction, query /claimable_balances with different filters, etc. Note that while (1) may be unavailable in some SDKs as its just a helper, the other methods are universal.

// Method 1: Not available in the JavaScript SDK yet.

// Method 2: Suppose `txResponse` comes from the transaction submission
// above.
let txResult = sdk.xdr.TransactionResult.fromXDR(
  txResponse.result_xdr, "base64");
let results = txResult.result().results();

// We look at the first result since our first (and only) operation
// in the transaction was the CreateClaimableBalanceOp.
let operationResult = results[0].value().createClaimableBalanceResult();
let balanceId = operationResult.balanceId().toXDR("hex");
console.log("Balance ID (2):", balanceId);

// Method 3: Account B could alternatively do something like:
let balances = await server
    .claimableBalances()
    .claimant(B.publicKey())
    .limit(1)       // there may be many in general
    .order("desc")  // so always get the latest one
    .call()
    .catch(function(err) {
      console.error(`Claimable balance retrieval failed: ${err}`)
    });
if (!balances) { return; }

balanceId = balances.records[0].id;
console.log("Balance ID (3):", balanceId);

With the claimable balance ID acquired, either Account B or A can actually submit a claim, depending on which predicate is fulfilled. We’ll assume here that a minute has passed, so Account A just reclaims the balance entry.

let claimBalance = sdk.Operation.claimClaimableBalance({ balanceId: balanceId });
console.log(A.publicKey(), "claiming", balanceId);

let tx = new sdk.TransactionBuilder(aAccount, {fee: sdk.BASE_FEE})
  .addOperation(claimBalance)
  .setNetworkPassphrase(sdk.Networks.TESTNET)
  .setTimeout(180)
  .build();

tx.sign(A);
await server.submitTransaction(tx).catch(function (err) {
  console.error(`Tx submission failed: ${err}`)
});

And that’s it! At this point, since we opted for the “reclaim” path, Account A should have the same balance as what it started with (sans fees), and Account B should be unchanged.

Last updated Jun. 10, 2021

Next Up: Clawbacks
Page Outline