Claimable Balances
Claimable balances were introduced in CAP-0023 and are used to split a payment into two parts.
- Part 1: sending account creates a payment, or ClaimableBalanceEntry, using the Create Claimable Balance operation
- Part 2: destination account(s), or claimant(s), accepts the ClaimableBalanceEntry using the Claim Claimable Balance operation
Claimable balances allow an account to send a payment to another account that is not necessarily prepared to receive the payment. They can be used when you send a non-native asset to an account that has not yet established a trustline, which can be useful for anchors onboarding new users. A trustline must be established by the claimant to the asset before it can claim the claimable balance, otherwise, the claim will result in an op_no_trust
error.
It is important to note that if a claimable balance isn’t claimed, it sits on the ledger forever, taking up space and ultimately making the network less efficient. For this reason, it is a good idea to put one of your own accounts as a claimant for a claimable balance. Then you can accept your own claimable balance if needed, freeing up space on the network.
Each ClaimableBalanceEntry is a ledger entry, and each claimant in that entry increases the source account’s minimum balance by one base reserve.
Once a ClaimableBalanceEntry has been claimed, it is deleted.
Operations
Create Claimable Balance
For basic parameters, see the Create Claimable Balance entry in our List of Operations section.
Additional parameters
Claim_Predicate_
Claimant — 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. Below are some examples with the Claim_Predicate_
prefix removed for readability. Note that the SDKs expect the Unix timestamps to be expressed in seconds.
- Can claim at any time -
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))
ClaimableBalanceID
ClaimableBalanceID is a union with one possible type (CLAIMABLE_BALANCE_ID_TYPE_V0
). It contains an SHA-256 hash of the OperationID for Claimable Balances.
A successful Create Claimable Balance operation will return a Balance ID, which is required when claiming the ClaimableBalanceEntry with the Claim Claimable Balance operation.
Claim Claimable Balance
For basic parameters, see the Claim Claimable Balance entry in our List of Operations section.
This operation will load the ClaimableBalanceEntry that corresponds to the Balance ID 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), meaning the claimant must establish a trustline to the asset before claiming it.
Clawback Claimable Balance
This operation claws back a claimable balance, returning the asset to the issuer account, burning it. You must claw back the entire claimable balance, not just part of it. Once a claimable balance has been claimed, use the regular clawback operation to claw it back.
Clawback claimable balances require the claimable balance ID.
Learn more about clawbacks in our Clawback Encyclopedia Entry.
Example
The below code demonstrates via both the JavaScript and Go SDKs how an account (Account A) creates a ClaimableBalanceEntry with two claimants: Account A (itself) and Account B (another recipient).
Each of these accounts can only claim the balance under unique conditions. Account B has a full minute to claim the balance before Account A can reclaim the balance back for itself.
Note: 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 example below acts as a safety net for this situation.
- JavaScript
- Go
- Python
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}`);
});
}
package main
import (
"fmt"
"time"
sdk "github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/keypair"
"github.com/stellar/go/network"
"github.com/stellar/go/txnbuild"
)
func main() {
client := sdk.DefaultTestNetClient
// Suppose that these accounts exist and are funded accordingly:
A := "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"
B := "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5"
// Load the corresponding account for A.
aKeys := keypair.MustParseFull(A)
aAccount, err := client.AccountDetail(sdk.AccountRequest{
AccountID: aKeys.Address(),
})
check(err)
// Create a claimable balance with our two above-described conditions.
soon := time.Now().Add(time.Second * 60)
bCanClaim := txnbuild.BeforeRelativeTimePredicate(60)
aCanReclaim := txnbuild.NotPredicate(
txnbuild.BeforeAbsoluteTimePredicate(soon.Unix()),
)
claimants := []txnbuild.Claimant{
txnbuild.NewClaimant(B, &bCanClaim),
txnbuild.NewClaimant(aKeys.Address(), &aCanReclaim),
}
// Create the operation and submit it in a transaction.
claimableBalanceEntry := txnbuild.CreateClaimableBalance{
Destinations: claimants,
Asset: txnbuild.NativeAsset{},
Amount: "420",
}
// Build, sign, and submit the transaction
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: aAccount.AccountID,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
// Use a real timeout in production!
Timebounds: txnbuild.NewInfiniteTimeout(),
Operations: []txnbuild.Operation{&claimableBalanceEntry},
},
)
check(err)
tx, err = tx.Sign(network.TestNetworkPassphrase, aKeys)
check(err)
txResp, err := client.SubmitTransaction(tx)
check(err)
fmt.Println(txResp)
fmt.Println("Claimable balance created!")
}
import time
from stellar_sdk.xdr import TransactionResult, OperationType
from stellar_sdk.exceptions import NotFoundError, BadResponseError, BadRequestError
from stellar_sdk import (
Keypair,
Network,
Server,
TransactionBuilder,
Transaction,
Asset,
Operation,
Claimant,
ClaimPredicate,
CreateClaimableBalance,
ClaimClaimableBalance
)
server = Server("https://horizon-testnet.stellar.org")
A = Keypair.from_secret("SANRGB5VXZ52E7XDGH2BHVBFZR4S25AUQ4BR7SFXIQYT5J6W2OES2OP7")
B = Keypair.from_public_key("GAAPSRMYNFAO3TDQTLNLKN76IQ3E6IQAKU23PSQX3BIV7RTEBXHQIWU6")
# NOTE: Proper error checks are omitted for brevity; always validate things!
try:
aAccount = server.load_account(A.public_key)
except NotFoundError:
raise Exception(f"Failed to load {A.public_key}")
# Create a claimable balance with our two above-described conditions.
soon = int(time.time() + 60)
bCanClaim = ClaimPredicate.predicate_before_relative_time(60)
aCanClaim = ClaimPredicate.predicate_not(
ClaimPredicate.predicate_before_absolute_time(soon)
)
# Create the operation and submit it in a transaction.
claimableBalanceEntry = CreateClaimableBalance(
asset = Asset.native(),
amount = "420",
claimants = [
Claimant(destination = B.public_key, predicate = bCanClaim),
Claimant(destination = A.public_key, predicate = aCanClaim)
]
)
tx = (
TransactionBuilder (
source_account = aAccount,
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE,
base_fee = server.fetch_base_fee()
)
.append_operation(claimableBalanceEntry)
.set_timeout(180)
.build()
)
tx.sign(A)
try:
txResponse = server.submit_transaction(tx)
print("Claimable balance created!")
except (BadRequestError, BadResponseError) as err:
print(f"Tx submission failed: {err}")
At this point, the ClaimableBalanceEntry
exists in the ledger, but we’ll need its Balance ID to claim it, which can be done in several ways:
- The submitter of the entry (Account A in this case) can retrieve the Balance ID before submitting the transaction;
- The submitter parses the XDR of the transaction result’s operations; or
- Someone queries the list of claimable balances.
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 it's just a helper, the other methods are universal.
- JavaScript
- Go
- Python
// Method 1: Suppose `txResponse` comes from the transaction submission
// above on testnet.
const builder = sdk.TransactionBuilder(
txResponse.envelope_xdr,
Networks.TESTNET,
);
console.log("Balance ID (1):", builder.build().getClaimableBalanceId(0));
// Method 2:
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);
// Method 1: Suppose `tx` comes from the transaction built above.
// Notice that this can be done *before* submission.
balanceId, err := tx.ClaimableBalanceID(0)
check(err)
// Method 2: Suppose `txResp` comes from the transaction submission above.
var txResult xdr.TransactionResult
err = xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult)
check(err)
if results, ok := txResult.OperationResults(); ok {
// We look at the first result since our first (and only) operation in the
// transaction was the CreateClaimableBalanceOp.
operationResult := results[0].MustTr().CreateClaimableBalanceResult
balanceId, err := xdr.MarshalHex(operationResult.BalanceId)
check(err)
fmt.Println("Balance ID:", balanceId)
}
// Method 3: Account B could alternatively do something like:
balances, err := client.ClaimableBalances(sdk.ClaimableBalanceRequest{Claimant: B})
check(err)
balanceId := balances.Embedded.Records[0].BalanceID
# Method 1: Not available in the Python SDK yet.
# Method 2: Suppose `txResponse` comes from the transaction submission
# above.
txResult = TransactionResult.from_xdr(txResponse["result_xdr"])
results = txResult.result.results
# We look at the first result since our first (and only) operation
# in the transaction was the CreateClaimableBalanceOp.
operationResult = results[0].tr.create_claimable_balance_result
balanceId = operationResult.balance_id.to_xdr_bytes().hex()
print(f"Balance ID (2): {balanceId}")
# Method 3: Account B could alternatively do something like:
try:
balances = (
server
.claimable_balances()
.for_claimant(B.public_key)
.limit(1)
.order(desc = True)
.call()
)
except (BadRequestError, BadResponseError) as err:
print(f"Claimable balance retrieval failed: {err}")
balanceId = balances["_embedded"]["records"][0]["id"]
print(f"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.
- JavaScript
- Go
- Python
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}`);
});
claimBalance := txnbuild.ClaimClaimableBalance{BalanceID: balanceId}
tx, err = txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: aAccount.AccountID, // or Account B, depending on the condition!
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
Timebounds: txnbuild.NewInfiniteTimeout(),
Operations: []txnbuild.Operation{&claimBalance},
},
)
check(err)
tx, err = tx.Sign(network.TestNetworkPassphrase, aKeys)
check(err)
txResp, err = client.SubmitTransaction(tx)
check(err)
claimBalance = ClaimClaimableBalance(balance_id = balanceId)
print(f"{A.public_key} claiming {balanceId}")
tx = (
TransactionBuilder (
source_account = aAccount,
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE,
base_fee = server.fetch_base_fee()
)
.append_operation(claimBalance)
.set_timeout(180)
.build()
)
tx.sign(A)
try:
txResponse = server.submit_transaction(tx)
except (BadRequestError, BadResponseError) as err:
print(f"Tx submission failed: {err}")
And that’s it! Since we opted for the reclaim path, Account A should have the same balance as what it started with (minus fees), and Account B should be unchanged.