From ae1d3bf05b27df25189494e7565f05c62bedf438 Mon Sep 17 00:00:00 2001 From: Bri <92327786+briwylde08@users.noreply.github.com> Date: Tue, 30 Aug 2022 10:13:26 -0600 Subject: [PATCH] Added code blocks --- docs/encyclopedia/claimable-balances.mdx | 362 ++++++++++++++++++++++- 1 file changed, 358 insertions(+), 4 deletions(-) diff --git a/docs/encyclopedia/claimable-balances.mdx b/docs/encyclopedia/claimable-balances.mdx index 95df2a552..ae4eb2517 100644 --- a/docs/encyclopedia/claimable-balances.mdx +++ b/docs/encyclopedia/claimable-balances.mdx @@ -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 @@ -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] + + +```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}") +``` + + + 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: @@ -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] + + +```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}") +``` + + + 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] + + +```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}") +``` + + + -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. \ No newline at end of file +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.