Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added code blocks #14

Merged
merged 1 commit into from
Aug 30, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 358 additions & 4 deletions docs/encyclopedia/claimable-balances.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: Claimable Balances
---

import { CodeExample } from "@site/src/components/CodeExample"

Claimable balances were introduced in [CAP-0023](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0023.md) 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
Expand Down Expand Up @@ -57,7 +59,202 @@ Each of these accounts can only claim the balance under unique conditions. Accou

**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.

[insert code]
<CodeExample>

```js
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}`);
});
}
```

```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"
)


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!")
}
```

```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
)

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}")
```

</CodeExample>


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:

Expand All @@ -67,10 +264,167 @@ At this point, the `ClaimableBalanceEntry` exists in the ledger, but we’ll nee

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.

[insert code]
<CodeExample>

```js
// 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);
```

```go
// 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
```

```python
# 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}")
```

</CodeExample>


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.

[insert code]
<CodeExample>

```js
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}`);
});
```


```go
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)
```

```python
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}")
```

</CodeExample>


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, Account A should have the same balance as what it started with (minus fees), and Account B should be unchanged.