Skip to content

These new docs are in beta. Please submit bugs 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))

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 uses the Go SDK to demonstrate 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.

Go
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"
    "github.com/stellar/go/xdr"
)

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(),
    })

    // 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,
            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("Claimable balance created!")
}

At this point, the claimable balance exists in the ledger. We can now claim it by its Balance ID. This can be acquired in two ways:

  1. the submitter of the entry (Account A in this case) parses the XDR of the transaction result’s operations, or
  2. someone queries the list of claimable balances (filtering accordingly, if necessary)
Go
// 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 {
    operationResult := results[0].MustTr().CreateClaimableBalanceResult
    balanceId, err := xdr.MarshalHex(operationResult.BalanceId)
    fmt.Println("Balance ID:", balanceId)
}

// Alternatively, Account B would do something like:
balances, err := client.ClaimableBalances(sdk.ClaimableBalanceRequest{Claimant: B})
check(err)
balanceId := balances.Embedded.Records[0].BalanceID

// Now Account B can actually claim the balance, provided too much time hasn't elapsed!
claimBalance := txnbuild.ClaimClaimableBalance{BalanceID: balanceId}
tx, err = txnbuild.NewTransaction(
    txnbuild.TransactionParams{
        SourceAccount:        &aAccount, // 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)
err = client.SubmitTransaction(tx)
check(err)

Last updated Oct. 15, 2020

Page Outline