diff --git a/docs/build/guides/transactions/claimable-balances.mdx b/docs/build/guides/transactions/claimable-balances.mdx index f3623127c..a88d5fd11 100644 --- a/docs/build/guides/transactions/claimable-balances.mdx +++ b/docs/build/guides/transactions/claimable-balances.mdx @@ -1,19 +1,23 @@ --- -title: Claimable balances +title: Claimable Balances description: Split a payment into two parts by creating a claimable balance. sidebar_position: 20 --- import Details from "@theme/Details"; -Claimable balances were introduced in [CAP-23](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0023.md) and are used to split a payment into two parts. +[CAP-23](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0023.md) introduced claimable balances 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 +1. Sending account creates a payment, or ClaimableBalanceEntry, using the Create Claimable Balance operation. +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. +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 is 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. +:::note[Claimant Permanence] + +Unclaimed claimable balances sit on the ledger forever, taking up space and ultimately making the network less efficient. Thus, it is best practice to put one of your own accounts as a claimant (assuming no issuer clawbacks). Then you can accept your own claimable balance if needed, freeing up space and [account reserves](./sponsored-reserves.mdx#effect-on-claimable-balances). + +::: Each ClaimableBalanceEntry is a ledger entry, and each claimant in that entry increases the source account’s minimum balance by one base reserve. @@ -23,83 +27,80 @@ Once a ClaimableBalanceEntry has been claimed, it is deleted. ### Create Claimable Balance -For basic parameters, see the Create Claimable Balance entry in our [List of Operations section](../../../learn/fundamentals/transactions/list-of-operations.mdx#create-claimable-balance). - -#### 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. +For basic parameters, see the Create Claimable Balance entry in our [List of Operations section](../../../learn/fundamentals/transactions/list-of-operations.mdx#create-claimable-balance). It includes the asset held in the Claimable BalanceEntry, its amount, and a `Claimants` list. Each claimant object holds both the recipient destination account and a predicate 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. +#### Other Parameters -- 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. +- **`ClaimPredicate`**: 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. + - `UNCONDITIONAL`: Can claim at any time. + - `BEFORE_RELATIVE_TIME(X)`: Can claim if the close time of the ledger including the claim is before X seconds, plus 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 seconds, plus the ledger close time in which the ClaimableBalanceEntry was created. + - `BEFORE_ABSOLUTE_TIME(X)`: Can claim if the close time of the ledger including the claim is before X (Unix timestamp). + - `NOT( BEFORE_ABSOLUTE_TIME(X) )`: Can claim if the close time of the ledger including the claim is at or after X (Unix timestamp). + - `AND[ NOT( BEFORE_ABSOLUTE_TIME(X) )`, `BEFORE_ABSOLUTE_TIME(Y) ]`: Can claim between X and Y Unix timestamps (given X < Y). + - `OR[ BEFORE_ABSOLUTE_TIME(X)`, `NOT( BEFORE_ABSOLUTE_TIME(Y) ) ]`: Can claim outside X and Y Unix timestamps (given X < Y). +- **`ClaimableBalanceID`**: ClaimableBalanceID is a union with one possible type (`CLAIMABLE_BALANCE_ID_TYPE_V0`). This one type's only item is a SHA-256 hash of the source account, sequence number, and operation index in an XDR discriminator unit. Its `StrKey` representation is a 58-character string beginning with `B`, such as `BAAD6DBUX6J22DMZOHIEZTEQ64CVCHEDRKWZONFEUL5Q26QD7R76RGR4TU`. +- **`ClientBalanceID`**: Hex of `ClaimableBalanceID` returned after a successful `CreateClaimableBalance` operation. The `ClaimClaimableBalance` operation uses this (with zero-padding to 72 characters) to claim the `ClaimableBalanceEntry`. ### Claim Claimable Balance For basic parameters, see the Claim Claimable Balance entry in our [List of Operations section](../../../learn/fundamentals/transactions/list-of-operations#claim-claimable-balance). -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. +This operation will load the `ClaimableBalanceEntry` that corresponds to the `ClientBalanceID` 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. +[This operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#clawback-claimable-balance) 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. +You clawback a claimable balances with its `ClientBalanceID`. Learn more about clawbacks in our [Clawback Guide](./clawbacks.mdx). ## 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). +The below code demonstrates how an account (Account $\mathcal{A}$) creates a ClaimableBalanceEntry with two claimants: $\mathcal{A}$ (itself) and Account $\mathcal{B}$ (another recipient). + +### Setup -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. +Each of these accounts can only claim the balance under unique conditions. $\mathcal{B}$ has a full minute to claim the balance before $\mathcal{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. +The reclaim logic acts as a safety net if none of the predicates can be fulfilled. Otherwise the transaction could render the asset unusable forever. -
+
```go func fundAccount(rpcClient *client.Client, address string) error { - ctx := context.Background() + ctx := context.Background() - // Use GetNetwork method from client - networkResp, err := rpcClient.GetNetwork(ctx) - if err != nil { - return err - } + // 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 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 - } + 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) + return fmt.Errorf("friendbot not configured for network - %s", networkResp.Passphrase) } func panicIf(err error) { - if err != nil { - log.Fatal(err) - } + if err != nil { + log.Fatal(err) + } } ``` @@ -109,282 +110,344 @@ func panicIf(err error) { +```python +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 +) + + var txResult xdr.TransactionResult + err := xdr.SafeUnmarshalBase64(resp.ResultXDR, &txResult) + if err != nil { + return "", err + } + + if results, ok := txResult.OperationResults(); ok && len(results) > 0 { + operationResult := results[0].MustTr().CreateClaimableBalanceResult + return xdr.MarshalHex(operationResult.BalanceId) + } + +try: + aAccount = server.load_account(A.public_key) +except NotFoundError: + raise Exception(f"Failed to load account") + +# Create a claimable balance with our two above-described conditions. +bCanClaim = ClaimPredicate.predicate_before_relative_time(60) + +soon = int(time.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 = "64", + claimants = [ + Claimant( + destination = B.public_key, + predicate = bCanClaim + ), + Claimant( + destination = A.public_key, + predicate = aCanClaim + ) + ] +) + +transaction = ( + TransactionBuilder ( + source_account = aAccount, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = Network.MIN_BASE_FEE + ) + .append_operation(claimableBalanceEntry) + .set_timeout(180) + .build() +) + +transaction.sign(A) +try: + txResponse = server.submit_transaction(transaction) + print("Claimable balance created!") +except (BadRequestError, BadResponseError) as err: + print(f"Tx submission failed: {err}") +``` + ```js -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 sdk = require("stellar-sdk"); - const A = StellarSdk.Keypair.random(); - const B = StellarSdk.Keypair.random(); +async function main() { + let server = new sdk.Server("https://horizon-testnet.stellar.org"); - console.log( - `Account A... public key: ${A.publicKey()}, secret: ${A.secret()}`, + let A = sdk.Keypair.fromSecret( + "SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ", ); - console.log( - `Account B... public key: ${B.publicKey()}, secret: ${B.secret()}`, + let B = sdk.Keypair.fromPublicKey( + "GAS4V4O2B7DW5T7IQRPEEVCRXMDZESKISR7DVIGKZQYYV3OSQ5SH5LVP", ); + let aAccount; try { - // Fund the source account using testnet's built-in airdrop - await server.requestAirdrop(A.publicKey()); + aAccount = await server.loadAccount(A.publicKey()); + } catch (err) { + console.error(`Failed to load ${A.publicKey()}: ${err}`); + return; + } - // 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 = sdk.Claimant.predicateBeforeRelativeTime("60"); + let aCanReclaim = sdk.Claimant.predicateNot( + sdk.Claimant.predicateBeforeAbsoluteTime(soon.toString()), + ); - // 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()), - ); + let claimableBalanceEntry = sdk.Operation.createClaimableBalance({ + claimants: [ + new sdk.Claimant(B.publicKey(), bCanClaim), + new sdk.Claimant(A.publicKey(), aCanReclaim), + ], + asset: sdk.Asset.native(), + amount: "64", + }); - // 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", - }); + let tx = new sdk.TransactionBuilder(aAccount, { fee: sdk.BASE_FEE }) + .addOperation(claimableBalanceEntry) + .setNetworkPassphrase(sdk.Networks.TESTNET) + .setTimeout(180) + .build(); - // 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(); + tx.sign(A); - /* - 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}`); + try { + let txResponse = await server.submitTransaction(tx); + console.log("Claimable balance created!"); + } catch (err) { + console.error(`Tx submission failed: ${err}`); } } -// Run the function -createClaimableBalance(); +main(); ``` -```go -package main - -import ( - "context" - "fmt" - "log" - "net/http" - "net/url" - "time" +```java +import org.stellar.sdk.*; +import org.stellar.sdk.requests.RequestBuilder; +import org.stellar.sdk.responses.AccountResponse; +import org.stellar.sdk.responses.SubmitTransactionResponse; - client "github.com/stellar/go-stellar-sdk/clients/rpcclient" - protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" +import java.util.ArrayList; +import java.util.List; - "github.com/stellar/go-stellar-sdk/keypair" - "github.com/stellar/go-stellar-sdk/network" - "github.com/stellar/go-stellar-sdk/txnbuild" - "github.com/stellar/go-stellar-sdk/xdr" -) +public class StellarClaimableBalance { + public static void main(String[] args) { + Network.useTestNetwork(); + Server server = new Server("https://horizon-testnet.stellar.org"); -func main() { - // Create RPC client - rpcClient := client.NewClient("https://soroban-testnet.stellar.org", nil) - defer rpcClient.Close() + KeyPair aKeypair = KeyPair.fromSecretSeed( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" + ); + String bPublicKey = "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5"; - // Generate random keypairs - A := keypair.MustRandom() - B := keypair.MustRandom() + AccountResponse aAccount; + try { + aAccount = server.accounts().account(aKeypair.getAccountId()); + } catch (Exception e) { + throw new RuntimeException("Failed to load account"); + } - fmt.Printf("Account A: public key: %s, secret key: %s\n", A.Address(), A.Seed()) - fmt.Printf("Account B: public key: %s\n", B.Address()) + // Create a claimable balance with our two above-described conditions. + long soon = System.currentTimeMillis() / 1000L + 60; + ClaimPredicate bCanClaim = ClaimPredicate.BeforeRelativeTime(60L); + ClaimPredicate aCanReclaim = ClaimPredicate.Not( + ClaimPredicate.BeforeAbsoluteTime(soon) + ); - // Fund account using GetNetwork + friendbot - fmt.Println("\nFunding account...") - panicIf(fundAccount(rpcClient, A.Address())) - fmt.Println("Account funded") + List claimants = new ArrayList<>(); + claimants.add(new Claimant(bPublicKey, bCanClaim)); + claimants.add(new Claimant(aKeypair.getAccountId(), aCanReclaim)); - // Wait for funding - time.Sleep(3 * time.Second) + CreateClaimableBalanceOperation entryCB = new CreateClaimableBalanceOperation.Builder( + AssetTypeNative.INSTANCE, "64", claimants + ).build(); - // Use LoadAccount method from the client - ctx := context.Background() - sourceAccount, err := rpcClient.LoadAccount(ctx, A.Address()) - panicIf(err) + // Build, sign, and submit the transaction + Transaction transaction = new Transaction.Builder(aAccount, Network.TESTNET) + .addOperation(entryCB) + .setBaseFee(Transaction.MIN_BASE_FEE) + .setTimeout(180) + .build(); - // 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()), - ) + transaction.sign(aKeypair); - // Create claimable balance operation - claimableBalanceOp := txnbuild.CreateClaimableBalance{ - Destinations: []txnbuild.Claimant{ - txnbuild.NewClaimant(B.Address(), &bCanClaim), - txnbuild.NewClaimant(A.Address(), &aCanReclaim), - }, - Asset: txnbuild.NativeAsset{}, - Amount: "1", - } + try { + SubmitTransactionResponse response = server.submitTransaction(transaction); + System.out.println(response); + System.out.println("Claimable balance created!"); + } catch (Exception e) { + throw new RuntimeException("Failed to submit transaction"); + } + } +} +``` - // Build transaction - tx, err := txnbuild.NewTransaction( - txnbuild.TransactionParams{ - SourceAccount: sourceAccount, - IncrementSequenceNum: true, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()}, - Operations: []txnbuild.Operation{&claimableBalanceOp}, - }, - ) - panicIf(err) +```go +package main - // Sign transaction - tx, err = tx.Sign(network.TestNetworkPassphrase, A) - panicIf(err) +import ( + "fmt" + "time" - // Get transaction XDR - txXDR, err := tx.Base64() - panicIf(err) + client "github.com/stellar/go-stellar-sdk/clients/rpcclient" + protocol "github.com/stellar/go-stellar-sdk/protocols/rpc" - // Submit using RPC client's SendTransaction method - fmt.Println("Submitting transaction...") - sendResp, err := rpcClient.SendTransaction(ctx, protocol.SendTransactionRequest{ - Transaction: txXDR, - }) - panicIf(err) + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/network" + "github.com/stellar/go-stellar-sdk/txnbuild" + "github.com/stellar/go-stellar-sdk/xdr" +) - if sendResp.Status != "PENDING" { - log.Fatalf("Transaction not pending: %s", sendResp.Status) - } +func main() { + client := sdk.DefaultTestNetClient - fmt.Printf("Transaction submitted: %s\n", sendResp.Hash) + aKeys := keypair.MustParseFull( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" + ) + B := "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5" - // Poll using RPC client's GetTransaction method - fmt.Println("Polling for result...") - for i := 0; i < 10; i++ { - resp, err := rpcClient.GetTransaction(ctx, protocol.GetTransactionRequest{ - Hash: sendResp.Hash, - }) - if err != nil { - log.Printf("Error getting transaction: %v", err) - time.Sleep(1 * time.Second) - continue - } + aAccount, err := client.AccountDetail( + sdk.AccountRequest{ + AccountID: aKeys.Address(), + } + ) + if err != nil { + panic("Failed to load account A") + } - if resp.Status != protocol.TransactionStatusNotFound { - if resp.Status == protocol.TransactionStatusSuccess { - // Extract balance ID - balanceID, err := extractBalanceID(&resp) - if err != nil { - log.Printf("Error extracting balance ID: %v", err) - } else { - fmt.Println("\nSUCCESS: Claimable balance created") - fmt.Printf("Balance ID: %s\n", balanceID) - } - } else { - fmt.Printf("Transaction failed: %s\n", resp.Status) - } - return - } + // 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), + } - time.Sleep(time.Duration(i+1) * time.Second) - } + claimableBalanceEntry := txnbuild.CreateClaimableBalance{ + Destinations: claimants, + Asset: txnbuild.NativeAsset{}, + Amount: "64", + } - fmt.Println("Transaction polling timeout") + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: aAccount.AccountID, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewTimeout(180), + Operations: []txnbuild.Operation{&claimableBalanceEntry}, + }, + ) + if err != nil { + panic("Failed to build transaction") + } + tx, err = tx.Sign(network.TestNetworkPassphrase, aKeys) + if err != nil { + panic("Failed to sign transaction") + } + txResponse, err := client.SubmitTransaction(tx) + if err != nil { + panic("Failed to submit transaction") + } + fmt.Println("Claimable balance created", txResponse) } +``` -func extractBalanceID(resp *protocol.GetTransactionResponse) (string, error) { - if resp.ResultXDR == "" { - return "", fmt.Errorf("no result XDR") - } - - var txResult xdr.TransactionResult - err := xdr.SafeUnmarshalBase64(resp.ResultXDR, &txResult) - if err != nil { - return "", err - } + - if results, ok := txResult.OperationResults(); ok && len(results) > 0 { - operationResult := results[0].MustTr().CreateClaimableBalanceResult - return xdr.MarshalHex(operationResult.BalanceId) - } +### Retrieval - return "", fmt.Errorf("no operation results") -} -``` +At this point, the `ClaimableBalanceEntry` exists in the ledger, but we’ll need its client balance ID to claim it, which can be done in several ways: - +1. The submitter of the entry ($\mathcal{A}$) can retrieve the client balance ID before submitting the transaction. +2. The submitter parses the XDR of the transaction result’s operations. +3. Someone queries the list of claimable balances. -At this point, the `ClaimableBalanceEntry` exists in the ledger, but we’ll need its Balance ID to claim it. You can call the RPC's [`getLedgerEntries`](../../../data/apis/rpc/api-reference/methods/getLedgerEntries.mdx) endpoint to do this. +Either party could also check the [`/effects`](../../../data/apis/horizon/api-reference/resources/effects/README.mdx) of the transaction or query [`/claimable_balances`](../../../data/apis/horizon/api-reference/resources/claimablebalances/README.mdx) with different filters in Horizon. Note that while (1) may be unavailable in some SDKs, as it’s just a helper, the other methods are universal. +```python +# Method 1: Suppose `tx` comes from the transaction built above. +# Notice that this can be done *before* submission. +# Use zero for `CreateClaimableBalance` first op. +clientBalanceID = tx.get_claimable_balance_id(0) +print(f"Balance ID (1): {clientBalanceID}") + +# 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 +clientBalanceID = operationResult.balance_id.to_xdr_bytes().hex() +print(f"Balance ID (2): {clientBalanceID}") + +# 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}") + +clientBalanceID = balances["_embedded"]["records"][0]["id"] +print(f"Balance ID (3): {clientBalanceID}") +``` + ```js -import * as StellarSdk from "@stellar/stellar-sdk"; +// Method 1: Suppose `tx` comes from the transaction built above. +// Notice that this can be done *before* submission. +// Use zero for `CreateClaimableBalance` first op. +let clientBalanceID = tx.getClaimableBalanceId(0); +console.log("Balance ID (1):", clientBalanceID); // 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", - ); +// 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 clientBalanceID = operationResult.balanceId().toXDR("hex"); +console.log("Balance ID (2):", clientBalanceID); try { console.log(`Looking up balance ID: ${balanceId}`); @@ -433,11 +496,58 @@ async function fetchClaimableBalance(balanceId) { } } -fetchClaimableBalance(BALANCE_ID); +clientBalanceID = balances.records[0].id; +console.log("Balance ID (3):", clientBalanceID); +``` + +```java +// Method 1: Suppose `tx` comes from the transaction built above. +// Notice that this can be done *before* submission. +// Use zero for `CreateClaimableBalance` first op. +String clientBalanceID = tx.getClaimableBalanceId(0) +System.out.println("Balance ID (1): " + clientBalanceID); + +// Method 2: Suppose txResponse comes from the transaction submission above. +String txResponseResultXdr = txResponse.getResultXdr().get(); +try { + TransactionResult txResult = TransactionResult.decode( + TransactionResult.class, + Util.fromBase64( + txResponseResultXdr + ) + ); + OperationResult operationResult = txResult.getResult().getResults()[0]; + XdrDataInputStream xdrDataInputStream = new XdrDataInputStream(Util.fromBase64(txResponseResultXdr)); + TransactionResult result = TransactionResult.decode(xdrDataInputStream); + + CreateClaimableBalanceResult createClaimableBalanceResult = operationResult.getTr().getCreateClaimableBalanceResult(); + String clientBalanceID = Util.bytesToHex(createClaimableBalanceResult.getBalanceId().toXdrByteArray()); + System.out.println("Balance ID (2): " + clientBalanceID); +} catch (IOException e) { + e.printStackTrace(); +} + +// Method 3: Account B could alternatively do something like: +try { + Page balances = server.claimableBalances().forClaimant( + B.getAccountId() + ).limit(1).order(RequestBuilder.Order.DESC).execute(); + if (balances.getRecords().size() > 0) { + String clientBalanceID = balances.getRecords().get(0).getId(); + System.out.println("Balance ID (3): " + clientBalanceID); + } +} catch (IOException e) { + System.out.println("Claimable balance retrieval failed: " + e.getMessage()); +} ``` ```go -package main +// Method 1: Suppose `tx` comes from the transaction built above. +// Notice that this can be done *before* submission. +// Use zero for `CreateClaimableBalance` first op. +clientBalanceID, err := tx.ClaimableBalanceID(0) +check(err) +fmt.Println("Balance ID (1):", clientBalanceID) import ( "context" @@ -512,78 +622,71 @@ func main() { -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. - - +### Claiming -```js -import * as StellarSdk from "@stellar/stellar-sdk"; +With the client balance ID acquired, either $\mathcal{B}$ or $\mathcal{A}$ can actually submit a claim, depending on which predicate is fulfilled. We’ll assume here that a minute has passed, so $\mathcal{A}$ just reclaims the balance entry. -// 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(), - ); +```python +tx = ( + TransactionBuilder( + source_account = aAccount, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = server.fetch_base_fee() + ) + .append_operation( + ClaimClaimableBalance( + balance_id = clientBalanceID + ) + ) + .set_timeout(180) + .build() +) - // Convert balance ID to proper format for the operation - const claimableBalanceId = StellarSdk.xdr.ClaimableBalanceId.fromXDR( - balanceId, - "hex", - ); - const balanceIdHex = claimableBalanceId.toXDR("hex"); +tx.sign(A) +try: + txResponse = server.submit_transaction(tx) + print(f"{A.public_key} claimed {clientBalanceID}") +except (BadRequestError, BadResponseError) as err: + print(f"Tx submission failed: {err}") +``` - // Create claim operation - const claimOperation = StellarSdk.Operation.claimClaimableBalance({ - balanceId: balanceIdHex, +```js +let tx = new sdk.TransactionBuilder(aAccount, { fee: sdk.BASE_FEE }) + .addOperation( + sdk.Operation.claimClaimableBalance({ + balanceId: clientBalanceID, }); + ) + .setNetworkPassphrase(sdk.Networks.TESTNET) + .setTimeout(180) + .build(); + +tx.sign(A); +await server.submitTransaction(tx).catch(function (err) { + console.error(`Tx submission failed: ${err}`); +}); +console.log(A.publicKey(), "claimed", clientBalanceID); +``` - // 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}`); - } +```java +Transaction tx = new Transaction.Builder(aAccount, Network.TESTNET) + .addOperation( + new ClaimClaimableBalanceOperation.Builder(clientBalanceID).build(); + ) + .setBaseFee(tx.MIN_BASE_FEE) + .setTimeout(180) + .build(); + +tx.sign(A); + +try { + SubmitTransactionResponse response = server.submitTransaction(tx); + System.out.println(A.getAccountId() + " claimed " + clientBalanceID); +} catch (Exception e) { + System.err.println("Tx submission failed: " + e.getMessage()); } - -claimClaimableBalance(BALANCE_ID, CLAIMANT_SECRET); ``` ```go @@ -699,4 +802,4 @@ func main() { -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. +And that’s it! Since we opted for the reclaim path, $\mathcal{A}$ should have the same balance as what it started with (minus fees), and $\mathcal{B}$ should be unchanged.