diff --git a/.husky/pre-commit b/.husky/pre-commit index 45f8b30339..1f36600c02 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,33 @@ +#!/bin/bash + +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + +if ! nvm use 22 >/dev/null 2>&1; then + echo "⚠️ Node v22 not installed. Skipping formatting check." + exit 0 +fi + G='\033[0;32m' P='\033[0;35m' CLEAN='\033[0;0m' -yarn run check:mdx || (echo -e "${G}Hint:${CLEAN} execute ${P}yarn run format:mdx${CLEAN} to format files" && exit 1) +if command -v yarn >/dev/null 2>&1; then + CHECK_OUTPUT=$(yarn run check:mdx 2>&1) + STATUS=$? + + if echo "$CHECK_OUTPUT" | grep -q 'The engine "node" is incompatible with this module'; then + echo -e "${G}Warning:${CLEAN} Node version mismatch. Skipping formatting check." + exit 0 + fi + + if [ "$STATUS" -ne 0 ]; then + echo "$CHECK_OUTPUT" + echo -e "${G}Hint:${CLEAN} Run ${P}yarn run format:mdx${CLEAN} to fix formatting." + exit 1 + fi + +else + echo -e "${G}Hint:${CLEAN} Yarn not installed. Skipping formatting check." + exit 0 +fi \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js index 1fa0597766..e89fae43d3 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,12 +1,10 @@ module.exports = { ...require("@stellar/prettier-config"), - // This is mostly content, and prose wrap has a way of exploding markdown - // diffs. Override the default for a better experience. overrides: [ { files: "*.mdx", options: { - proseWrap: "never", + proseWrap: "never", // Minimize `mdx` diffs with simpler content lines }, }, ], diff --git a/docs/build/apps/wallet/sep24.mdx b/docs/build/apps/wallet/sep24.mdx index 88e4eab8bf..2a40e75b04 100644 --- a/docs/build/apps/wallet/sep24.mdx +++ b/docs/build/apps/wallet/sep24.mdx @@ -11,7 +11,7 @@ import Header from "./component/header.mdx";
-The [SEP-24] standard defines the standard way for anchors and wallets to interact on behalf of users. Wallets use this standard to facilitate exchanges between on-chain assets (such as stablecoins) and off-chain assets (such as fiat, or other network assets such as BTC). +The [SEP-24] standard defines the standard way for anchors and wallets to interact on behalf of users. Wallets use this standard to facilitate exchanges between on-chain assets (such as stablecoins) and off-chain assets (such as fiat or other crypto networks). During the flow, a wallet makes several requests to the anchor, and finally receives an interactive URL to open in iframe. This URL is used by the user to provide an input (such as KYC) directly to the anchor. Finally, the wallet can fetch transaction information using query endpoints. diff --git a/docs/build/apps/wallet/sep6.mdx b/docs/build/apps/wallet/sep6.mdx index 88d596564b..ecc3d0ee70 100644 --- a/docs/build/apps/wallet/sep6.mdx +++ b/docs/build/apps/wallet/sep6.mdx @@ -10,7 +10,7 @@ import Header from "./component/header.mdx";
-The [SEP-6] standard defines a way for anchors and wallets to interact on behalf of users. Wallets use this standard to facilitate exchanges between on-chain assets (such as stablecoins) and off-chain assets (such as fiat, or other network assets such as BTC). +The [SEP-6] standard defines a way for anchors and wallets to interact on behalf of users. Wallets use this standard to facilitate exchanges between on-chain assets (such as stablecoins) and off-chain assets (such as fiat or other crypto networks). Please note, this is for _programmatic_ deposits and withdrawals. For hosted deposits and withdrawals, where the anchor interacts with wallets interactively using a popup, please see [Hosted Deposit and Withdrawal](./sep24.mdx). diff --git a/docs/build/guides/storage/use-temporary.mdx b/docs/build/guides/storage/use-temporary.mdx index f27dbbc77a..2324e7ad76 100644 --- a/docs/build/guides/storage/use-temporary.mdx +++ b/docs/build/guides/storage/use-temporary.mdx @@ -4,21 +4,21 @@ hide_table_of_contents: true description: Temporary storage is useful for a contract to store data that can quickly become irrelevant or out-dated --- -Temporary storage is useful for a contract to store data that can quickly become irrelevant or out-dated. For example, here's how a contract might be used to store a recent price of BTC against the US Dollar. +Temporary storage is useful for a contract to store data that can quickly become irrelevant or out-dated. For example, here's how a contract might be used to store a recent price of AstroPesos against AstroDollars. ```rust -// This function updates the BTC price -pub fn update_btc_price(env: Env, price: i128) { - env.storage().temporary().set(&!symbol_short("BTC"), &price); +// This function updates the PEN price +pub fn update_pen_price(env: Env, price: i128) { + env.storage().temporary().set(&!symbol_short("PEN"), &price); } -// This function reads and returns the current BTC price (zero if the storage +// This function reads and returns the current PEN price (zero if the storage // entry is archived) -pub fn get_btc_price(env: Env) -> i128 { - if let Some(price) = env.storage().temporary().get(&!symbol_short("BTC")) { - price - } else { - 0 - } +pub fn get_pen_price(env: Env) -> i128 { + if let Some(price) = env.storage().temporary().get(&!symbol_short("PEN")) { + price + } else { + 0 + } } ``` diff --git a/docs/build/guides/tokens/README.mdx b/docs/build/guides/tokens/README.mdx index fd7e60edb5..ddff1ba5da 100644 --- a/docs/build/guides/tokens/README.mdx +++ b/docs/build/guides/tokens/README.mdx @@ -1,5 +1,5 @@ --- -title: Stellar Asset Contract (SAC) Tokens +title: Stellar Asset Contract Tokens description: Tokens and Stellar Asset Contract (SAC) hide_table_of_contents: true sidebar_class_name: sidebar-category-items-hidden diff --git a/docs/build/guides/tokens/stellar-asset-contract.mdx b/docs/build/guides/tokens/stellar-asset-contract.mdx index 9e8aafa6d6..5792bea837 100644 --- a/docs/build/guides/tokens/stellar-asset-contract.mdx +++ b/docs/build/guides/tokens/stellar-asset-contract.mdx @@ -57,7 +57,7 @@ client.mint(...); ## Testing -Soroban Rust SDK provides an easy way to instantiate a Stellar Asset Contract tokens using `register_stellar_asset_contract_v2`. This function can be seen as the deployment of a generic token. It also allows you to manipulate flags on the issuer account like `AUTH_REVOCABLE` and `AUTH_REQUIRED`. In the following example, we are following the best practices outlined in the [Issuing and Distribution Accounts section](../../../tokens/control-asset-access.mdx#issuing-and-distribution-accounts): +Soroban Rust SDK provides an easy way to instantiate a Stellar Asset Contract tokens using `register_stellar_asset_contract_v2`. This function can be seen as the deployment of a generic token. It also allows you to manipulate flags on the issuer account like `AUTH_REVOCABLE` and `AUTH_REQUIRED`. In the following example, we are following the best practices outlined in the [Issuing and Distribution Accounts section](../../../tokens/control-asset-access.mdx#issuer-and-distributor-accounts): ```rust #![cfg(test)] diff --git a/docs/build/guides/transactions/channel-accounts.mdx b/docs/build/guides/transactions/channel-accounts.mdx index cb981e0d53..4801289e00 100644 --- a/docs/build/guides/transactions/channel-accounts.mdx +++ b/docs/build/guides/transactions/channel-accounts.mdx @@ -1,75 +1,986 @@ --- -title: Channel accounts +title: Channel Accounts description: Create channel accounts to submit transactions to the network at a high rate. sidebar_position: 10 --- -Channel accounts provide a method for submitting transactions to the network at a high rate. +Channel accounts are a design pattern for submitting transactions to the network in bursts. Channel accounts are not what might come to mind in terms of layer-2 channels. Rather, they are a set of specialized accounts that act as proxies to submit transactions quickly. -An account’s transactions always need to be submitted to the network in increments of one sequence number (unless minimum sequence number preconditions are set). This can cause problems if you are submitting transactions at a high rate, as they can potentially reach Stellar Core out of order and will then bounce with a bad sequence error. +:::caution -To avoid this, you can create separate channel accounts that can be used as the source account for the transaction and use the account holding the assets as the base account or the source account for the individual operations in the transaction. In this scenario, the assets will come out of the base account, and the sequence number and fees will be consumed by the channel account. +Submitting transactions quickly [often requires](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md) hot signing keys. It is best practice to extensively test your systems on [the testnet](../../../networks/README.mdx#network-comparison) before deploying in high-speed production. Moreover, you may consider using hardware security modules or [multisignature wallets](../../../learn/fundamentals/transactions/signatures-multisig.mdx#thresholds) to secure your accounts. -Channels take advantage of the fact that the source account of a transaction can be different than the source account of the operations inside the transaction. With this setup, you can make as many channels as you need to maintain your desired transaction rate. +::: -You will, of course, have to sign the transaction with both the base account key and the channel account key. +## Background Motivation -For example: +Channel accounts take advantage of the fact that the [source account](../../../learn/glossary.mdx#source-account) of a [transaction](../../../data/apis/horizon/api-reference/resources/transactions/README.mdx) ($S_T$) can be different than the source account of the [operations](../../../data/apis/horizon/api-reference/resources/operations/README.mdx) inside a transaction ($S_O$). + +![Separate Sources](/assets/channel-accounts/layering.png) + + + +:::info Packet Propagation + +Stellar validators are spread across the globe, making it challenging to guarantee immediate order of arrival from one source. Only by sequencing [a validator](../../../validators/README.mdx) can you control physical delays for your transactions, as signals vary in network lag to reach [other nodes](../../../data/apis/horizon/admin-guide/ingestion.mdx). Accordingly, channel accounts are the only way to guarantee layer-1 settlement for many consecutive transactions. + +::: + +This guide walks through an example using channel accounts to send 500 payment operations in close ledgers. It uses a primary account ($A_P$) to hold [lumens](../../../learn/fundamentals/lumens.mdx) and five channel accounts for submitting transactions ($A_{C_{1\text{--}5}}$). + +![Asset Allocation](/assets/channel-accounts/custody.png) + +### Sequence Numbers + +The network rejects transactions with [sequence numbers](../../../learn/glossary.mdx#sequence-number) that are not strictly increasing. Previously, if you sent even just two transactions in the same ledger, there was a [reasonable chance](https://stellar.stackexchange.com/questions/1675/channel-concept-in-stellar) they would arrive out of sequence. Accordingly, to prevent failures, the network [now restricts](https://stellar.org/blog/developers/proposed-changes-to-transaction-submission#:~:text=1,ledger) each source account to submit no more than one transaction per ledger. -```js -// channelAccounts[] is an array of accountIDs, one for each channel -// channelKeys[] is an array of secret keys, one for each channel -// channelIndex is the channel you want to send this transaction over - -// create payment from baseAccount to customerAddress -var transaction = new StellarSdk.TransactionBuilder( - channelAccounts[channelIndex], - { - fee: StellarSdk.BASE_FEE, - networkPassphrase: StellarSdk.Networks.TESTNET, - }, +```python +from stellar_sdk import Asset, Keypair, Network, Server, TransactionBuilder + +# While you might know where this Horizon instance should be, +# You'd need to manually delay transmission to control order. +server = Server("https://horizon-testnet.stellar.org") + +secret_key = "SDY5TRQSEUSHS7UX26QMNMZ4X543UWZQPJZ7LQQUA3NAKDFVYTAWAB74" +source_keypair = Keypair.from_secret(secret_key) +source_account = server.load_account(source_keypair.public_key) + +transaction1 = TransactionBuilder( + source_account = source_account, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = 100 ) - .addOperation( - StellarSdk.Operation.payment({ - source: baseAccount.address(), - destination: customerAddress, - asset: StellarSdk.Asset.native(), - amount: amountToSend, - }), + +transaction2 = TransactionBuilder( + source_account = source_account, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = 100 +) + +# Initialize arrays of 200 recipient public keys +first100Users = [...] +second100Users = [...] + +# Attempting to send 200 operations at once from a single source +for n in range(100): + transaction1.append_payment_op( + destination = first100Users[n], + amount = "10", + asset = Asset.native() ) - // Wait a maximum of three minutes for the transaction - .setTimeout(180) - .build(); + transaction2.append_payment_op( + destination = second100Users[n], + amount = "10", + asset = Asset.native() + ) + +transaction1 = transaction1.set_timeout(3600).build() +transaction1.sign(sourceKeypair) + +transaction2 = transaction2.set_timeout(3600).build() +transaction2.sign(sourceKeypair) -transaction.sign(baseAccountKey); // base account must sign to approve the payment -transaction.sign(channelKeys[channelIndex]); // channel must sign to approve it being the source of the transaction +# Likely to fail due to misordered sequence numbers at validator +server.submit_transaction(transaction1) + +# Adding a delay here or checking for transaction1 confirmation lets you +# account for network latency and submission timing in slower use cases. +server.submit_transaction(transaction2) ``` +```js +const { + Server, + Keypair, + TransactionBuilder, + Networks, + Asset, +} = require("stellar-sdk"); + +const server = new Server('https://horizon-testnet.stellar.org'); + +const secretKey = 'SDY5TRQSEUSHS7UX26QMNMZ4X543UWZQPJZ7LQQUA3NAKDFVYTAWAB74'; +const sourceKeypair = Keypair.fromSecret(secretKey); +const sourceAccount = await server.loadAccount(sourceKeypair.publicKey()); + +const transaction1 = new TransactionBuilder(sourceAccount, { + networkPassphrase: Networks.TESTNET, + fee: 100 +}); + +const transaction2 = new TransactionBuilder(sourceAccount, { + networkPassphrase: Networks.TESTNET, + fee: 100 +}); + +// Initialize arrays of 200 recipient public keys +const first100Users = [...]; +const second100Users = [...]; + +// Attempting to send 200 operations at once from a single source +for (let n = 0; n < 100; n++) { + transaction1.addOperation(StellarSdk.Operation.payment({ + destination: first100Users[n], + asset: Asset.native(), + amount: "10" + })); + transaction2.addOperation(StellarSdk.Operation.payment({ + destination: second100Users[n], + asset: Asset.native(), + amount: "10" + })); +} + +const builtTransaction1 = transaction1.setTimeout(3600).build(); +builtTransaction1.sign(sourceKeypair); + +const builtTransaction2 = transaction2.setTimeout(3600).build(); +builtTransaction2.sign(sourceKeypair); + +// Submit transactions +await server.submitTransaction(builtTransaction1); +// Add a delay or confirmation check for builtTransaction1 here or face conflicts +await server.submitTransaction(builtTransaction2); +``` + +```java +import java.util.List; +import org.stellar.sdk.*; +import org.stellar.sdk.requests.Server; +import org.stellar.sdk.responses.AccountResponse; + +public static void main(String[] args) { + try { + Server server = new Server("https://horizon-testnet.stellar.org"); + + String secretKey = "SDY5TRQSEUSHS7UX26QMNMZ4X543UWZQPJZ7LQQUA3NAKDFVYTAWAB74"; + KeyPair sourceKeypair = KeyPair.fromSecretSeed(secretKey); + AccountResponse sourceAccount = server.accounts().account(sourceKeypair.getAccountId()); + + Transaction.Builder transactionBuilder1 = new Transaction.Builder(sourceAccount, Network.TESTNET) + .setBaseFee(100).set_timeout(3600); + + Transaction.Builder transactionBuilder2 = new Transaction.Builder(sourceAccount, Network.TESTNET) + .setBaseFee(100).set_timeout(3600); + + // Initialize arrays of 200 recipient public keys + List first100Users = List.of(...); + List second100Users = List.of(...); + + // Attempting to send 200 operations at once from a single source + for (int n = 0; n < 100; n++) { + transactionBuilder1.addOperation( + new PaymentOperation.Builder( + first100Users.get(n), + AssetTypeNative.INSTANCE, "10" + ).build()); + transactionBuilder2.addOperation( + new PaymentOperation.Builder( + second100Users.get(n), + AssetTypeNative.INSTANCE, "10" + ).build()); + } + + Transaction transaction1 = transactionBuilder1.build(); + transaction1.sign(sourceKeypair); + + Transaction transaction2 = transactionBuilder2.build(); + transaction2.sign(sourceKeypair); + + // Submit transactions + server.submitTransaction(transaction1); + // Add a delay or confirmation check for transaction1 here or face conflicts + server.submitTransaction(transaction2); + } catch (Exception e) { + e.printStackTrace(); + } +} +``` + +```go +package main + +import ( + "github.com/stellar/go-stellar-sdk/clients/horizonclient" + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/network" + "github.com/stellar/go-stellar-sdk/protocols/horizon" + "github.com/stellar/go-stellar-sdk/txnbuild" +) + +func main() { + server := horizonclient.DefaultTestNetClient + + secretKey := "SDY5TRQSEUSHS7UX26QMNMZ4X543UWZQPJZ7LQQUA3NAKDFVYTAWAB74" + sourceKeypair, _ := keypair.ParseFull(secretKey) + sourceAccount, _ := server.AccountDetail(horizonclient.AccountRequest{ + AccountID: sourceKeypair.Address(), + }) + + txParams := txnbuild.TransactionParams{ + SourceAccount: &sourceAccount, + IncrementSequenceNum: true, + BaseFee: 100, + Network: network.TestNetworkPassphrase, + } + + transaction1, _ := txnbuild.NewTransaction(txParams) + transaction2, _ := txnbuild.NewTransaction(txParams) + + // Initialize arrays of 200 recipient public keys + first100Users := []string{...} + second100Users := []string{...} + + // Attempting to send 200 operations at once from a single source + for n := 0; n < 100; n++ { + paymentOp1 := txnbuild.Payment{ + Destination: first100Users[n], + Amount: "10", + Asset: txnbuild.NativeAsset{}, + } + transaction1.Operations = append(transaction1.Operations, &paymentOp1) + + paymentOp2 := txnbuild.Payment{ + Destination: second100Users[n], + Amount: "10", + Asset: txnbuild.NativeAsset{}, + } + transaction2.Operations = append(transaction2.Operations, &paymentOp2) + } + + tx1, _ := transaction1.BuildSignEncode(sourceKeypair) + tx2, _ := transaction2.BuildSignEncode(sourceKeypair) + + // Submit transactions + server.SubmitTransactionXDR(tx1) + // Add a delay or confirmation check for tx1 here or face conflicts + server.SubmitTransactionXDR(tx2) +} +``` + + + +### Account Separation + +By distributing transactions across multiple channel accounts, you can achieve high transaction rates without sequence number conflicts. Each channel account can handle [up to 100 operations](../../../learn/fundamentals/transactions/operations-and-transactions.mdx#transactions) per transaction. + +By using multiple accounts, you bypass the limitation of one transaction for each source account per ledger. This helps you with high-frequency, multi-party, and spiked-demand applications. Here are the accounts for our example, which might be separated into different levels of custody security in production: + +1. **Primary Account ($A_P$)**: Holds the main balance of lumens (or any asset) and is responsible for authorizing operations. This account doesn't directly submit transactions but instead delegates this task to channel accounts. +2. **Channel Accounts ($A_{C_{1\text{--}5}}$)**: Act as intermediaries that submit transactions on behalf of the primary account. Each channel account has its own sequence number, allowing multiple transactions to be submitted in parallel without conflicts. +3. **Multisig Signers ($A_{MPC}$)**: Enhance security by ensuring that no single account has unilateral control over the assets. For example, multiple signers can have authority over $A_P$'s `medium` [threshold](../../../learn/fundamentals/transactions/signatures-multisig.mdx#thresholds) to facilitate valid channel transactions without using $A_P$'s (cold) master key(s). + +![Separated Isolation](/assets/channel-accounts/segregation.png) + +The simple solution of sending all 500 payments from $A_P$ would be rate-limited and prone to sequence number errors. Accordingly, we can split the operations up between five channel accounts. Each channel account submits transactions containing 100 payment operations. This approach ensures that the channel accounts only perform the necessary network submissions, while the primary account retains secure custody over the assets. + +:::info + +For our example, we'll assume the goal is high throughput rather than operational delegation. In the [delegation examples](../../../learn/fundamentals/transactions/signatures-multisig.mdx#example-3-expense-accounts) only a single hot signer may be necessary. Accordingly, let us use an [abnormally-high](https://horizon.stellar.org/fee_stats) base fee of `640` for inclusion in an example ledger. + +::: + + + ```python -# channelAccounts[] is an array of accountIDs, one for each channel -# channelKeys[] is an array of secret keys, one for each channel -# channelIndex is the channel you want to send this transaction over - -transaction = ( - TransactionBuilder( - source_account=channelAccounts[channelIndex], - network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, - base_fee=base_fee, +channelAccountSecrets = [ + "SBXEVUPBW66BU5F2NU4S4QMOBTAU7TVTF4HXWD37VKPTHEC4ULXFGRUH", + "SBQMVILQKB2MXIDQIUN6FGS26PDNBRYKZM7WYZWHEU5MAGCP6HDNV74S", + "SAN3ZTCSWSLVQIBBB4IHN6566K4LMRCIAAXE7THYGB3L45JDTX7WVWGY", + "SACBLHU7OJJR2GTVBNX6WR7OR77CZ3NQHHIHRTEKV5OPO4IKMR2GDRIQ", + "SCFFCZS3FV4VVTIJY7SN2T4GDDW37GPKGTVA4XWOPACPMJHYXDUXXNUS" +] + +channelKeypairs = [Keypair.from_secret(secret) for secret in channelAccountSecrets] +channelAccounts = [server.load_account(keypair.public_key) for keypair in channelKeypairs] + +# Example hot primary account secret +# Generally only use the public key +# Can pre-sign offline or use MPCs +primaryKeypair = Keypair.from_secret("SB6NB3SRNRQTHUXF7PQPWJP7RWY2LCL43IDWRUUSXKJOTY5SBUOVPKWL") + +# 500 example recipient public keys +# Duplicate values for simplicity +allRecipients = [["GD72...B2D"] * 100, + ["GDX7...G7M"] * 100, + ["GBI3...Q4V"] * 100, + ["GBH2...N6E"] * 100, + ["GCJY...OJ2"] * 100] + +txOutput = [] + +# Generating the initial envelope, which can be done before +for channelIndex, channels in enumerate(channelAccounts): + transaction = TransactionBuilder( + source_account = channels, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = 640 + ) + + # The channel index just iterates over channelAccountSecrets[] + for recipients in allRecipients[channelIndex]: + transaction.append_payment_op( + source = primaryKeypair.public_key, + destination = recipients, + amount = "10", + asset = Asset.native() ) - .append_payment_op( - source=baseAccount.public_key, - destination=customerAddress, - asset=Asset.native(), - amount=amountToSend, + + transaction = transaction.set_timeout(3600).build() + + transaction.sign(primaryKeypair) # This can be done before sending to the channel. + # You can implement other MPC signers over the primary account here. + transaction.sign(channelKeypairs[channelIndex]) # Approve being the transaction source. + txOutput.append(transaction) + +# With all channel accounts in one script, you can speed up submission via thredding. +for transactions in txOutput: + try: + response = server.submit_transaction(transaction) + print(f"Transaction succeeded with hash: {response['hash']}") + except Exception as e: + print(f"Transaction failed: {e}") +``` + +```js +const channelAccountSecrets = [ + "SBXEVUPBW66BU5F2NU4S4QMOBTAU7TVTF4HXWD37VKPTHEC4ULXFGRUH", + "SBQMVILQKB2MXIDQIUN6FGS26PDNBRYKZM7WYZWHEU5MAGCP6HDNV74S", + "SAN3ZTCSWSLVQIBBB4IHN6566K4LMRCIAAXE7THYGB3L45JDTX7WVWGY", + "SACBLHU7OJJR2GTVBNX6WR7OR77CZ3NQHHIHRTEKV5OPO4IKMR2GDRIQ", + "SCFFCZS3FV4VVTIJY7SN2T4GDDW37GPKGTVA4XWOPACPMJHYXDUXXNUS", +]; + +const channelKeypairs = channelAccountSecrets.map((secret) => + Keypair.fromSecret(secret), +); + +const channelAccounts = await Promise.all( + channelKeypairs.map((keypair) => server.loadAccount(keypair.publicKey())), +); + +const main = async () => { + // Example hot primary account, generally only use the public key with pre-signed tx + const primaryAccountSecret = + "SB6NB3SRNRQTHUXF7PQPWJP7RWY2LCL43IDWRUUSXKJOTY5SBUOVPKWL"; + const primaryKeypair = Keypair.fromSecret(primaryAccountSecret); + const primaryAccount = await server.loadAccount(primaryKeypair.publicKey()); + + // 500 example recipients + // Assume 100 per list + const allRecipients = [ + ["GD7F...B2D", "..."], + ["GDX7...G7M", "..."], + ["GBI3...Q4V", "..."], + ["GBH2...N6E", "..."], + ["GCJY...OJ2", "..."], + ]; + + const txOutput = []; + + // Generating the initial envelope, which can be done before + for ( + let channelIndex = 0; + channelIndex < channelAccounts.length; + channelIndex++ + ) { + let transaction = new TransactionBuilder(channelAccounts[channelIndex], { + fee: 640, + networkPassphrase: Networks.TESTNET, + }); + + // Channel index just iterates over channelAccountSecrets[] + for (let recipients of allRecipients[channelIndex]) { + transaction.addOperation({ + source: primaryKeypair.publicKey(), + destination: recipients, + asset: Asset.native(), + amount: "10", + type: "payment", + }); + } + + transaction = transaction.setTimeout(3600).build(); + transaction.sign(primaryKeypair); // This can be done before sending to the channel. + // You can implement other MPC signers over the primary account here. + transaction.sign(channelKeypairs[channelIndex]); // Approve being the transaction source. + txOutput.push(transaction); + } + + // With all channel accounts in one script, you can speed up submission via threading. + for (let transaction of txOutput) { + try { + const response = await server.submitTransaction(transaction); + console.log(`Transaction succeeded with hash: ${response.hash}`); + } catch (e) { + console.log(`Transaction failed: ${e}`); + } + } +}; +``` + +```java +import org.stellar.sdk.responses.TransactionResponse; + +List channelAccountSecrets = Arrays.asList( + "SBXEVUPBW66BU5F2NU4S4QMOBTAU7TVTF4HXWD37VKPTHEC4ULXFGRUH", + "SBQMVILQKB2MXIDQIUN6FGS26PDNBRYKZM7WYZWHEU5MAGCP6HDNV74S", + "SAN3ZTCSWSLVQIBBB4IHN6566K4LMRCIAAXE7THYGB3L45JDTX7WVWGY", + "SACBLHU7OJJR2GTVBNX6WR7OR77CZ3NQHHIHRTEKV5OPO4IKMR2GDRIQ", + "SCFFCZS3FV4VVTIJY7SN2T4GDDW37GPKGTVA4XWOPACPMJHYXDUXXNUS" +); + +Server server = new Server("https://horizon-testnet.stellar.org"); + +List channelKeypairs = channelAccountSecrets.stream() + .map(KeyPair::fromSecretSeed) + .toList(); + +List channelAccounts = channelKeypairs.stream() + .map(keypair -> { + try { + return server.accounts().account(keypair.getAccountId()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .toList(); + +// Example hot primary account, generally only use the public key with pre-signed tx +String primaryAccountSecret = "SB6NB3SRNRQTHUXF7PQPWJP7RWY2LCL43IDWRUUSXKJOTY5SBUOVPKWL"; +KeyPair primaryKeypair = KeyPair.fromSecretSeed(primaryAccountSecret); +AccountResponse primaryAccount = server.accounts().account(primaryKeypair.getAccountId()); + +// 500 example recipients +List allRecipients = Arrays.asList( + new String[] {"GD7F...B2D", "..."}, + new String[] {"GDX7...G7M", "..."}, + new String[] {"GBI3...Q4V", "..."}, + new String[] {"GBH2...N6E", "..."}, + new String[] {"GCJY...OJ2", "..."} +); + +// `i` here just iterates over the `channelAccountSecrets` list +for (int i = 0; i < channelAccounts.size(); i++) { + AccountResponse channelAccount = channelAccounts.get(i); + + Transaction.Builder transactionBuilder = new Transaction.Builder(channelAccount, Network.TESTNET) + .setBaseFee(Transaction.MIN_BASE_FEE); + + for (String recipient : allRecipients.get(i)) { + transactionBuilder.addOperation( + new PaymentOperation.Builder(recipient, AssetTypeNative.INSTANCE, "10") + .setSourceAccount(primaryKeypair.getAccountId()) + .build() + ); + } + + Transaction transaction = transactionBuilder.setTimeout(3600).build(); + transaction.sign(primaryKeypair); // This can be done before sending to the channel. + // You can implement other MPC signers over the primary account here. + transaction.sign(channelKeypairs.get(i)); // Approve being the transaction source. + + try { + SubmitTransactionResponse response = server.submitTransaction(transaction); + System.out.println("Transaction succeeded"); + } catch (Exception e) { + System.out.println("Transaction failed: " + e.getMessage()); + } +} +``` + +```go +func main() { + channelAccountSecrets := []string{ + "SBXEVUPBW66BU5F2NU4S4QMOBTAU7TVTF4HXWD37VKPTHEC4ULXFGRUH", + "SBQMVILQKB2MXIDQIUN6FGS26PDNBRYKZM7WYZWHEU5MAGCP6HDNV74S", + "SAN3ZTCSWSLVQIBBB4IHN6566K4LMRCIAAXE7THYGB3L45JDTX7WVWGY", + "SACBLHU7OJJR2GTVBNX6WR7OR77CZ3NQHHIHRTEKV5OPO4IKMR2GDRIQ", + "SCFFCZS3FV4VVTIJY7SN2T4GDDW37GPKGTVA4XWOPACPMJHYXDUXXNUS", + } + + server := horizonclient.DefaultTestNetClient + + var channelKeypairs []*keypair.Full + for _, secret := range channelAccountSecrets { + kp, err := keypair.ParseFull(secret) + check(err) + channelKeypairs = append(channelKeypairs, kp) + } + + var channelAccounts []horizon.Account + for _, kp := range channelKeypairs { + accountRequest := horizonclient.AccountRequest{AccountID: kp.Address()} + account, err := server.AccountDetail(accountRequest) + check(err) + channelAccounts = append(channelAccounts, account) + } + + // Example hot primary account, generally only use the public key with pre-signed tx + primaryAccountSecret := "SB6NB3SRNRQTHUXF7PQPWJP7RWY2LCL43IDWRUUSXKJOTY5SBUOVPKWL" + primaryKeypair, err := keypair.ParseFull(primaryAccountSecret) + check(err) + primaryAccountRequest := horizonclient.AccountRequest{AccountID: primaryKeypair.Address()} + primaryAccount, err := server.AccountDetail(primaryAccountRequest) + check(err) + + // 500 example recipients + allRecipients := [][]string{ + {"GD7F...B2D", "..."}, + {"GDX7...G7M", "..."}, + {"GBI3...Q4V", "..."}, + {"GBH2...N6E", "..."}, + {"GCJY...OJ2", "..."}, + } + + // Channel index iterates over channelAccountSecrets slice keys + for channelIndex, channelAccount := range channelAccounts { + var txOps []txnbuild.Operation + for _, recipients := range allRecipients[channelIndex] { + paymentOp := txnbuild.Payment{ + Destination: recipients, + Amount: "10", + Asset: txnbuild.NativeAsset{}, + SourceAccount: &primaryKeypair.Address(), + } + txOps = append(txOps, &paymentOp) + } + + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &channelAccount, + IncrementSequenceNum: true, + Operations: txOps, + BaseFee: 640, + }, ) - .set_timeout(180) # Wait a maximum of three minutes for the transaction - .build() -) + check(err) + + // You can implement other MPC signers over the primary account here. + tx, err = tx.Sign( + network.TestNetworkPassphrase, + primaryKeypair, // Signature can be made before sending to the channel. + channelKeypairs[channelIndex] // Approve being the transaction source. + ) + check(err) + + resp, err := server.SubmitTransaction(tx) + check(err) + } + } +} +``` + + + +### Principle of Least Trust + +In the custody chain for channels, assets generally leave the primary account. The channel accounts only consume transaction fees and current sequence numbers. By separating transaction approvals from network submissions, you can manage business logic offline, signing more securely. + +This separation of duties also allows you to manage approvals in real time with MPC keys configured to only sign in specific approved instances. You can send the transaction envelope to channel accounts after signing for your operations. This leaves you protected even if an attacker uncovers the hot keys for $A_{C_{1\text{--}5}}$. + +![Key Relationship](/assets/channel-accounts/signers.png) + +:::note + +Channel accounts should have no signing authority over the primary account. However, a primary account or other transaction generator should know channel account public keys. This lets you build initial envelopes with specific channels as the transaction source. + +::: + +This approach works because of Stellar's unique origin-agnostic design. Namely, accounts can submit operations signed by and for any other accounts. This flexibility lets you encode different sources throughout a transaction: + +- **Transaction Envelope** (Channel): Every transaction requires an envelope signer. This is the source account for the entire [transaction envelope](../../../learn/glossary.mdx#transaction-envelope). This becomes the default source of transaction fees and the exclusive source of sequence numbers. +- **Individual Operations** (Main): Set different source accounts for specific operations within the transaction ([individual operations](https://github.com/stellar/stellar-docs/pull/1301)) based on where the transacting assets exist. +- **Wrapping Context** (Optional): Use [fee-bump transactions](https://quest.stellar.org/side-quests/1) to [wrap envelopes](fee-bump-transactions.mdx#fee-account) if you need to [adjust fees](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0015.md) on [stuck transactions](https://bitcoin.stackexchange.com/questions/118184/stuck-transaction-with-enough-fee-rate). + +:::info + +The network only accepts the final transaction once it is wholly constructed and signed by all required accounts, but [not more](https://github.com/stellar/stellar-docs/issues/773). + +::: + +## Configuration Requirements + +Channel accounts let you reliably send transactions without waiting for submission acknowledgments. While this greatly increases your potential transaction rate, channel accounts also introduce operational requirements. These considerations keep your operations in sync with [Stellar Core](https://github.com/stellar/stellar-core/tree/master) while maintaining high throughput. + +:::note + +Instead of channel accounts, you can set [minimum sequence number](../../../learn/fundamentals/transactions/operations-and-transactions.mdx#minimum-sequence-number) preconditions, ensuring transactions are processed in the correct order. This option will not speed up your submissions or solve failed transactions in bursts. However, if you only have a medium-frequency application, then preconditions can ensure execution integrity. + +::: + +### Necessary Signers + +The above example assumes that your accounts are all properly configured on the network. $A_P$ needs the least lumens to cover transaction fees, but you still need to fund the account with minimal base reserves. In contrast, $A_{C_{1\text{--}5}}$ need the most lumens since they pay for all transactional costs. + +### State Rotation + +You can distribute transaction submissions evenly across channel accounts to maximize throughput. This example walks through one way to monitor and manage the lifecycle of channel accounts. It combines our preparations with funding, transaction submission, and securing each account. + +Principally, channel account groups use two states for effective rotation: + +1. **In Use / Submitting**: These are accounts currently submitting transactions. They are temporarily locked until their transactions are confirmed. If you have your own validator, then you can include them directly in your proposed transaction set. + - **Locking Mechanism**: Once a transaction is built and submitted by a channel account, that account should be marked as "in use" and should not be assigned new transactions until the current ones are confirmed or dropped. + - **Monitoring**: Continuously monitor the status of these transactions to determine when the channel accounts become available again. + +2. **Available**: These are channel accounts that are ready to be assigned new transactions. They have either completed their previous transactions or are idle. + - **Assignment**: Distribute new transactions to these available accounts to maintain high throughput and avoid delays. + +Here's an example of what that might look like: + + + +```python +# Dynamic recipients' public keys +allRecipients = ["GD72...B2D", "GDX7...G7M", "GBI3...Q4V", "GBH2...N6E", "GCJY...OJ2", ...] + +channelAccountsTracker = [ + { # index = 0-4 in our example + "account": channelAccounts[i], + "keypair": channelKeypairs[i], + "state": "available" + } for i in range(len(channelAccounts)) +] + +txOutput = [] +recipientIndex = 0 +while recipientIndex < len(allRecipients): + for channelData in channelAccountsTracker: + if recipientIndex >= len(allRecipients): break + if channelData["state"] == "available": + channelData["state"] = "active" + txBundle = TransactionBuilder( + source_account = channelData["account"], + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = 640 + ) + + for _ in range(100): + if recipientIndex >= len(allRecipients): break + txBundle.append_payment_op( + source = primaryKeypair.public_key, + destination = allRecipients[recipientIndex], + amount = "10", + asset = Asset.native() + ) + recipientIndex += 1 + + txBundle = txBundle.set_timeout(3600).build() + txBundle.sign(primaryKeypair) + txBundle.sign(channelData["keypair"]) + + txOutput.append((transaction, channelData)) + else: + print("No available channel account. Waiting...") + # Perhaps sleep here + continue + +failedRecipients = [] -transaction.sign(baseAccountKey) # base account must sign to approve the payment -transaction.sign(channelKeys[channelIndex]) # channel must sign to approve it being the source of the transaction +for bundle, channelData in txOutput: + try: + response = server.submit_transaction(bundle) + print(f"Transaction succeeded with hash: {response['hash']}") + except Exception as e: + print(f"Transaction failed: {e}") + # Partial failure logic example + for op in bundle.operations: + if isinstance(op, Asset): # Assuming only payments + failedRecipients.append(op.destination) + finally: + channelData["state"] = "available" +``` + +```js +// Dynamic recipients' public keys +const allRecipients = ["GD72...B2D", "GDX7...G7M", "GBI3...Q4V", "GBH2...N6E", "GCJY...OJ2", ...]; + +// index = 0-4 in our example +const channelAccountsTracker = channelAccounts.map((account, index) => ({ + account: account, + keypair: channelKeypairs[index], + state: "available" +})); + +const txOutput = []; +let recipientIndex = 0; + +while (recipientIndex < allRecipients.length) { + for (const channelData of channelAccountsTracker) { + if (recipientIndex >= allRecipients.length) break; + if (channelData.state === "available") { + channelData.state = "active"; + let txBundle = new TransactionBuilder(channelData.account, { + networkPassphrase: Networks.TESTNET, + fee: "640" + }); + + for (let i = 0; i < 100; i++) { + if (recipientIndex >= allRecipients.length) break; + txBundle = txBundle.addOperation(Operation.payment({ + source: primaryKeypair.publicKey(), + destination: allRecipients[recipientIndex], + asset: Asset.native(), + amount: "10" + })); + recipientIndex++; + } + + const tx = txBundle.setTimeout(3600).build(); + tx.sign(primaryKeypair); + tx.sign(channelData.keypair); + + txOutput.push({ transaction: tx, channelData: channelData }); + } else { + console.log("No available channel account. Waiting..."); + // Perhaps sleep here + continue; + } + } +} + +const failedRecipients = []; + +for (const {transaction, channelData} of txOutput) { + try { + server.submitTransaction(transaction).then(response => { + console.log(`Transaction succeeded with hash: ${response.hash}`); + }).catch(error => { + console.log(`Transaction failed: ${error}`); + for (const op of transaction.operations) { + if (op.type === "payment") { + failedRecipients.push(op.destination); + } + } + }); + } finally { + channelData.state = "available"; + } +} +``` + +```java +import java.util.ArrayList + +public static void main(String[] args) { + // Dynamic recipients' public keys + String[] allRecipients = {"GD72...B2D", "GDX7...G7M", "GBI3...Q4V", "GBH2...N6E", "GCJY...OJ2"}; + + // index i = 0-4 in our example + List channelAccountsTracker = new ArrayList<>(); + for (int i = 0; i < channelAccounts.length; i++) { + channelAccountsTracker.add(new ChannelAccount(channelAccounts[i], channelKeypairs[i], "available")); + } + + List txOutput = new ArrayList<>(); + int recipientIndex = 0 + while (recipientIndex < allRecipients.length) { + for (ChannelAccount channelData : channelAccountsTracker) { + if (recipientIndex >= allRecipients.length) break; + if (channelData.state.equals("available")) { + channelData.state = "active"; + Transaction.Builder txBuilder = new Transaction.Builder(channelData.account, Network.TESTNET) + .setBaseFee(640) + + for (int i = 0; i < 100; i++) { + if (recipientIndex >= allRecipients.length) break; + txBuilder.addOperation(new PaymentOperation.Builder( + primaryKeypair.getAccountId(), + allRecipients[recipientIndex], + AssetTypeNative.INSTANCE, "10" + ).build()); + recipientIndex++; + } + + Transaction txBundle = txBuilder.setTimeout(3600).build(); + txBundle.sign(primaryKeypair); + txBundle.sign(channelData.keypair); + txOutput.add(new TransactionBundle(txBundle, channelData)); + } else { + System.out.println("No available channel account. Waiting..."); + // Perhaps sleep here + continue; + } + } + } + + List failedRecipients = new ArrayList<>(); + + for (TransactionBundle txBundle : txOutput) { + try { + TransactionResponse response = server.submitTransaction(txBundle.transaction); + System.out.println("Transaction succeeded with hash: " + response.getHash()); + } catch (Exception e) { + System.out.println("Transaction failed: " + e.getMessage()); + for (Operation op : txBundle.transaction.getOperations()) { + if (op instanceof PaymentOperation) { + failedRecipients.add(((PaymentOperation) op).getDestination()); + } + } + } finally { + txBundle.channelData.state = "available"; + } + } +} + +static class ChannelAccount { + Account account; + KeyPair keypair; + String state + ChannelAccount(Account account, KeyPair keypair, String state) { + this.account = account; + this.keypair = keypair; + this.state = state; + } +} + +static class TransactionBundle { + Transaction transaction; + ChannelAccount channelData + TransactionBundle(Transaction transaction, ChannelAccount channelData) { + this.transaction = transaction; + this.channelData = channelData; + } +} +``` + +```go +import "fmt" + +// Dynamic recipients' public keys +var allRecipients = []string{"GD72...B2D", "GDX7...G7M", "GBI3...Q4V", "GBH2...N6E", "GCJY...OJ2"} + +func main() { + channelAccountsTracker := make([]map[string]interface{}, len(channelAccounts)) + + // index = 0-4 in our example + for index, account := range channelAccounts { + channelAccountsTracker[index] = map[string]interface{}{ + "account": account, + "keypair": channelKeypairs[i], + "state": "available", + } + } + + txOutput := []struct { + transaction *txnbuild.Transaction + channelData map[string]interface{} + }{} + recipientIndex := 0 + + for recipientIndex < len(allRecipients) { + for _, channelData := range channelAccountsTracker { + if recipientIndex >= len(allRecipients) { + break + } + if channelData["state"] == "available" { + channelData["state"] = "active" + + sourceAccount := channelData["account"].(*txnbuild.SimpleAccount) + txBundle := txnbuild.TransactionParams{ + SourceAccount: sourceAccount, + BaseFee: txnbuild.MinBaseFee * 10, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(3600)}, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{}, + } + + for i := 0; i < 100; i++ { + if recipientIndex >= len(allRecipients) { + break + } + paymentOp := txnbuild.Payment{ + SourceAccount: primaryKeypair.Address(), + Destination: allRecipients[recipientIndex], + Amount: "10", + Asset: txnbuild.NativeAsset{}, + } + txBundle.Operations = append(txBundle.Operations, &paymentOp) + recipientIndex++ + } + + tx, err := txnbuild.NewTransaction(txBundle) + check(err) + tx, err = tx.Sign( + network.TestNetworkPassphrase, + primaryKeypair, + channelData["keypair"].(*keypair.Full) + ) + check(err) + + txOutput = append( + txOutput, struct { + transaction *txnbuild.Transaction + channelData map[string]interface{} + }{ + transaction: tx, + channelData: channelData + }) + } else { + fmt.Println("No available channel account. Waiting...") + // Perhaps sleep here + continue + } + } + } + + failedRecipients := []string{} + + for _, output := range txOutput { + resp, err := server.SubmitTransaction(output.transaction) + if err != nil { + fmt.Printf("Transaction failed: %v\n", err) + for _, op := range output.transaction.Operations() { + if paymentOp, ok := op.(*txnbuild.Payment); ok { + failedRecipients = append(failedRecipients, paymentOp.Destination) + } + } + } else { + fmt.Printf("Transaction succeeded with hash: %s\n", resp.Hash) + } + output.channelData["state"] = "available" + } +} ``` + +### Bundle Size + +The bundle size is how many operations you fit in each transaction sent by channel accounts. While the network [defines a maximum](../../../networks/software-versions.mdx) [of 100](../../../validators/admin-guide/monitoring.mdx#general-node-information), it may take longer than desired for your flow of operations to reach this threshold. If that's the case, each channel can submit their own transaction each ledger with all incoming operations. + +Desired Flow + +In our example, we assume the bulk payment operations greatly exceed a single transaction, must occur promptly, and come pre-signed by $A_P$. Here, the main constraint is network capacity itself, as referenced earlier with payment channels. Accordingly, it is best practice to considerately send channel transactions based on how much you want to pay in fees. + +If you fill up the ledger with all of your own transactions, you can expect to pay [exponentially-higher fees](../../../learn/fundamentals/fees-resource-limits-metering.mdx#surge-pricing) than dynamic bundle sizes which spread operations out over time. This chiefly depends on the rate you want to submit transactions. As long as you aren't consistently above 100 operations per ledger, each channel can just submit a transaction each ledger. + +![Filling Ledger](/assets/channel-accounts/congesting.png) + +:::note Block Size + +You can create as many channel accounts as needed to maintain your desired transaction rate. However, one ledger [can only fit](https://stellar.expert/explorer/public/protocol-history) 1,000 operations from all peers. To keep [fees](../../../learn/fundamentals/fees-resource-limits-metering.mdx#inclusion-fee) within reason and minimize congestion, it is best practice to limit yourself to 300 operations per ledger. For higher throughput in non-emergency applications, consider using other scaling solutions like [Starlight](../../../learn/glossary.mdx#starlight). + +::: + +## Implementation Considerations + +We walked through one example of using channel accounts, but ultimately their application depends on your use case. Therefore, we will wrap up by reiterating ecosystem design choices related to high-frequency transaction submission. These foundational choices can help you get the most out of Stellar no matter your size. + +### Security + +You must sign the transaction with both the primary-account and channel-account keys since the final transaction implicates both keypairs. It's best practice to keep these signatures in isolated instances or storage devices so as to minimize breach risks. For instance, to protect your main account, you can deploy security practices around hot keys, signature weights, and access rotations. + +### Receiving Node + +Horizon instances limit [request frequency](../../../data/apis/horizon/api-reference/structure/rate-limiting.mdx) by default. If you both submit transactions and read data quickly, then your channel accounts can exceed a public validator's request threshold. If you consistently have excessive queries, you may need your [own node](../../../validators/admin-guide/README.mdx) to submit transaction sets quickly. + +### Fee Sponsorships + +While we've discussed sending lumens directly to the channel accounts, you can also have transaction fees sponsored by the primary account. In a [custodial solution](../../../../platforms/anchor-platform/admin-guide/custody-services), you may prefer that channel accounts hold no assets at all, maintaining trustlines with [sponsored reserves](./sponsored-reserves.mdx) for example. While you can use payments from $A_P$ to $A_C$ to cover fees each transaction, it is best practice to simply leave channels with adequate funding. diff --git a/docs/build/guides/transactions/claimable-balances.mdx b/docs/build/guides/transactions/claimable-balances.mdx index e01ec7aa28..d816ef923a 100644 --- a/docs/build/guides/transactions/claimable-balances.mdx +++ b/docs/build/guides/transactions/claimable-balances.mdx @@ -1,5 +1,5 @@ --- -title: Claimable balances +title: Claimable Balances description: Split a payment into two parts by creating a claimable balance. sidebar_position: 20 --- @@ -8,10 +8,10 @@ import Details from "@theme/Details"; 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 -- Part 2: destination account(s), or claimant(s), accepts the ClaimableBalanceEntry using the Claim Claimable Balance operation +- 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. +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. @@ -23,31 +23,28 @@ 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). +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). (/_ ⚠ Should probably be together _/) -#### Additional parameters +#### Other 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. +- **`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. +- **`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`). It contains an SHA-256 hash of the [OperationID](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0023.md#user-content-fn-id-arithmatic-d2727bb47f78787e54824bc772d9861c) for claimable balances. {/* TODO StrKey reprenentation */} +- **`ClientBalanceID`**: Hex of `ClaimableBalanceID` returned after a successful `CreateClaimableBalance` operation. It is required when claiming the `ClaimableBalanceEntry` with the `ClaimClaimableBalance` operation. {/* padded to 72 chars */} ### 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 @@ -59,11 +56,17 @@ 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. +:::note Claimant Permanence + +[Currently](https://github.com/stellar/stellar-protocol/discussions/1504#discussioncomment-10117060), you cannot recover a claimable balance if none of the predicates can be fulfilled. The reclaim example below acts as a safety net for this situation. + +:::
@@ -71,35 +74,35 @@ Each of these accounts can only claim the balance under unique conditions. Accou ```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 +112,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; - "github.com/stellar/stellar-rpc/client" - "github.com/stellar/stellar-rpc/protocol" +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) + "github.com/stellar/stellar-rpc/client" + "github.com/stellar/stellar-rpc/protocol" - // 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 +498,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 +624,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 +804,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. diff --git a/docs/build/guides/transactions/clawbacks.mdx b/docs/build/guides/transactions/clawbacks.mdx index 3656667290..91606acb01 100644 --- a/docs/build/guides/transactions/clawbacks.mdx +++ b/docs/build/guides/transactions/clawbacks.mdx @@ -4,15 +4,15 @@ description: Use clawbacks to burn a specific amount of a clawback-enabled asset sidebar_position: 30 --- -Clawbacks were introduced in [CAP-0035: Asset Clawback](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0035.md) and allow an asset issuer to burn a specific amount of a clawback-enabled asset from a trustline or claimable balance, effectively destroying it and removing it from a recipient's balance. +Clawbacks let an asset issuer burn a specific amount of a [clawback-enabled](../../../tokens/control-asset-access.mdx#clawback-enabled-0x8) asset. Introduced in [CAP-0035](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0035.md), issuers can clawback from account trustlines or claimable balances. Clawbacks effectively [destroy](https://stellar.org/blog/developers/using-protocol-17s-asset-clawback) the assets by removing them from a recipient’s balance. -They were designed to allow asset issuers to meet securities regulations, which in many jurisdictions require asset issuers (or designated transfer agents) to have the ability to revoke assets in the event of a mistaken or fraudulent transaction or other regulatory action regarding a specific person or asset. +They allow asset issuers or their designated transfer agent to meet securities regulations, which in many jurisdictions require the ability to revoke assets in the event of a mistake, fraudulent transaction, or other regulatory action regarding a specific person or asset. Clawbacks are useful for: -- Recovering assets that have been fraudulently obtained -- Responding to regulatory actions -- Enabling identity-proofed persons to recover an enabled asset in the event of loss of key custody or theft +- Recovering assets that have been fraudulently obtained, +- Responding to regulatory actions, and +- Enabling identity-proofed persons to recover an enabled asset in the event of loss of key custody or theft. ## Operations @@ -24,864 +24,1219 @@ If an issuing account wants to set the `AUTH_CLAWBACK_ENABLED_FLAG`, it must hav ### Clawback -The issuing account uses this operation to claw back some or all of an asset. Once an account holds a particular asset for which clawbacks have been enabled, the issuing account can claw it back, burning it. You need to provide the asset, a quantity, and the account from which you're clawing back the asset. For more details, refer the [Clawback operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#clawback). +The issuing account uses this operation to claw back some or all of an asset. Once an account holds a particular asset for which clawbacks have been enabled, the issuing account can claw it back, burning it. You need to provide the asset, a quantity, and the account from which you’re clawing back the asset. For more details, refer to the [Clawback operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#clawback). ### 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. For more details, refer the [Clawback Claimable Balance operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#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. For more details, refer to the [Clawback Claimable Balance operation](../../../learn/fundamentals/transactions/list-of-operations.mdx#clawback-claimable-balance). ### Set Trust Line Flag The issuing account uses this operation to remove clawback capabilities on a specific trustline by removing the `TRUSTLINE_CLAWBACK_ENABLED_FLAG` via the [**SetTrustLineFlags**](../../../learn/fundamentals/transactions/list-of-operations.mdx#set-trustline-flags) operation. -You can only clear a flag, not set it. So clearing a clawback flag on a trustline is irreversible. This is done so that you don't retroactively change the rules on your asset holders. If you'd like to enable clawbacks again, holders must reissue their trustlines. +You can only clear a flag, not set it. Thus, clearing a clawback flag on a trustline is irreversible. This is done so that you don’t retroactively change the rules for your asset holders. If you’d like to enable clawbacks again, holders must reissue their trustlines. ## Examples -Here we'll cover the following approaches to clawing back an asset. +Here we’ll cover the following approaches to clawing back an asset. -**Example 1:** Issuing account (Account A) creates a clawback-enabled asset and sends it to Account B. Account B sends that asset to Account C. Account A will then clawback the asset from C. **Example 2:** Account B creates a claimable balance for Account C, and Account A claws back the claimable balance. **Example 3:** Account A issues a clawback-enabled asset to Account B. A claws back some of the asset from B, then removes the clawback enabled flag from the trustline and can no longer clawback the asset. +- **[Example 1](#example-1-payments):** Issuing account $\mathcal{A}$ creates a clawback-enabled asset and sends it to Account $\mathcal{B}$. Then, $\mathcal{B}$ sends that asset to Account $\mathcal{C}$. Lastly, $\mathcal{A}$ will clawback the asset from $\mathcal{C}$. +- **[Example 2](#example-2-claimable-balances):** $\mathcal{B}$ creates a claimable balance for $\mathcal{C}$, and $\mathcal{A}$ claws back the new claimable balance. +- **[Example 3](#example-3-selectively-enabling-clawback):** $\mathcal{A}$ issues a clawback-enabled asset to $\mathcal{B}$. Then, $\mathcal{A}$ claws back some of the asset from $\mathcal{B}$. Next, $\mathcal{A}$ removes the clawback-enabled flag from the trustline and can no longer clawback the asset. -### Preamble: Creating + Funding Accounts and Issuing a Clawback-able Asset +### Preamble: Issuing a Clawback-able Asset -First, we'll set up an account to enable clawbacks and issue an asset accordingly. +First, we’ll set up an account to enable clawbacks and issue an asset accordingly. Properly issuing an asset with separate [issuer and distributor](../../../tokens/control-asset-access.mdx#issuer-and-distributor-accounts) accounts is a little more involved. We’ll start with a simpler method as an example. -Properly issuing an asset (with separate issuing and distribution accounts) is a little more involved, but we'll use a simpler method here. +:::note -Also, note that we first need to enable clawbacks and then establish trustlines since you cannot retroactively enable clawback on existing trustlines. +We first need to enable clawbacks and then establish trustlines, since you cannot retroactively enable clawback on existing trustlines. -The following code snippet contains helper functions that will be used in the following examples. +::: +```python +from stellar_sdk import Server, Keypair, Asset, Network, TransactionBuilder, Operation + +server = Server("https://horizon-testnet.stellar.org") + +A = Keypair.from_secret("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ") +B = Keypair.from_secret("SAAY2H7SANIS3JLFBFPLJRTYNLUYH4UTROIKRVFI4FEYV4LDW5Y7HDZ4") +C = Keypair.from_secret("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4") + +AstroToken = Asset("ClawbackCoin", A.public_key) + +def enableClawback(account, keys): + tx = buildTx(account, keys, [ + Operation.set_options( + set_flags=Operation.Flag.AUTH_CLAWBACK_ENABLED_FLAG | Operation.Flag.AUTH_REVOCABLE_FLAG + # Also add `revocable` for control over who can hold the asset. + ) + ]) + return server.submit_transaction(tx) + +def establishTrustline(recipient, key): + tx = buildTx(recipient, key, [ + Operation.change_trust( + asset=AstroToken, + ) + ]) + return server.submit_transaction(tx) + +def getAccounts(): + return [ + server.load_account(A.public_key), + server.load_account(B.public_key), + server.load_account(C.public_key) + ] + +def preamble(): + accounts = getAccounts() + accountA, accountB, accountC = accounts + enableClawback(accountA, A) + return establishTrustline(accountB, B), establishTrustline(accountC, C) + +def buildTx(source, signer, ops): + tx = TransactionBuilder( + source, + network_passphrase = Network.TESTNET, + base_fee = Network.BASE_FEE + ) + for op in ops: + tx.append_operation(op) + tx.set_timeout(3600) + tx = tx.build() + tx.sign(signer) + return tx + +def showBalances(accounts): + for accs in accounts: + print(f"{accs.account_id}: {getBalance(accs)}") + +def getBalance(account): + for balance in account.balances: + if( + balance["asset_type"] != "native" and + balance["asset_code"] == AstroToken.code and + balance["asset_issuer"] == AstroToken.issuer + ): + return balance["balance"] + return "0" +``` + ```js -import * as sdk from "@stellar/stellar-sdk"; - -let server = new sdk.rpc.Server("https://soroban-testnet.stellar.org"); - -const A = sdk.Keypair.random(); -const B = sdk.Keypair.random(); -const C = sdk.Keypair.random(); - -console.log("=== ACCOUNT SETUP ==="); -console.log(`Account A (Issuer): ${A.publicKey()}`); -console.log(`Account B (Trustor): ${B.publicKey()}`); -console.log(`Account C (Trustor): ${C.publicKey()}`); -console.log(); - -const ASSET = new sdk.Asset("CLAW", A.publicKey()); - -// Helper function to format account ID with label -function formatAccount(accountId) { - const shortId = accountId.substring(0, 8); - if (accountId === A.publicKey()) { - return `${shortId} (Account A)`; - } else if (accountId === B.publicKey()) { - return `${shortId} (Account B)`; - } else if (accountId === C.publicKey()) { - return `${shortId} (Account C)`; - } - return shortId; -} +const sdk = require("stellar-sdk"); -// Helper function to safely scale XDR Int64 asset amounts -function scaleAsset(x) { - return Number((x * 10n) / 10000000n) / 10; // one decimal place -} +let server = new sdk.Server("https://horizon-testnet.stellar.org"); -// Helper function to fetch claimable balance details using SDK's built-in method -async function fetchClaimableBalance( - balanceId, - description = "Claimable Balance", -) { - try { - console.log(`\n--- Checking ${description} ---`); - console.log(`Looking up balance ID: ${balanceId}`); - - // Use SDK's built-in getClaimableBalance method - const claimableBalance = await server.getClaimableBalance(balanceId); - const asset = sdk.Asset.fromOperation(claimableBalance.asset()); - const amount = scaleAsset(claimableBalance.amount().toBigInt()).toFixed(1); - - console.log(`✅ Found claimable balance`); - console.log(` Amount: ${amount} ${asset.code}`); - console.log( - ` Number of claimants: ${claimableBalance.claimants().length}`, - ); +const A = sdk.Keypair.fromSecret( + "SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ", +); +const B = sdk.Keypair.fromSecret( + "SAAY2H7SANIS3JLFBFPLJRTYNLUYH4UTROIKRVFI4FEYV4LDW5Y7HDZ4", +); +const C = sdk.Keypair.fromSecret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", +); - // Show claimant details - claimableBalance.claimants().forEach((claimant, index) => { - const destination = claimant.v0().destination().ed25519(); - const claimantAddress = sdk.StrKey.encodeEd25519PublicKey(destination); - console.log( - ` Claimant ${index + 1}: ${formatAccount(claimantAddress)}`, - ); - }); - - return true; // Balance exists - } catch (error) { - console.log(`❌ Claimable balance not found (${error.message})`); - return false; // Balance doesn't exist - } -} - -// Fund accounts first -function fundAccounts() { - console.log("=== FUNDING ACCOUNTS WITH XLM ==="); - return Promise.all([ - server.requestAirdrop(A.publicKey()), - server.requestAirdrop(B.publicKey()), - server.requestAirdrop(C.publicKey()), - ]).then(() => { - console.log("All accounts funded with XLM via airdrop"); - // Wait for funding to complete - return new Promise((resolve) => setTimeout(resolve, 3000)); - }); -} +const AstroToken = new sdk.Asset("ClawbackCoin", A.publicKey()); // Enables AuthClawbackEnabledFlag on an account. function enableClawback(account, keys) { - console.log( - `Enabling clawback flags on account ${formatAccount(account.accountId())}`, - ); - return submitAndPollTransaction( + return server.submitTransaction( buildTx(account, keys, [ sdk.Operation.setOptions({ setFlags: sdk.AuthClawbackEnabledFlag | sdk.AuthRevocableFlag, + // Also add `revocable` for control over who can hold the asset. }), ]), - "Enable Clawback Flags", ); } -// Establishes a trustline for `recipient` for the CLAW Asset +// Establishes a trustline for `recipient` for AstroToken (from above). const establishTrustline = function (recipient, key) { - console.log( - `${formatAccount(recipient.accountId())} establishing trustline for ${ - ASSET.code - }`, - ); - return submitAndPollTransaction( + return server.submitTransaction( buildTx(recipient, key, [ sdk.Operation.changeTrust({ - asset: ASSET, - limit: "5000", // arbitrary + asset: AstroToken, }), ]), - `Establish Trustline (${formatAccount(recipient.accountId())})`, ); }; // Retrieves latest account info for all accounts. function getAccounts() { return Promise.all([ - server.getAccount(A.publicKey()), - server.getAccount(B.publicKey()), - server.getAccount(C.publicKey()), + server.loadAccount(A.publicKey()), + server.loadAccount(B.publicKey()), + server.loadAccount(C.publicKey()), ]); } -// Show XLM balances (after funding) -function showXLMBalances(accounts) { - console.log("\n=== XLM BALANCES ==="); - return Promise.all( - accounts.map((acc) => { - return getXLMBalance(acc.accountId()).then((balance) => { - console.log(`${formatAccount(acc.accountId())}: ${balance} XLM`); - }); - }), - ); -} - -// Get XLM balance using account ledger entry -function getXLMBalance(accountId) { - return server - .getAccountEntry(accountId) - .then((accountEntry) => { - return scaleAsset(accountEntry.balance().toBigInt()).toFixed(1); - }) - .catch(() => "0"); -} - -// Show CLAW balances -function showCLAWBalances(accounts) { - console.log("\n=== CLAW BALANCES ==="); - return Promise.all( - accounts.map((acc) => { - return getBalance(acc.accountId()).then((balance) => { - console.log(`${formatAccount(acc.accountId())}: ${balance} CLAW`); - }); - }), - ); -} - -// Get CLAW balance using getTrustline -function getBalance(accountId) { - return server - .getTrustline(accountId, ASSET) - .then((trustlineEntry) => { - return scaleAsset(trustlineEntry.balance().toBigInt()).toFixed(1); - }) - .catch(() => "0"); +// Enables clawback on A, and establishes trustlines from C, B -> A. +function preamble() { + return getAccounts().then(function (accounts) { + let [accountA, accountB, accountC] = accounts; + return enableClawback(accountA, A).then( + Promise.all([ + establishTrustline(accountB, B), + establishTrustline(accountC, C), + ]), + ); + }); } // Helps simplify creating & signing a transaction. function buildTx(source, signer, ops) { - var tx = new sdk.TransactionBuilder(source, { + var tx = new StellarSdk.TransactionBuilder(source, { fee: sdk.BASE_FEE, - networkPassphrase: sdk.Networks.TESTNET, + networkPassphrase: StellarSdk.Networks.TESTNET, }); - ops.forEach((op) => tx.addOperation(op)); - tx = tx.setTimeout(30).build(); + tx = tx.setTimeout(3600).build(); tx.sign(signer); return tx; } -// Helper function to submit transaction and poll for completion using RPC -function submitAndPollTransaction(transaction, description = "Transaction") { - return server.sendTransaction(transaction).then((submitResponse) => { - if (submitResponse.status !== "PENDING") { - throw new Error( - `Transaction submission failed: ${submitResponse.status}`, - ); +// Prints the balances of a list of accounts. +function showBalances(accounts) { + accounts.forEach((acc) => { + console.log(`${acc.accountId()}: ${getBalance(acc)}`); + }); +} + +// Retrieves the balance of AstroToken in `account`. +function getBalance(account) { + const balances = account.balances.filter((balance) => { + return ( + balance.asset_code == AstroToken.code && + balance.asset_issuer == AstroToken.issuer + ); + }); + return balances.length > 0 ? balances[0].balance : "0"; +} +``` + +```java +import org.stellar.sdk.*; +import org.stellar.sdk.requests.RequestBuilder; +import org.stellar.sdk.responses.AccountResponse; +import org.stellar.sdk.responses.SubmitTransactionResponse; + +public class Clawback { + + private static final Server server = new Server("https://horizon-testnet.stellar.org"); + private static final KeyPair A = KeyPair.fromSecretSeed("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ"); + private static final KeyPair B = KeyPair.fromSecretSeed("SAAY2H7SANIS3JLFBFPLJRTYNLUYH4UTROIKRVFI4FEYV4LDW5Y7HDZ4"); + private static final KeyPair C = KeyPair.fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); + private static final Asset AstroToken = Asset.createNonNativeAsset("ClawbackCoin", A.getAccountId()); + + public static void main(String[] args) throws IOException { + AccountResponse[] accounts = getAccounts(); + AccountResponse accountA = accounts[0]; + AccountResponse accountB = accounts[1]; + AccountResponse accountC = accounts[2]; + + enableClawback(accountA, A); + establishTrustline(accountB, B); + establishTrustline(accountC, C); + } + + // Enables AuthClawbackEnabledFlag on an account. + private static void enableClawback(AccountResponse account, KeyPair keys) throws IOException { + Transaction transaction = buildTx(account, keys, new Operation[]{ + new SetOptionsOperation.Builder() + .setSetFlags(AccountFlag.AUTH_CLAWBACK_ENABLED_FLAG | AccountFlag.AUTH_REVOCABLE_FLAG) + // Also add `revocable` for control over who can hold the asset. + .build() + }); + SubmitTransactionResponse response = server.submitTransaction(transaction); + System.out.println("Clawback enabled: " + response); + } + + // Establishes a trustline for `recipient` for AstroToken (from above). + private static void establishTrustline(AccountResponse recipient, KeyPair key) throws IOException { + Transaction transaction = buildTx(recipient, key, new Operation[]{ + new ChangeTrustOperation.Builder(AstroToken, "922337203685.4775807").build() + }); + SubmitTransactionResponse response = server.submitTransaction(transaction); + System.out.println("Trustline established: " + response); + } + + // Retrieves latest account info for all accounts. + private static AccountResponse[] getAccounts() throws IOException { + AccountResponse accountA = server.accounts().account(A.getAccountId()); + AccountResponse accountB = server.accounts().account(B.getAccountId()); + AccountResponse accountC = server.accounts().account(C.getAccountId()); + return new AccountResponse[]{accountA, accountB, accountC}; + } + + // Helper method to build and sign a transaction + private static Transaction buildTx(AccountResponse source, KeyPair signer, Operation[] ops) { + Transaction.Builder txBuilder = new Transaction.Builder(source, Network.TESTNET) + .setTimeout(3600) + .setBaseFee(Transaction.MIN_BASE_FEE); + + for (Operation op : ops) { + txBuilder.addOperation(op); } - console.log(`${description} submitted: ${submitResponse.hash}`); + Transaction transaction = txBuilder.build(); + transaction.sign(signer); + return transaction; + } + + // Prints the balances of a list of accounts. + private static void showBalances(AccountResponse[] accounts) throws IOException { + for (AccountResponse account : accounts) { + System.out.println( + account.getAccountId() + + ": " + + getBalance(account) + ); + } + } - return server.pollTransaction(submitResponse.hash).then((finalResponse) => { - if (finalResponse.status === "SUCCESS") { - console.log(`${description} completed successfully`); - } else { - console.log(`${description} failed: ${finalResponse.status}`); + // Retrieves the balance of AstroToken in `account`. + private static String getBalance(AccountResponse account) { + for (AccountResponse.Balance balance : account.getBalances()) { + if (!balance.getAssetType().equals("native") && + balance.getAssetCode().equals(AstroToken.getCode()) && + balance.getAssetIssuer().equals(AstroToken.getIssuer())) { + return balance.getBalance(); } + } + return "0"; + } +} +``` - return { - hash: submitResponse.hash, - status: finalResponse.status, - resultXdr: finalResponse.resultXdr, - }; - }); - }); +```go +package main + +import ( + "fmt" + "github.com/stellar/go-stellar-sdk/build" + "github.com/stellar/go-stellar-sdk/clients/horizonclient" + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/network" + "github.com/stellar/go-stellar-sdk/protocols/horizon" + "github.com/stellar/go-stellar-sdk/txnbuild" +) + +var client = horizonclient.DefaultTestNetClient + +var ( + A = keypair.MustParseFull("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ") + B = keypair.MustParseFull("SAAY2H7SANIS3JLFBFPLJRTYNLUYH4UTROIKRVFI4FEYV4LDW5Y7HDZ4") + C = keypair.MustParseFull("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4") + + AstroToken = txnbuild.CreditAsset{Code: "ClawbackCoin", Issuer: A.Address()} +) + +// Enables AuthClawbackEnabledFlag on an account. +func enableClawback(account horizon.Account, keys *keypair.Full) { + tx, err := buildTx(account, keys, []txnbuild.Operation{ + &txnbuild.SetOptions{ + SetFlags: []txnbuild.AccountFlag{ + txnbuild.AuthClawbackEnabled, + txnbuild.AuthRevocable, + // Also add `revocable` for control over who can hold the asset. + }, + }, + }) + check(err) + resp, err := client.SubmitTransaction(tx) + check(err) + fmt.Println("Clawback enabled:", resp) } -// Makes payment from `fromAccount` to `toAccount` of `amount` -function makePayment(toAccount, fromAccount, fromKey, amount) { - console.log( - `\nPayment: ${formatAccount(fromAccount.accountId())} → ${formatAccount( - toAccount.accountId(), - )} (${amount} CLAW)`, - ); - return submitAndPollTransaction( - buildTx(fromAccount, fromKey, [ - sdk.Operation.payment({ - destination: toAccount.accountId(), - asset: ASSET, - amount: amount, - }), - ]), - `Payment of ${amount} CLAW`, - ); +// Establishes a trustline for `recipient` for AstroToken (from above). +func establishTrustline(recipient horizon.Account, key *keypair.Full) { + tx, err := buildTx(recipient, key, []txnbuild.Operation{ + &txnbuild.ChangeTrust{ + Line: AstroToken, + }, + }) + check(err) + resp, err := client.SubmitTransaction(tx) + check(err) + fmt.Println("Trustline established:", resp) } -// Creates a claimable balance from `fromAccount` to `toAccount` of `amount` -function createClaimable(fromAccount, fromKey, toAccount, amount) { - console.log( - `\nCreating claimable balance: ${formatAccount( - fromAccount.accountId(), - )} → ${formatAccount(toAccount.accountId())} (${amount} CLAW)`, - ); - return submitAndPollTransaction( - buildTx(fromAccount, fromKey, [ - sdk.Operation.createClaimableBalance({ - asset: ASSET, - amount: amount, - claimants: [new sdk.Claimant(toAccount.accountId())], - }), - ]), - `Create Claimable Balance of ${amount} CLAW`, - ); +// Retrieves latest account info for all accounts. +func getAccounts() ([]horizon.Account, error) { + accountA, err := client.AccountDetail( + horizonclient.AccountRequest{ + AccountID: A.Address(), + }, + ) + check(err) + accountB, err := client.AccountDetail( + horizonclient.AccountRequest{ + AccountID: B.Address(), + }, + ) + check(err) + accountC, err := client.AccountDetail( + horizonclient.AccountRequest{ + AccountID: C.Address(), + }, + ) + check(err) + return []horizon.Account{accountA, accountB, accountC}, nil } -// Parse the ClaimableBalanceId from the transaction result XDR -function getBalanceId(txResponse) { - const txResult = txResponse.resultXdr; - const operationResult = txResult.result().results()[0]; +// Helper method to build and sign a transaction. +func buildTx(source horizon.Account, signer *keypair.Full, ops []txnbuild.Operation) (*txnbuild.Transaction, error) { + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &source, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Operations: ops, + Timebounds: txnbuild.NewTimeout(3600), + }, + ) + check(err) + tx, err = tx.Sign(network.TestNetworkPassphrase, signer) + check(err) + return tx, nil +} - let creationResult = operationResult.value().createClaimableBalanceResult(); - return creationResult.balanceId().toXDR("hex"); +// Enables clawback on A, and establishes trustlines from C, B -> A. +func main() { + accounts, err := getAccounts() + check(err) + accountA := accounts[0] + accountB := accounts[1] + accountC := accounts[2] + + enableClawback(accountA, A) + establishTrustline(accountB, B) + establishTrustline(accountC, C) } -// Clawback the claimable balance using its ID -function clawbackClaimable(issuerAccount, issuerKey, balanceId) { - console.log( - `\nClawback claimable balance: ${formatAccount( - issuerAccount.accountId(), - )} clawing back balance ${balanceId}`, - ); - return submitAndPollTransaction( - buildTx(issuerAccount, issuerKey, [ - sdk.Operation.clawbackClaimableBalance({ balanceId }), +// Prints the balances of a list of accounts. +func showBalances(accounts []horizon.Account) { + for _, acc := range accounts { + fmt.Printf( + "%s: %s\n", + acc.AccountID, + getBalance(acc), + ) + } +} + +// Retrieves the balance of AstroToken in `account`. +func getBalance(account horizon.Account) string { + for _, balance := range account.Balances { + if balance.Asset.Type != "native" && + balance.Asset.Code == AstroToken.GetCode() && + balance.Asset.Issuer == AstroToken.GetIssuer() { + return balance.Balance + } + } + return "0" +} +``` + + + +### Example 1: Payments + +With the shared setup code out of the way, we can now demonstrate how clawback works for payments. This example will highlight how the asset issuer holds control over their asset regardless of how it gets distributed to the world. + +In our scenario, Account $\mathcal{A}$ will pay Account $\mathcal{B}$ with 1000 `AstroToken`; then, $\mathcal{B}$ will pay Account $\mathcal{C}$ 500 tokens in turn. Finally, $\mathcal{A}$ will claw back half of $\mathcal{C}$’s balance, burning 250 tokens forever. Let’s dive into the helper functions: + + + +```python +# Make a payment to `toAccount` from `fromAccount` for `amount`. +def makePayment(toAccount, fromAccount, fromKey, amount): + tx = buildTx(fromAccount, fromKey, [ + Operation.payment( + destination=toAccount.account_id, + asset=AstroToken, + amount=amount, + ) + ]) + return server.submit_transaction(tx) + +# Perform a clawback by `byAccount` of `amount` from `fromAccount`. +def doClawback(byAccount, byKey, fromAccount, amount): + tx = buildTx(byAccount, byKey, [ + Operation.clawback( + from_=fromAccount.account_id, + asset=AstroToken, + amount=amount, + ) + ]) + return server.submit_transaction(tx) +``` + +```js +// Make a payment to `toAccount` from `fromAccount` for `amount`. +function makePayment(toAccount, fromAccount, fromKey, amount) { + return server.submitTransaction( + buildTx(fromAccount, fromKey, [ + sdk.Operation.payment({ + destination: toAccount.accountId(), + asset: AstroToken, // defined in preamble + amount: amount, + }), ]), - `Clawback Claimable Balance`, ); } -// Clawback `amount` of CLAW from `fromAccount` by `byAccount` +// Perform a clawback by `byAccount` of `amount` from `fromAccount`. function doClawback(byAccount, byKey, fromAccount, amount) { - console.log( - `\nClawback: ${formatAccount( - byAccount.accountId(), - )} clawing back ${amount} CLAW from ${formatAccount( - fromAccount.accountId(), - )}`, - ); - return submitAndPollTransaction( + return server.submitTransaction( buildTx(byAccount, byKey, [ sdk.Operation.clawback({ from: fromAccount.accountId(), - asset: ASSET, + asset: AstroToken, // defined in preamble amount: amount, }), ]), - `Clawback of ${amount} CLAW`, ); } +``` -// Disable clawback for a trustline by the issuer -function disableClawback(issuerAccount, issuerKeys, forTrustor) { - console.log( - `\nDisabling clawback for ${formatAccount( - forTrustor.accountId(), - )} on asset ${ASSET.code}`, - ); - return submitAndPollTransaction( - buildTx(issuerAccount, issuerKeys, [ - sdk.Operation.setTrustLineFlags({ - trustor: forTrustor.accountId(), - asset: ASSET, - flags: { - clawbackEnabled: false, - }, - }), - ]), - "Disable Clawback on Trustline", - ); +```java +// Make a payment to `toAccount` from `fromAccount` for `amount`. +private static void makePayment(AccountResponse toAccount, AccountResponse fromAccount, KeyPair fromKey, String amount) throws IOException { + Transaction tx = buildTx(fromAccount, fromKey, new Operation[]{ + new PaymentOperation.Builder( + toAccount.getAccountId(), + AstroToken, + amount + ).build() + }); + SubmitTransactionResponse response = server.submitTransaction(tx); } -// Enables clawback on A, and establishes trustlines for the CLAW asset for accounts B and C. -function preamble() { - console.log("\n=== SETTING UP CLAWBACK AND TRUSTLINES ==="); - return getAccounts().then(function (accounts) { - let [accountA, accountB, accountC] = accounts; - - return enableClawback(accountA, A) - .then(() => { - console.log("Clawback enabled successfully"); - // Get fresh accounts after enabling clawback - return getAccounts(); - }) - .then((refreshedAccounts) => { - let [newAccountA, newAccountB, newAccountC] = refreshedAccounts; - - return Promise.all([ - establishTrustline(newAccountB, B), - establishTrustline(newAccountC, C), - ]); - }) - .then(() => { - console.log("All trustlines established successfully"); - }); +// Perform a clawback by `byAccount` of `amount` from `fromAccount`. +private static void doClawback(AccountResponse byAccount, KeyPair byKey, AccountResponse fromAccount, String amount) throws IOException { + Transaction tx = buildTx(byAccount, byKey, new Operation[]{ + new ClawbackOperation.Builder( + AstroToken, + fromAccount.getAccountId(), + amount + ).build() }); + SubmitTransactionResponse response = server.submitTransaction(tx); +} +``` + +```go +// Make a payment to `toAccount` from `fromAccount` for `amount`. +func makePayment(toAccount horizon.Account, fromAccount horizon.Account, fromKey *keypair.Full, amount string) { + tx, err := buildTx(fromAccount, fromKey, []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: toAccount.AccountID, + Asset: AstroToken, + Amount: amount, + }, + }) + check(err) + resp, err := client.SubmitTransaction(tx) + check(err) +} + +// Perform a clawback by `byAccount` of `amount` from `fromAccount`. +func doClawback(byAccount horizon.Account, byKey *keypair.Full, fromAccount horizon.Account, amount string) { + tx, err := buildTx(byAccount, byKey, []txnbuild.Operation{ + &txnbuild.Clawback{ + From: fromAccount.AccountID, + Asset: AstroToken, + Amount: amount, + }, + }) + check(err) + resp, err := client.SubmitTransaction(tx) + check(err) } ``` -### Example 1: Payments +These snippets will help us with the final composition: making some payments to distribute the asset to the world and clawing some of it back. -This example will highlight how the asset issuer holds control over their asset regardless of how it gets distributed to the world. + -In this scenario: +```python +def examplePaymentClawback(): + accounts = getAccounts() + accountA, accountB, accountC = accounts -- Account A will pay Account B with 1000 tokens of its custom asset. -- Account B will then pay Account C 500 tokens in turn. -- Finally, Account A will claw back half of Account C's balance, burning 250 CLAW tokens. + makePayment(accountB, accountA, A, "1000") + makePayment(accountC, accountB, B, "500") + doClawback(accountA, A, accountC, "250") - + accounts = getAccounts() + showBalances(accounts) +``` ```js -function examplePaymentAndThenClawback() { - console.log("\n=== PAYMENT AND CLAWBACK EXAMPLE ==="); +function examplePaymentClawback() { return getAccounts() .then(function (accounts) { let [accountA, accountB, accountC] = accounts; - - // A issues 1000 CLAW to B return makePayment(accountB, accountA, A, "1000") - .then(() => { - console.log("\n--- After A → B payment ---"); - return getAccounts(); - }) - .then((refreshedAccounts) => { - [accountA, accountB, accountC] = refreshedAccounts; - return showCLAWBalances([accountA, accountB, accountC]); - }) - .then(() => { - // B sends 500 CLAW to C - return makePayment(accountC, accountB, B, "500"); - }) - .then(() => { - console.log("\n--- After B → C payment ---"); - return getAccounts(); - }) - .then((refreshedAccounts2) => { - [accountA, accountB, accountC] = refreshedAccounts2; - return showCLAWBalances([accountA, accountB, accountC]); - }) - .then(() => { - // A claws back 250 CLAW from C - return doClawback(accountA, A, accountC, "250"); - }); - }) - .then(() => getAccounts()); -} - -// Run the example with proper promise chaining -function runExample1() { - fundAccounts() - .then(() => getAccounts()) - .then(showXLMBalances) - .then(preamble) - .then(examplePaymentAndThenClawback) - .then((finalAccounts) => { - console.log("\n--- FINAL BALANCES ---"); - return showCLAWBalances(finalAccounts); - }) - .then(() => { - console.log("\n=== CLAWBACK DEMO COMPLETED ==="); + .then(makePayment(accountC, accountB, B, "500")) + .then(doClawback(accountA, A, accountC, "250")); }) - .catch((error) => { - console.error("Error in example:", error.message); - }); + .then(getAccounts) + .then(showBalances); +} + +preamble().then(examplePaymentClawback); +``` + +```java +private static void examplePaymentClawback() throws IOException { + AccountResponse[] accounts = getAccounts(); + AccountResponse accountA = accounts[0]; + AccountResponse accountB = accounts[1]; + AccountResponse accountC = accounts[2]; + + makePayment(accountB, accountA, A, "1000"); + makePayment(accountC, accountB, B, "500"); + doClawback(accountA, A, accountC, "250"); + + accounts = getAccounts(); + showBalances(accounts); +} +``` + +```go +func examplePaymentClawback() { + accounts, err := getAccounts() + check(err) + accountA := accounts[0] + accountB := accounts[1] + accountC := accounts[2] + + makePayment(accountB, accountA, A, "1000") + makePayment(accountC, accountB, B, "500") + doClawback(accountA, A, accountC, "250") + + accounts, err = getAccounts() + check(err) + showBalances(accounts) } ``` -When you invoke `runExample1()`, you should see output similar to: +After running our example, we should see the balances reflect the example flow: -```text -=== ACCOUNT SETUP === -Account A (Issuer): GDYIV7XB5M6OS4S2P3DAGIEKRXNKSYZ4LSS5VBWMFGNAO42WGBOQY2E5 -Account B (Trustor): GA7JEMMG46H6CDXR757ZUZ6HCEXE64RKC4M4DYX5DME2XP2P3LXQFVIM -Account C (Trustor): GCLJYLVE43A73KV62YVC2H7ZK4CDSGMK7IYV2NC4WHQOFRQDFTBG6ZB7 +``` +A - GCIHA...72MJN: 0 +B - GDS5N...C7KKX: 500 +C - GC2BK...CQVGF: 250 +``` -=== FUNDING ACCOUNTS WITH XLM === -All accounts funded with XLM via airdrop +
+ Full Clawback Flow Chart +```mermaid +flowchart TD + subgraph ASF["Account Setup & Funding"] + A["Account A (Issuer)"] + B["Account B (Trustor)"] + C["Account C (Trustor)"] + F["All accounts funded with 10,000 XLM"] -=== XLM BALANCES === -GDYIV7XB (Account A): 10000.0 XLM -GCLJYLVE (Account C): 10000.0 XLM -GA7JEMMG (Account B): 10000.0 XLM + A --> F + B --> F + C --> F -=== SETTING UP CLAWBACK AND TRUSTLINES === -Enabling clawback flags on account GDYIV7XB (Account A) -Enable Clawback Flags submitted: aa48d5bbf1e3c5b3b3ee6b21f4ab4dfbf52632e0d70b1f6e2a09ee33943093d5 -Enable Clawback Flags completed successfully -Clawback enabled successfully -GA7JEMMG (Account B) establishing trustline for CLAW -GCLJYLVE (Account C) establishing trustline for CLAW -Establish Trustline (GCLJYLVE (Account C)) submitted: effb33b48f126ddd54237b8942d48fdfb562d7dc07500e9db07a1b223a5ef50e -Establish Trustline (GA7JEMMG (Account B)) submitted: e05fe02b4da8dff22285da2d927545288160ec8f68c1359cefbabdfd4f0020df -Establish Trustline (GA7JEMMG (Account B)) completed successfully -Establish Trustline (GCLJYLVE (Account C)) completed successfully -All trustlines established successfully +end -=== PAYMENT AND CLAWBACK EXAMPLE === +subgraph ECT["Clawback & Trustlines"] EC["A enables clawback"] BT["B adds trustline for CLAW"] CT["C adds trustline for CLAW"] TL["Trustlines established"] -Payment: GDYIV7XB (Account A) → GA7JEMMG (Account B) (1000 CLAW) -Payment of 1000 CLAW submitted: be5cfda0f1625762b3b3b704affa356ff04e5ab388b7b63ede1fa5ca9873c96a -Payment of 1000 CLAW completed successfully + EC --> BT + EC --> CT + BT --> TL + CT --> TL ---- After A → B payment --- +end -=== CLAW BALANCES === -GDYIV7XB (Account A): 0 CLAW -GA7JEMMG (Account B): 1000.0 CLAW -GCLJYLVE (Account C): 0 CLAW +F --> EC -Payment: GA7JEMMG (Account B) → GCLJYLVE (Account C) (500 CLAW) -Payment of 500 CLAW submitted: 8a0d19c8e56487255ffe24f5453ffe78177acc6f39bad204bab2849415032555 -Payment of 500 CLAW completed successfully +subgraph S1["Send 1000 CLAW"] P1["A pays B 1000 CLAW"] S1R["CLAW after A to B
A: 0 CLAW
B: 1000 CLAW
C: 0 CLAW"] ---- After B → C payment --- + P1 --> S1R -=== CLAW BALANCES === -GDYIV7XB (Account A): 0 CLAW -GA7JEMMG (Account B): 500.0 CLAW -GCLJYLVE (Account C): 500.0 CLAW +end -Clawback: GDYIV7XB (Account A) clawing back 250 CLAW from GCLJYLVE (Account C) -Clawback of 250 CLAW submitted: e63eed593a20fb5571e8189ff549cd1360849749c06294f00596fae30da0f23d -Clawback of 250 CLAW completed successfully +TL --> P1 ---- FINAL BALANCES --- +subgraph S2["Send 500 CLAW"] P2["B pays C 500 CLAW"] S2R["CLAW after B to C
A: 0 CLAW
B: 500 CLAW
C: 500 CLAW"] -=== CLAW BALANCES === -GCLJYLVE (Account C): 250.0 CLAW -GA7JEMMG (Account B): 500.0 CLAW -GDYIV7XB (Account A): 0 CLAW + P2 --> S2R -=== CLAWBACK DEMO COMPLETED === -``` +end -
- Clawback Flow Chart +S1R --> P2 + +subgraph CB["Clawback Operation"] CB1["A clawbacks 250 CLAW from C"] CB2["Final CLAW
A: 0 CLAW
B: 500 CLAW
C: 250 CLAW"] -![example1](/assets/clawback/example1.png) + CB1 --> CB2 +end + +S2R --> CB1 + +````
-Notice that Account A (the issuer) holds none of the asset despite clawing back 250 from Account C. This should drive home the fact that clawed-back assets are burned, not transferred. +Notice that $\mathcal{A}$ (the issuer) holds none of the asset despite clawing back 250 from $\mathcal{C}$. Thus, the clawed-back assets are burned, not transferred. -It may be strange that A never holds any tokens of its custom asset, but that's exactly how issuing works: you create value where there used to be none. Sending an asset to its issuing account is equivalent to burning it, and auditing the total amount of an asset in existence is one of the benefits of properly distributing an asset via a distribution account, which we avoid doing here for example brevity. +:::info -### Example 2: Claimable Balances +It may be strange that $\mathcal{A}$ never holds any `AstroToken`, but that’s exactly how issuing works: you create value where there used to be none. Sending an asset to its issuing account is equivalent to burning it, and auditing the total amount of an asset in existence is one of the benefits of [properly distributing](../../../tokens/control-asset-access.mdx#auditing) an asset. -Direct payments aren't the only way to transfer assets between accounts: claimable balances also do this. Since they are a separate payment mechanism, they need a separate clawback mechanism. +::: + +### Example 2: Claimable Balances -In this scenario: +Direct payments aren’t the only way to transfer assets between accounts: claimable balances also do this. Since they are a separate payment mechanism, they need a separate clawback mechanism. For our example, you should be familiar with resolving [balance IDs](claimable-balances.mdx#example). -- Account A will pay Account B with 1000 tokens of its custom asset. -- Account B creates a Claimable Balance for Account C for 300 tokens. (You can query the Claimable Balance via RPC's `getLedgerEntries` endpoint.) -- Account A then claws back the Claimable Balance from Account C. -- The Claimable Balance entry is deleted and no longer queryable. +We need some additional helper methods to get started working efficiently with claimable balances: +```python +def createClaimable(fromAccount, fromKey, toAccount, amount): + tx = buildTx(fromAccount, fromKey, [ + Operation.create_claimable_balance( + asset = AstroToken, + amount = amount, + claimants = [ + Claimant( + destination = toAccount.account_id + ) + ] + ) + ]) + response = server.submit_transaction(tx) + return response + +def getBalanceId(txResponse): + txResult = xdr.TransactionResult.from_xdr(txResponse["result_xdr"], "base64") + operationResult = txResult.result.results[0] + creationResult = operationResult.tr.create_claimable_balance_result + return creationResult.balance_id.to_xdr() + +def clawbackClaimable(issuerAccount, issuerKey, balanceId): + tx = buildTx(issuerAccount, issuerKey, [ + Operation.clawback_claimable_balance(balance_id = balanceId) + ]) + return server.submit_transaction(tx) +```` + ```js -function exampleClaimableBalanceClawback() { - console.log("\n=== CLAIMABLE BALANCE CLAWBACK EXAMPLE ==="); - let balanceId; +function createClaimable(fromAccount, fromKey, toAccount, amount) { + return server.submitTransaction( + buildTx(fromAccount, fromKey, [ + sdk.Operation.createClaimableBalance({ + asset: AstroToken, + amount: amount, + claimants: [new sdk.Claimant(toAccount.accountId())], + }), + ]), + ); +} - return getAccounts() - .then(function (accounts) { - let [accountA, accountB, accountC] = accounts; +function getBalanceId(txResponse) { + const txResult = sdk.xdr.TransactionResult.fromXDR( + txResponse.result_xdr, + "base64", + ); + const operationResult = txResult.result().results()[0]; - console.log("\n--- Initial CLAW balances ---"); - return showCLAWBalances([accountA, accountB, accountC]) - .then(() => { - // A pays 1000 CLAW to B - return makePayment(accountB, accountA, A, "1000"); - }) - .then(() => { - console.log("\n--- After A → B payment ---"); - return getAccounts(); - }) - .then((refreshedAccounts) => { - [accountA, accountB, accountC] = refreshedAccounts; - return showCLAWBalances([accountA, accountB, accountC]); - }) - .then(() => { - // B creates claimable balance for C - return createClaimable(accountB, B, accountC, "300"); - }) - .then((txResp) => { - balanceId = getBalanceId(txResp); - console.log(`Claimable balance created with ID: ${balanceId}`); - - console.log("\n--- After claimable balance creation ---"); - return getAccounts() - .then((refreshedAccounts2) => { - [accountA, accountB, accountC] = refreshedAccounts2; - return showCLAWBalances([accountA, accountB, accountC]); - }) - .then(() => { - // Check that the claimable balance exists - return fetchClaimableBalance( - balanceId, - "claimable balance after creation", - ); - }) - .then(() => { - // A claws back the claimable balance - return clawbackClaimable(accountA, A, balanceId); - }) - .then(() => { - // Check that the claimable balance no longer exists - return fetchClaimableBalance( - balanceId, - "claimable balance after clawback", - ); - }); - }); - }) - .then(() => getAccounts()); -} - -// Run the example with proper promise chaining -function runExample2() { - fundAccounts() - .then(() => getAccounts()) - .then(showXLMBalances) - .then(preamble) - .then(exampleClaimableBalanceClawback) - .then((finalAccounts) => { - console.log("\n--- FINAL BALANCES ---"); - return showCLAWBalances(finalAccounts); - }) - .then(() => { - console.log("\n=== CLAIMABLE BALANCE CLAWBACK DEMO COMPLETED ==="); - }) - .catch((error) => { - console.error("Error in example:", error.message); - }); + let creationResult = operationResult.value().createClaimableBalanceResult(); + return creationResult.balanceId().toXDR("hex"); +} + +function clawbackClaimable(issuerAccount, issuerKey, balanceId) { + return server.submitTransaction( + buildTx(issuerAccount, issuerKey, [ + sdk.Operation.clawbackClaimableBalance({ balanceId }), + ]), + ); } ``` - +```java +public static SubmitTransactionResponse createClaimable( + AccountResponse fromAccount, + KeyPair fromKey, + AccountResponse toAccount, + String amount +) throws IOException { + Transaction tx = buildTx( + fromAccount, + fromKey, + new Operation[]{ + CreateClaimableBalanceOperation.Builder(AstroToken, amount) + .addClaimant( + Claimant.create( + toAccount.getAccountId() + ) + ) + .build() + } + ); + return server.submitTransaction(tx); +} -When you invoke `runExample2()`, you should see output similar to: +public static String getBalanceId(SubmitTransactionResponse txResponse) { + XdrDataInputStream txResultStream = new XdrDataInputStream( + Base64.getDecoder().decode(txResponse.getResultXdr()) + ); + TransactionResult txResult = TransactionResult.decode(txResultStream); + OperationResult opResult = txResult.getResult().getResults()[0]; + CreateClaimableBalanceResult creationResult = opResult.getTr().getCreateClaimableBalanceResult(); + return creationResult.getBalanceId().toXdrBase64(); +} +public static SubmitTransactionResponse clawbackClaimable( + AccountResponse issuerAcc, + KeyPair issuerKey, + String balanceId +) throws IOException { + Transaction tx = buildTx(issuerAcc, issuerKey, new Operation[]{ + ClawbackClaimableBalanceOperation.Builder(balanceId).build() + }); + return server.submitTransaction(tx); +} ``` -=== ACCOUNT SETUP === -Account A (Issuer): GBOK4XIKNCKVWKRG27EEMYX2H7H5GP6ZCLYJTORHJVV3ZDJZJXTUPKTJ -Account B (Trustor): GBI5XUOWLBL44DWJXURGQJX46TUSNEPQ553LZUGLFCVQ6KRRPEEFBPKE -Account C (Trustor): GANXLMMUIG7G5NT6GQZNE3OPCRCROYJLTR6PGRZHVURUGNURSF3H3ZEZ - -=== FUNDING ACCOUNTS WITH XLM === -All accounts funded with XLM via airdrop -=== XLM BALANCES === -GBI5XUOW (Account B): 10000.0 XLM -GANXLMMU (Account C): 10000.0 XLM -GBOK4XIK (Account A): 10000.0 XLM +```go +func createClaimable( + fromAccount horizon.Account, + fromKey *keypair.Full, + toAccount horizon.Account, + amount string +) (*horizon.Transaction, error) { + tx, err := buildTx(fromAccount, fromKey, []txnbuild.Operation{ + &txnbuild.CreateClaimableBalance{ + Asset: AstroToken, + Amount: amount, + Claimants: []txnbuild.Claimant{ + txnbuild.NewClaimant( + toAccount.AccountID + ) + }, + } + }) + check(err) + return client.SubmitTransaction(tx) +} -=== SETTING UP CLAWBACK AND TRUSTLINES === -Enabling clawback flags on account GBOK4XIK (Account A) -Enable Clawback Flags submitted: 4812d2be7e8652dbeb29fd4d9387c71725e09e5f1d3500b3e913eebc9bec03b1 -Enable Clawback Flags completed successfully -Clawback enabled successfully -GBI5XUOW (Account B) establishing trustline for CLAW -GANXLMMU (Account C) establishing trustline for CLAW -Establish Trustline (GANXLMMU (Account C)) submitted: eb2c17135109408d751cdbab9235c8a833922a66c6576fe76a7930a2ef9e7042 -Establish Trustline (GBI5XUOW (Account B)) submitted: 1ebb5ed64cb1c5c9beb121d56c31b802fab885ef06b771b3d8617371482e115d -Establish Trustline (GANXLMMU (Account C)) completed successfully -Establish Trustline (GBI5XUOW (Account B)) completed successfully -All trustlines established successfully +func getBalanceId(txResponse *horizon.Transaction) (string, error) { + txResultBytes, err := base64.StdEncoding.DecodeString(txResponse.ResultXdr) + check(err) + var txResult xdr.TransactionResult + _, err = xdr.Unmarshal(txResultBytes, &txResult) + check(err) + operationResult := txResult.Result.Results[0] + creationResult := operationResult.Tr.CreateClaimableBalanceResult + return creationResult.BalanceId.ToXDR(), nil +} -=== CLAIMABLE BALANCE CLAWBACK EXAMPLE === +func clawbackClaimable( + issuerAcc horizon.Account, + issuerKey *keypair.Full, + balanceId string +) (*horizon.Transaction, error) { + tx, err := buildTx(issuerAcc, issuerKey, []txnbuild.Operation{ + &txnbuild.ClawbackClaimableBalance{ + BalanceID: balanceId, + } + }) + check(err) + return client.SubmitTransaction(tx) +} +``` ---- Initial CLAW balances --- + -=== CLAW BALANCES === -GANXLMMU (Account C): 0 CLAW -GBI5XUOW (Account B): 0 CLAW -GBOK4XIK (Account A): 0 CLAW +Now we can fulfill the flow: $\mathcal{A}$ pays $\mathcal{B}$, who sends a claimable balance to $\mathcal{C}$, who gets it clawed back by $\mathcal{A}$. (Note that we rely on the `makePayment` helper from the earlier example.) -Payment: GBOK4XIK (Account A) → GBI5XUOW (Account B) (1000 CLAW) -Payment of 1000 CLAW submitted: ec852daea2fea4e5b6a1d56530618f30642d9207767f30558d577b4f98e59850 -Payment of 1000 CLAW completed successfully + ---- After A → B payment --- +```python +def exampleClaimableBalanceClawback(): + accounts = getAccounts() + accountA, accountB, accountC = accounts -=== CLAW BALANCES === -GANXLMMU (Account C): 0.0 CLAW -GBI5XUOW (Account B): 1000.0 CLAW -GBOK4XIK (Account A): 0 CLAW + makePayment(accountB, accountA, A, "1000") + txResp = createClaimable(accountB, B, accountC, "500") + balanceId = getBalanceId(txResp) + clawbackClaimable(accountA, A, balanceId) -Creating claimable balance: GBI5XUOW (Account B) → GANXLMMU (Account C) (300 CLAW) -Create Claimable Balance of 300 CLAW submitted: 743066775839ef5112fd2b8a26730700a986655c557abcb98b91c6fdbd12abde -Create Claimable Balance of 300 CLAW completed successfully -Claimable balance created with ID: 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c + accounts = getAccounts() + showBalances(accounts) +``` ---- After claimable balance creation --- +```js +function exampleClaimableBalanceClawback() { + return getAccounts() + .then(function (accounts) { + let [accountA, accountB, accountC] = accounts; -=== CLAW BALANCES === -GBI5XUOW (Account B): 700.0 CLAW -GBOK4XIK (Account A): 0 CLAW -GANXLMMU (Account C): 0 CLAW + return makePayment(accountB, accountA, A, "1000") + .then(() => createClaimable(accountB, B, accountC, "500")) + .then((txResp) => clawbackClaimable(accountA, A, getBalanceId(txResp))); + }) + .then(getAccounts) + .then(showBalances); +} +``` ---- Checking claimable balance after creation --- -Looking up balance ID: 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c -✅ Found claimable balance - Amount: 300.0 CLAW - Number of claimants: 1 - Claimant 1: GANXLMMU (Account C) +```java +public static void exampleClaimableBalanceClawback() throws IOException { + AccountResponse[] accounts = getAccounts(); + AccountResponse accountA = accounts[0]; + AccountResponse accountB = accounts[1]; + AccountResponse accountC = accounts[2]; + + makePayment(accountB, accountA, A, "1000"); + SubmitTransactionResponse txResp = createClaimable( + accountB, + B, + accountC, + "500" + ); + clawbackClaimable(accountA, A, getBalanceId(txResp)); -Clawback claimable balance: GBOK4XIK (Account A) clawing back balance 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c -Clawback Claimable Balance submitted: a82a0ed067d34361da622a56a3b6d59a7c4a351414a69da4dc9df8a5728e7758 -Clawback Claimable Balance completed successfully + accounts = getAccounts(); + showBalances(accounts); +} +``` ---- Checking claimable balance after clawback --- -Looking up balance ID: 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c -❌ Claimable balance not found (Claimable balance 0000000091b5fe84a029c79d409ac88d34b7047a6cc9f95b2c2f965843db122ef70fac2c not found) +```go +func exampleClaimableBalanceClawback() { + accounts, err := getAccounts() + check(err) + accountA := accounts[0] + accountB := accounts[1] + accountC := accounts[2] + + _, err = makePayment(accountB, accountA, A, "1000") + check(err) + txResp, err := createClaimable(accountB, B, accountC, "500") + check(err) + _, err = clawbackClaimable(accountA, A, getBalanceId(txResp)) + check(err) + + accounts, err = getAccounts() + check(err) + showBalances(accounts) +} +``` ---- FINAL BALANCES --- + -=== CLAW BALANCES === -GBI5XUOW (Account B): 700.0 CLAW -GBOK4XIK (Account A): 0 CLAW -GANXLMMU (Account C): 0.0 CLAW +After running `preamble().then(examplePaymentClawback)`, we should see the balances reflect our flow: -=== CLAIMABLE BALANCE CLAWBACK DEMO COMPLETED === +``` +A - GCIHA...72MJN: 0 +B - GDS5N...C7KKX: 500 +C - GC2BK...CQVGF: 0 ``` ### Example 3: Selectively Enabling Clawback -When you enable the `AUTH_CLAWBACK_ENABLED_FLAG` on your account, it will make all future trustlines have clawback enabled for any of your issued assets. This may not always be desirable as you may want certain assets to behave as they did before. Though you could work around this by reissuing assets from a “dedicated clawback” account, you can also simply disable clawbacks for certain trustlines by clearing the `TRUST_LINE_CLAWBACK_ENABLED_FLAG` on a trustline. +When you enable the `AUTH_CLAWBACK_ENABLED_FLAG` on your account, it will make all future trustlines have clawback enabled for any of your issued assets. This may not always be desirable, as you may want certain assets to behave as they did before. Though you could work around this by reissuing assets from a “dedicated clawback” account, you can also simply disable clawbacks for certain trustlines by clearing the `TRUSTLINE_CLAWBACK_ENABLED_FLAG` on a trustline. + +In the following example, we’ll have an account $\mathcal{A}$ issue a new asset and distribute it to a second account $\mathcal{B}$. Next, we’ll demonstrate how $\mathcal{A}$ claws back some of the assets from $\mathcal{B}$, then clears the trustline and can no longer claw back the asset. + +First, let’s prepare the accounts using the helper functions defined in the earlier examples: + + + +```python +def preambleRedux(): + accounts = getAccounts() + enableClawback(accounts[0], A) + establishTrustline(accounts[1], B) +``` + +```js +function preambleRedux() { + return getAccounts().then((accounts) => { + return enableClawback(accounts[0], A).then(() => + establishTrustline(accounts[1], B), + ); + }); +} +``` -In this scenario: +```java +public static void preambleRedux() throws IOException { + AccountResponse[] accounts = getAccounts(); + enableClawback(accounts[0], A); + establishTrustline(accounts[1], B); +} +``` -- Account A issues an asset and sends 1000 tokens to a distribution account (Account B). -- Account A claws back 500 tokens from Account B. -- Account A then clears the trustline so that it (the issuer) can no longer clawback the asset. -- Account A then attempts to clawback 250 tokens from Account B and fails. +```go +func preambleRedux() { + accounts, err := getAccounts() + check(err) + err = enableClawback(accounts[0], A) + check(err) + err = establishTrustline(accounts[1], B) + check(err) +} +``` -_Please note that Account C is not relevant in this example._ + + +Now, let’s distribute some of our asset to $\mathcal{B}$, just to claw it back. Then, we’ll clear the flag from the trustline and show that another clawback isn’t possible: +```python +def disableClawback(issuerAccount, issuerKeys, forTrustor): + tx = buildTx(issuerAccount, issuerKeys, [ + Operation.set_trust_line_flags( + trustor = forTrustor.account_id, + asset = AstroToken, + clear_flags = Operation.Flag.TRUSTLINE_CLAWBACK_ENABLED_FLAG, + ) + ]) + response = server.submit_transaction(tx) + return response + +def exampleSelectiveClawback(): + accounts = getAccounts() + accountA, accountB = accounts + + makePayment(accountB, accountA, A, "1000") + accounts = getAccounts() + showBalances(accounts) + + doClawback(accountA, A, accountB, "500") + accounts = getAccounts() + showBalances(accounts) + + disableClawback(accountA, A, accountB) + + try: + doClawback(accountA, A, accountB, "500") + except Exception as e: + if 'op_not_clawback_enabled' in str(e): + print("Clawback failed, as expected!") + else: + print("Uh-oh, other failure occurred") + + accounts = getAccounts() + showBalances(accounts) +``` + ```js -function exampleSelectiveClawbackThenDisableClawback() { - console.log("\n=== SELECTIVE CLAWBACK EXAMPLE ==="); +function disableClawback(issuerAccount, issuerKeys, forTrustor) { + return server.submitTransaction( + buildTx(issuerAccount, issuerKeys, [ + sdk.Operation.setTrustLineFlags({ + trustor: forTrustor.accountId(), + asset: AstroToken, + flags: { + clawbackEnabled: false, + }, + }), + ]), + ); +} + +function exampleSelectiveClawback() { return getAccounts() .then((accounts) => { let [accountA, accountB] = accounts; - - console.log("\n--- Initial CLAW balances ---"); - return showCLAWBalances([accountA, accountB]) - .then(() => { - // A pays 1000 CLAW to B - return makePayment(accountB, accountA, A, "1000"); - }) - .then(() => { - console.log("\n--- After A → B payment ---"); - return getAccounts(); - }) - .then((refreshedAccounts) => { - [accountA, accountB] = refreshedAccounts; - return showCLAWBalances([accountA, accountB]); - }) - .then(() => { - // A claws back 500 CLAW from B (should work) - return doClawback(accountA, A, accountB, "500"); - }) - .then(() => { - console.log("\n--- After first clawback ---"); - return getAccounts(); - }) - .then((refreshedAccounts2) => { - [accountA, accountB] = refreshedAccounts2; - return showCLAWBalances([accountA, accountB]); - }) - .then(() => { - // A disables clawback for B's trustline - return disableClawback(accountA, A, accountB); - }) - .then(() => { - // Try to clawback again (should fail) - return doClawback(accountA, A, accountB, "250"); - }) + return makePayment(accountB, accountA, A, "1000") + .then(getAccounts) + .then(showBalances) + .then(() => doClawback(accountA, A, accountB, "500")) + .then(getAccounts) + .then(showBalances) + .then(() => disableClawback(accountA, A, accountB)) + .then(() => doClawback(accountA, A, accountB, "500")) .catch((err) => { - console.log("Error:", err.message); + if (err.response && err.response.data) { + // This is a very specific way to check for errors, + // and you should probably never do it this way. + // We do this to demonstrate that the clawback + // error *does* occur as expected here. + const opErrors = err.response.data.extras.result_codes.operations; + if ( + opErrors && + opErrors.length > 0 && + opErrors[0] === "op_not_clawback_enabled" + ) { + console.log("Clawback failed, as expected!"); + } else { + console.log("Uh-oh, other failure occurred"); + } + } else { + console.error("Uh-oh, unknown failure"); + } }); }) - .then(() => getAccounts()); -} - -// Run the example with proper promise chaining -function runExample3() { - fundAccounts() - .then(() => getAccounts()) - .then((accounts) => showXLMBalances([accounts[0], accounts[1]])) // Only show A and B - .then(preamble) // This sets up clawback and trustlines for A, B, C - .then(exampleSelectiveClawbackThenDisableClawback) - .then((finalAccounts) => { - console.log("\n--- FINAL BALANCES ---"); - return showCLAWBalances([finalAccounts[0], finalAccounts[1]]); // Only show A and B - }) - .then(() => { - console.log("\n=== SELECTIVE CLAWBACK DEMO COMPLETED ==="); - }) - .catch((error) => { - console.error("Error in example:", error.message); - }); + .then(getAccounts) + .then(showBalances); } ``` - +```java +public static SubmitTransactionResponse disableClawback( + AccountResponse issuerAccount, + KeyPair issuerKeys, + AccountResponse forTrusto +) throws IOException { + Transaction transaction = buildTx(issuerAccount, issuerKeys, new Operation[]{ + new SetTrustLineFlagsOperation.Builder( + forTrustor.getAccountId(), + AstroToken + ) + .setClearFlags(SetTrustLineFlagsOperation.Flag.TRUSTLINE_CLAWBACK_ENABLED_FLAG) + .build() + }); + return server.submitTransaction(transaction); +} -When you invoke `runExample3()`, you should see output similar to: +public static void exampleSelectiveClawback() throws IOException { + AccountResponse[] accounts = getAccounts(); + AccountResponse accountA = accounts[0]; + AccountResponse accountB = accounts[1]; -```text -=== ACCOUNT SETUP === -Account A (Issuer): GCTYN2SAMM2SHM5LOCHS2P2I24MEHVYKHKCSO5XPR2H2GQCK6PFCQRK6 -Account B (Trustor): GAWDYTIKQI7J3YSCSLWGAJPN6J62WQKDA3XCAC55DDRH5KCTEZ5IGGWP -Account C (Trustor): GC5KQ7H5G5E65OVZGKBVCXBJKTEHWBBLQ56L7DK4ATUHUH5VJP4YHQVC + makePayment(accountB, accountA, A, "1000"); + accounts = getAccounts(); + showBalances(accounts); -=== FUNDING ACCOUNTS WITH XLM === -All accounts funded with XLM via airdrop + doClawback(accountA, A, accountB, "500"); + accounts = getAccounts(); + showBalances(accounts); -=== XLM BALANCES === -GCTYN2SA (Account A): 10000.0 XLM -GAWDYTIK (Account B): 10000.0 XLM + disableClawback(accountA, A, accountB); + try { + doClawback(accountA, A, accountB, "500"); + } catch (Exception e) { + // This is a very specific way to check for errors, + // and you should probably never do it this way. + // We do this to demonstrate that the clawback + // error *does* occur as expected here. + if (e.getMessage().contains("op_not_clawback_enabled")) { + System.out.println("Clawback failed, as expected!"); + } else { + System.out.println("Uh-oh, some other failure"); + } + } -=== SETTING UP CLAWBACK AND TRUSTLINES === -Enabling clawback flags on account GCTYN2SA (Account A) -Enable Clawback Flags submitted: 52e7e15d70e46dc3b551e787ec3be11692d2cc8e0b2fda070cebfa048c67cedd -Enable Clawback Flags completed successfully -Clawback enabled successfully -GAWDYTIK (Account B) establishing trustline for CLAW -GC5KQ7H5 (Account C) establishing trustline for CLAW -Establish Trustline (GAWDYTIK (Account B)) submitted: 3cf49bf597085ede004d566d151b9a917b31d1dbbfa81d71a64b615356702da7 -Establish Trustline (GC5KQ7H5 (Account C)) submitted: 9250e62be27e3111924d36c0169f3b08e12565f8f842bf32a8a7a0efb098a080 -Establish Trustline (GAWDYTIK (Account B)) completed successfully -Establish Trustline (GC5KQ7H5 (Account C)) completed successfully -All trustlines established successfully + accounts = getAccounts(); + showBalances(accounts); +} +``` -=== SELECTIVE CLAWBACK EXAMPLE === +```go +func disableClawback( + issuerAccount horizon.Account, + issuerKey *keypair.Full, + forTrustor horizon.Account +) { + tx, err := buildTx(issuerAccount, issuerKey, []txnbuild.Operation{ + &txnbuild.SetTrustLineFlags{ + Trustor: forTrustor.AccountID, + Asset: AstroToken, + ClearFlags: []txnbuild.TrustLineFlags{ + txnbuild.TrustLineClawbackEnabledFlag + }, + }, + }) + check(err) + _, err = client.SubmitTransaction(tx) + return err +} + +func exampleSelectiveClawback() error { + accounts, err := getAccounts() + check(err) + accountA := accounts[0] + accountB := accounts[1] + + _, err = makePayment(accountB, accountA, A, "1000") + check(err) + accounts, err = getAccounts() + check(err) + showBalances(accounts) + + _, err = doClawback(accountA, A, accountB, "500") + check(err) + accounts, err = getAccounts() + check(err) + showBalances(accounts) + + err = disableClawback(accountA, A, accountB) + check(err) + _, err = doClawback(accountA, A, accountB, "500") + if err != nil { + if err.Error() == "op_not_clawback_enabled" { + fmt.Println("Clawback failed, as expected!") + } else { + return fmt.Println("Uh-oh, some other failure occurred") + } + } + + accounts, err = getAccounts() + check(err) + showBalances(accounts) +} +``` ---- Initial CLAW balances --- + -=== CLAW BALANCES === -GCTYN2SA (Account A): 0 CLAW -GAWDYTIK (Account B): 0 CLAW +Next, we'll run the example: -Payment: GCTYN2SA (Account A) → GAWDYTIK (Account B) (1000 CLAW) -Payment of 1000 CLAW submitted: 9974f7a8f85dbe437a18364066959718ceeeaa3ae9a84ccd0e91da5f4e6bfaeb -Payment of 1000 CLAW completed successfully + ---- After A → B payment --- +```python +preambleRedux() +exampleSelectiveClawback() +``` -=== CLAW BALANCES === -GAWDYTIK (Account B): 1000.0 CLAW -GCTYN2SA (Account A): 0 CLAW +```js +preambleRedux().then(exampleSelectiveClawback); +``` -Clawback: GCTYN2SA (Account A) clawing back 500 CLAW from GAWDYTIK (Account B) -Clawback of 500 CLAW submitted: b1729001fba89198f67bcff2de7fb47ea97c358e902688d88b76c6ff3947cec2 -Clawback of 500 CLAW completed successfully +```java +preambleRedux(); +exampleSelectiveClawback(); +``` ---- After first clawback --- +```go +preambleRedux() +exampleSelectiveClawback() +``` -=== CLAW BALANCES === -GAWDYTIK (Account B): 500.0 CLAW -GCTYN2SA (Account A): 0 CLAW + -Disabling clawback for GAWDYTIK (Account B) on asset CLAW -Disable Clawback on Trustline submitted: a1fe18c12628806bac90936f563978d1f9a3333a1a77482d55d3b7af99e679a7 -Disable Clawback on Trustline completed successfully +And then we can observe its result: -Clawback: GCTYN2SA (Account A) clawing back 250 CLAW from GAWDYTIK (Account B) -Clawback of 250 CLAW submitted: eab31d237832b513ca911cd2b4a4466a7b7b502f274dcc926b9910223fc2043f -Clawback of 250 CLAW failed: FAILED +``` +A - GCIHA...72MJN: 0 +B - GDS5N...C7KKX: 1000 ---- FINAL BALANCES --- +A - GCIHA...72MJN: 0 +B - GDS5N...C7KKX: 500 -=== CLAW BALANCES === -GAWDYTIK (Account B): 500.0 CLAW -GCTYN2SA (Account A): 0 CLAW +Clawback failed, as expected! -=== SELECTIVE CLAWBACK DEMO COMPLETED === +A - GCIHA...72MJN: 0 +B - GDS5N...C7KKX: 500 ``` diff --git a/docs/build/guides/transactions/create-account.mdx b/docs/build/guides/transactions/create-account.mdx index f7af4d5d8b..dfa6d8cc38 100644 --- a/docs/build/guides/transactions/create-account.mdx +++ b/docs/build/guides/transactions/create-account.mdx @@ -1,5 +1,5 @@ --- -title: Create an account +title: Create an Account sidebar_position: 5 description: Learn about creating Stellar accounts, keypairs, funding, and account basics. --- diff --git a/docs/build/guides/transactions/fee-bump-transactions.mdx b/docs/build/guides/transactions/fee-bump-transactions.mdx index 9831fb7503..a591bba6d9 100644 --- a/docs/build/guides/transactions/fee-bump-transactions.mdx +++ b/docs/build/guides/transactions/fee-bump-transactions.mdx @@ -1,5 +1,5 @@ --- -title: Fee-bump transactions +title: Fee-bump Transactions description: Use fee-bump transactions to pay for transaction fees on behalf of another account without re-signing the transaction. sidebar_position: 40 --- diff --git a/docs/build/guides/transactions/path-payments.mdx b/docs/build/guides/transactions/path-payments.mdx index e1f907eab0..8fdcb7862e 100644 --- a/docs/build/guides/transactions/path-payments.mdx +++ b/docs/build/guides/transactions/path-payments.mdx @@ -1,30 +1,72 @@ --- -title: Path payments +title: Path Payments sidebar_position: 60 description: Send a payment where the asset received differs from the asset sent. --- -In a path payment, the asset received differs from the asset sent. Rather than the operation transferring assets directly from one account to another, path payments cross through the SDEX and/or liquidity pools before arriving at the destination account. For the path payment to succeed, there has to be a DEX offer or liquidity pool exchange path in existence. It can sometimes take several hops of conversion to succeed. +!req glossary ennbtry with jed diction: -For example: +"cross-asset transactions" -Account A sells XLM → [buy XLM / sell ETH → buy ETH / sell BTC → buy BTC / sell USDC] → Account B receives USDC +path p ayments let [accounts](../../../learn/fundamentals/stellar-data-structures/accounts.mdx) send one asset to a recipeint , whoi receiveds a differnt asset. The secure conversion takes advantagfe of the netowrk's [natie liquidity](https://todo-old-summmary-readme.co) to ewxchange using istant avaliable trades. -It is possible for path payments to fail if there are no viable exchange paths. +The [operations](../../../learn/fundamentals/transactions/operations-and-transactions.mdx#operations) specify a minimum amount which the market must meet or excceed. For instance, if you send 10 bananas and rquire a receiver to get at last 15 appleess, then the trahsfer will seek the best exxchange at or above 1.5 each. -For more information on the Stellar Decentralized Exchange and Liquidity Pools, see our [Liquidity on Stellar: SDEX and Liquidity Pools Encyclopedia Entry](../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx). +TRhis let accoutns transfer value without mandating a single universal currency. You converting one asset ito another at the point of transferring value. -## Operations +## Routign logic -Path payments use the Path Payment Strict Send or Path Payment Strict Receive operations. +!see below qupte! -### Path Payment Strict Send +Pay payments use [the DEX](../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#orderbook) or [AMM pools](../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#amms) whcih standy readyt t oswap assets at the time of a trnasaction. They cna only consume existing liquidity by drawing on offers previously posted. -Allows a user to specify the amount of the asset to send. The amount received will vary based on offers in the order books and/or liquidity pools. +Tje tramsfer suicceeds oiF nd only if there's enough eavlaible interest in the [ledger](../../../learn/fundamentals/stellar-data-structures/ledgers/README.mdx) submitted, at the minimum rate. Without this liquiddyty, the cohnversion fails and returns an [errro code](../../../data/apis/horizon/api-reference/errors/README.mdx). -### Path Payment Strict Receive +To prepatre for + +#### Path Hops + +When sending path payments, your transfer can hop between up to six order books or AMMs to find the best price. + +Validators perform this arithmatic automatically, allowing you to specify only the lowest total amount you will accept. + +At each step in the path, the network calculates the optimal source of liquidity to convert through given your destination asset. + +Both the order book and AMMs coexist, providing multiple avenues for liquidity. + +Instead of having to choose whether to go through the order book or an AMM, the pathfinding algorithm automatically checks both sources of liquidity and executes new trades using whichever offers the better rate. + +It also exchanges with an AMM over an order book at each step if the entire conversation happens at a price equal to or better than limit offers. + +## paths {#pathfinding} + +[s](../../../data/apis/horizon/api-reference/aggregations/paths/README.mdx) when you call this ou;ll notice differnt `soruce_amoutns` in /data/apis/horizon/api-reference/list-strict-receive-payment-paths or differnt `destination_amount` in docs/data/apis/horizon/api-reference/list-strict-send-payment-paths + +### Converting Path Paymtns + +offers or AMMs which convert to a minimum specified amount or better. + +Transfers automatically use up to six different orderbooks to get the best price, as fully defined in [the Encyclopedia page here] . + +Some assets will have a small or nonexistent order book between them. In these cases, Stellar facilitates path payments, which we’ll discuss later. + +### Atomicity + +payh maymentn operations take adntage of every teerasnaction's exclusive ability to [succeed i n full](../../../learn/fundamentals/transactions/operations-and-transactions.mdx) or fail. + +This means that your transfer off 10 or more AstroDollars will not + +In a path payment, the asset received differs from the asset sent. Rather than the operation transferring assets directly from one account to another, path payments cross through + +before arriving at the destination account. + +For the path payment to succeed, there has to be enough liquidity path in existence. Conversions can take up to six independent hops to succeed at the best avaliable price. -Allows a user to specify the amount of the asset received. The amount sent will vary based on the offers in the order books/liquidity pools. +### Example + +Account A sells XLM → [buy XLM / sell ETH → buy ETH / sell BTC → buy BTC / sell USDC] → Account B receives USDC {/* Image suggester here in #944 */} + +It is possible for path payments to fail if there are no viable exchange paths. ## Path payments - more info @@ -33,10 +75,60 @@ Allows a user to specify the amount of the asset received. The amount sent will - This is especially important when (`Destination, Destination Asset) == (Source, Send Asset`) as this provides a functionality equivalent to getting a no-interest loan for the duration of the operation. - `Destination min` is a protective measure, it allows you to specify a lower bound for an acceptable conversion. If offers in the order books are not favorable enough for the operation to deliver that amount, the operation will fail. +## Operations + +Path payments use the Path Payment Strict Send or Path Payment Strict Receive operations. + +### Path Payment Strict Send + +[errors](../../../data/apis/horizon/api-reference/errors/result-codes/operation-specific/path-payment-strict-send.mdx) Allows a user to specify the amount of the asset to send. The amount received will vary based on offers in the order books and/or liquidity pools. + +https://developers.stellar.org/docs/learn/fundamentals/transactions/list-of-operations#path-payment-strict-send + +### Path Payment Strict Receive + +[errors](../../../data/apis/horizon/api-reference/errors/result-codes/operation-specific/path-payment-strict-receive.mdx) Allows a user to specify the amount of the asset received. The amount sent will vary based on the offers in the order books/liquidity pools. + ## Example First, ensure the receiver has a trustline established for the asset they will receive. In this example, we will use USDC as the asset received. The sender will send XLM, which will be converted to USDC through the path payment operation. + + +```python +from stellar_sdk import Server, Keypair, Asset, TransactionBuilder, Network, Memo + +USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" # USDC issuer on Testnet +USDC_ASSET = Asset("USDC", USDC_ISSUER) + +RECEIVER_SECRET = "S..." # Receiver secret +SENDER_SECRET = "S..." # Sender secret + +HORIZON_SERVER = Server("https://horizon-testnet.stellar.org") + +receiverKP = Keypair.from_secret(RECEIVER_SECRET) +receiverAccount = HORIZON_SERVER.load_account(receiverKP.public_key) + +trustTx = ( + TransactionBuilder( + source_account=receiverAccount, + network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, + base_fee=HORIZON_SERVER.fetch_base_fee() + ) + .append_change_trust_op( + asset=USDC_ASSET, + limit="10" + ) + .add_memo(Memo.text("Trusting USDC")) + .set_timeout(30) + .build() +) + +trustTx.sign(receiverKP) +trustResp = HORIZON_SERVER.submit_transaction(trustTx) +print("Trustline response:", trustResp) +``` + ```js import { Horizon, @@ -51,9 +143,10 @@ import { const USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; // USDC issuer on Stellar Testnet const USDC_ASSET = new Asset("USDC", USDC_ISSUER); // USDC asset on Stellar Testnet -const RECEIVER_SECRET = "S..."; // Receiver's secret key +const RECEIVER_SECRET = "S..."; // Receiver's secret key const SENDER_SECRET = "S..."; // Sender's secret key + const horizonServer = new Horizon.Server("https://horizon-testnet.stellar.org"); // Create a USDC trustline for the receiver @@ -78,7 +171,139 @@ const resp = await SERVER.submitTransaction(transaction); console.log("resp", resp); ``` -Now let's send a path payment from the sender to the receiver, converting XLM to USDC. +```java +import org.stellar.sdk.*; +import org.stellar.sdk.requests.AccountsRequestBuilder; +import org.stellar.sdk.responses.AccountResponse; + +public class pathPay { + static final String HORIZON_SERVER_URL = "https://horizon-testnet.stellar.org"; + static final String USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; // Testnet issuer + + static final String RECEIVER_SECRET = "S..."; // Receiver secret seed + static final String SENDER_SECRET = "S..."; // Sender secret seed + + public static void main(String[] args) { + Server server = new Server(HORIZON_SERVER_URL); + + // Assets (use ChangeTrustAsset for trustline; Asset for payments) + ChangeTrustAsset usdcTrust = ChangeTrustAsset.createNonNativeAsset("USDC", USDC_ISSUER); + Asset usdcAsset = Asset.createNonNativeAsset("USDC", USDC_ISSUER); + + KeyPair receiverKP = KeyPair.fromSecretSeed(RECEIVER_SECRET); + AccountResponse receiverAccount = server.accounts().account(receiverKP.getAccountId()); + + Transaction trustTx = new TransactionBuilder(receiverAccount, Network.TESTNET) + .setBaseFee(Transaction.MIN_BASE_FEE) // you can raise this if network load is high + .setTimeout(30) + .addOperation( + new ChangeTrustOperation.Builder(usdcTrust, "10").build() + ) + .addMemo(Memo.text("Trusting USDC")) + .build(); + + trustTx.sign(receiverKP); + SubmitTransactionResponse trustResp = server.submitTransaction(trustTx); + System.out.println("Trustline response: " + trustResp.isSuccess()); + ... +``` + +```go +package main +import ( + "context" + "fmt" + "log" + "github.com/stellar/go-stellar-sdk/clients/horizonclient" + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/network" + "github.com/stellar/go-stellar-sdk/txnbuild" +) + +const HORIZON_SERVER_URL = "https://horizon-testnet.stellar.org" +const USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" // Testnet USDC issuer + +const RECEIVER_SECRET = "S..." // Receiver secret seed +const SENDER_SECRET = "S..." // Sender secret seed + +func main() { + ctx := context.Background() + + client := &horizonclient.Client{ + HorizonURL: HORIZON_SERVER_URL, + } + + // USDC asset definition + usdcAsset := txnbuild.CreditAsset{Code: "USDC", Issuer: USDC_ISSUER} + + // ========================================================== + // Step 1: Receiver creates a trustline to USDC (limit 10) + // ========================================================== + receiverKP, err := keypair.ParseFull(RECEIVER_SECRET) + check(err) + + receiverReq := horizonclient.AccountRequest{AccountID: receiverKP.Address()} + receiverAccount, err := client.AccountDetail(receiverReq) + check(err) + + trustOp := &txnbuild.ChangeTrust{ + Line: usdcAsset, + Limit: "10", + } + + trustTx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ + SourceAccount: &receiverAccount, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee * 100, // similar to BASE_FEE * 100 in your JS + Timebounds: txnbuild.NewTimeout(30), + Operations: []txnbuild.Operation{trustOp}, + Memo: txnbuild.MemoText("Trusting USDC"), + }) + check(err) + + trustTx, err = trustTx.Sign(network.TestNetworkPassphrase, receiverKP) + check(err) + trustTxB64, err := trustTx.Base64() + check(err) + trustResp, err := client.SubmitTransactionXDR(ctx, trustTxB64) + check(err) + fmt.Println("Trustline response: ", trustResp.Hash) +... +``` + + + +Now let's send a path payment from the sender to the receiver, converting XLM to USDC: + + + +```python +senderKP = Keypair.from_secret(SENDER_SECRET) +senderAccount = HORIZON_SERVER.load_account(senderKP.public_key) + +paymentTx = ( + TransactionBuilder( + source_account=senderAccount, + network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, + base_fee=HORIZON_SERVER.fetch_base_fee() + ) + .append_path_payment_strict_receive_op( + send_asset=Asset.native(), # Send XLM + send_max="10", # Max XLM to spend + destination=receiverKP.public_key, # Receiver + dest_asset=USDC_ASSET, # Receiver gets USDC + dest_amount="1" # Exactly 1 USDC + # path=[] # Optional explicit path + ) + .add_memo(Memo.text("XLM to USDC")) + .set_timeout(30) + .build() +) + +paymentTx.sign(senderKP) +payResp = HORIZON_SERVER.submit_transaction(paymentTx) +print("Payment response:", payResp) +``` ```js // Use path payment to send XLM from the receiver to the sender, who receives USDC @@ -105,3 +330,84 @@ transaction.sign(senderKP); const resp = await horizonServer.submitTransaction(transaction); console.log("resp", resp); ``` + +```java + ... + KeyPair senderKP = KeyPair.fromSecretSeed(SENDER_SECRET); + AccountResponse senderAccount = server.accounts().account(senderKP.getAccountId()); + + Transaction payTx = new TransactionBuilder(senderAccount, Network.TESTNET) + .setBaseFee(Transaction.MIN_BASE_FEE) + .setTimeout(30) + .addOperation( + new PathPaymentStrictReceiveOperation.Builder( + Asset.createNativeAsset(), // sendAsset (XLM) + "10", // sendMax (max XLM to spend) + receiverKP.getAccountId(), // destination + usdcAsset, // destAsset (USDC) + "1" // destAmount (exactly 1 USDC to receiver) + ) + // .setPath(List.of(...)) // optional explicit path if you want to force hops + .build() + ) + .addMemo(Memo.text("XLM to USDC")) + .build(); + + payTx.sign(senderKP); + SubmitTransactionResponse payResp = server.submitTransaction(payTx); + System.out.println("Payment response: " + payResp.isSuccess()); + } +} +``` + +```go +... + senderKP, err := keypair.ParseFull(SENDER_SECRET) + check(err) + + senderReq := horizonclient.AccountRequest{AccountID: senderKP.Address()} + senderAccount, err := client.AccountDetail(senderReq) + check(err) + + payOp := &txnbuild.PathPaymentStrictReceive{ + SendAsset: txnbuild.NativeAsset{}, // sending XLM + SendMax: "10", // max XLM to spend + Destination: receiverKP.Address(), // receiver account + DestAsset: usdcAsset, // receiver gets USDC + DestAmount: "1", // exactly 1 USDC + // Path: []txnbuild.Asset{...}, // optional explicit path + } + + payTx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ + SourceAccount: &senderAccount, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee * 100, + Timebounds: txnbuild.NewTimeout(30), + Operations: []txnbuild.Operation{payOp}, + Memo: txnbuild.MemoText("XLM to USDC"), + }) + check(err) + + payTx, err = payTx.Sign(network.TestNetworkPassphrase, senderKP) + check(err) + payTxB64, err := payTx.Base64() + check(err) + payResp, err := client.SubmitTransactionXDR(ctx, payTxB64) + check(err) + fmt.Println("Payment response: ", payResp.Hash) +} +``` + + + +[https://developers.stellar.org/docs/learn/fundamentals](../../../learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-receive) + +also we need to clairfy chanign API v openApia which si from #991 docs/data/apis/horizon/api-reference/structure/response-format.mdx#L170 \ + +\_contra- openapi/horizon/components/examples/responses/Offers/GetAllOffers.yml#L28 ++ openapi/horizon/components/examples/responses/Offers/GetAllOffers.yml#L28 + +Merge starts from new ex and leads into @Mootz12 remarks: + +> DEX prices sources are calculated via Horizon's "find strict receive payment path" endpoint. The specified `destAmount` will be received via the `sourceAsset` asset, and the price will be computed such that `sourceAmount / destAmount`, or the average price for the full path payment operation on that block. This can be useful to fetch prices for assets that are not on centralized exchanges. + +_Muust reconcile with base challenge at _ https://github.com/stellar/stellar-docs/issues/1529#issuecomment-3325034550 diff --git a/docs/build/guides/transactions/sponsored-reserves.mdx b/docs/build/guides/transactions/sponsored-reserves.mdx index bbacdfbafb..b934905ebc 100644 --- a/docs/build/guides/transactions/sponsored-reserves.mdx +++ b/docs/build/guides/transactions/sponsored-reserves.mdx @@ -1,5 +1,5 @@ --- -title: Sponsored reserves +title: Sponsored Reserves description: Use sponsored reserves to pay for base reserves on behalf of another account. sidebar_position: 50 --- @@ -482,23 +482,23 @@ For example, the following is an identical expression of the earlier Golang exam ```go - sponsorTrustline := []txnbuild.Operation{ - &txnbuild.BeginSponsoringFutureReserves{ - SponsoredID: addressA, - }, - &txnbuild.ChangeTrust{ - SourceAccount: aAccount.AccountID, - Line: &assets[0], - Limit: txnbuild.MaxTrustlineLimit, - }, - &txnbuild.EndSponsoringFutureReserves{ - SourceAccount: aAccount.AccountID, - }, - } +sponsorTrustline := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SponsoredID: addressA, + }, + &txnbuild.ChangeTrust{ + SourceAccount: aAccount.AccountID, + Line: &assets[0], + Limit: txnbuild.MaxTrustlineLimit, + }, + &txnbuild.EndSponsoringFutureReserves{ + SourceAccount: aAccount.AccountID, + }, +} - // Again, both participants must still sign the transaction: the sponsored - // account must consent to the sponsorship. - SignAndSend(client, s1Account.AccountID, []*keypair.Full{S1, A}, sponsorTrustline...) +// Again, both participants must still sign the transaction: the sponsored +// account must consent to the sponsorship. +SignAndSend(client, s1Account.AccountID, []*keypair.Full{S1, A}, sponsorTrustline...) ``` diff --git a/docs/build/smart-contracts/example-contracts/complex-account.mdx b/docs/build/smart-contracts/example-contracts/complex-account.mdx index ff33f67ece..8117e90d8c 100644 --- a/docs/build/smart-contracts/example-contracts/complex-account.mdx +++ b/docs/build/smart-contracts/example-contracts/complex-account.mdx @@ -25,7 +25,7 @@ Contract accounts are exclusive to Soroban and can't be used to perform other St :::danger -Implementing a contract account requires a very good understanding of authentication and authorization and requires rigorous testing and review. The example here is _not_ a full-fledged account contract - use it as an API reference only. +Implementing a custom account contract requires a very good understanding of authentication and authorization and requires rigorous testing and review. The example here is _not_ a full-fledged account contract - use it as an API reference only. ::: diff --git a/docs/build/smart-contracts/example-contracts/storage.mdx b/docs/build/smart-contracts/example-contracts/storage.mdx index 174992c853..5532e9a865 100644 --- a/docs/build/smart-contracts/example-contracts/storage.mdx +++ b/docs/build/smart-contracts/example-contracts/storage.mdx @@ -136,7 +136,7 @@ env.storage().instance().extend_ttl(50, 100); All contract data has a Time To Live (TTL), measured in ledgers, that must be periodically extended. If an entry's TTL is not periodically extended, the entry will eventually become "archived." You can learn more about this in the [State Archival](../../../learn/fundamentals/contract-development/storage/state-archival.mdx) document. -For now, it's worth knowing that there are three kinds of storage: `Persistent`, `Temporary`, and `Instance`. This contract only uses `Instance` storage: `env.storage().instance()`. Every time the counter is incremented, this storage's TTL gets extended by 100 [ledgers](../../../learn/fundamentals/stellar-data-structures/ledgers.mdx), or about 500 seconds. +For now, it's worth knowing that there are three kinds of storage: `Persistent`, `Temporary`, and `Instance`. This contract only uses `Instance` storage: `env.storage().instance()`. Every time the counter is incremented, this storage's TTL gets extended by 100 [ledgers](../../../learn/fundamentals/stellar-data-structures/ledgers/README.mdx), or about 500 seconds. ## Tests diff --git a/docs/build/smart-contracts/getting-started/storing-data.mdx b/docs/build/smart-contracts/getting-started/storing-data.mdx index 84ec5f9fc0..f75bd782da 100644 --- a/docs/build/smart-contracts/getting-started/storing-data.mdx +++ b/docs/build/smart-contracts/getting-started/storing-data.mdx @@ -122,7 +122,7 @@ env.storage().instance().extend_ttl(100, 100); All contract data has a Time To Live (TTL), measured in ledgers, that must be periodically extended. If an entry's TTL is not periodically extended, the entry will eventually become "archived." You can learn more about this in the [State Archival](../../../learn/fundamentals/contract-development/storage/state-archival.mdx) document. -For now, it's worth knowing that there are three kinds of storage: `Persistent`, `Temporary`, and `Instance`. This contract only uses `Instance` storage: `env.storage().instance()`. Every time the counter is incremented, this storage's TTL gets extended by 100 [ledgers](../../../learn/fundamentals/stellar-data-structures/ledgers.mdx), or about 500 seconds. +For now, it's worth knowing that there are three kinds of storage: `Persistent`, `Temporary`, and `Instance`. This contract only uses `Instance` storage: `env.storage().instance()`. Every time the counter is incremented, this storage's TTL gets extended by 100 [ledgers](../../../learn/fundamentals/stellar-data-structures/ledgers/README.mdx), or about 500 seconds. ### Build the contract diff --git a/docs/data/analytics/hubble/analyst-guide/optimizing-queries.mdx b/docs/data/analytics/hubble/analyst-guide/optimizing-queries.mdx index 5f73d739e9..59cb2d811a 100644 --- a/docs/data/analytics/hubble/analyst-guide/optimizing-queries.mdx +++ b/docs/data/analytics/hubble/analyst-guide/optimizing-queries.mdx @@ -3,7 +3,7 @@ title: "Optimizing Queries" sidebar_position: 30 --- -Hubble has terabytes of data to explore—that’s a lot of data! With access to so much data at your fingertips, it is crucial to performance-tune your queries. +Hubble has terabytes of data to explore—that’s a lot of data! With access to so much data at your fingertips, it is crucial to performance-tune your queries. One of the strengths of BigQuery is also its pitfall: you have access to tremendous compute capabilities, but you pay for what you use. If you fine-tune your queries, you will have access to powerful insights at the fraction of the cost of maintaining a data warehouse yourself. It is, however, easy to incur burdensome costs if you are not careful. diff --git a/docs/data/analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx b/docs/data/analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx index 980c981c30..cbad23d398 100644 --- a/docs/data/analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx +++ b/docs/data/analytics/hubble/analyst-guide/queries-for-horizon-like-data.mdx @@ -563,7 +563,7 @@ where true Offers are statements about how much of an asset an account wants to buy or sell. -Learn more about [offers](../../../../learn/glossary.mdx#decentralized-exchange). +Learn more about [offers](../../../../learn/glossary.mdx#offers). ### List All Offers diff --git a/docs/data/analytics/hubble/data-catalog/data-dictionary/bronze/liquidity-pools.mdx b/docs/data/analytics/hubble/data-catalog/data-dictionary/bronze/liquidity-pools.mdx index e0513e283f..f8f98cf416 100644 --- a/docs/data/analytics/hubble/data-catalog/data-dictionary/bronze/liquidity-pools.mdx +++ b/docs/data/analytics/hubble/data-catalog/data-dictionary/bronze/liquidity-pools.mdx @@ -19,7 +19,7 @@ description: "" | Name | Description | Data Type | Domain Values | Required? | Notes | | --- | --- | --- | --- | --- | --- | -| liquidity_pool_id | Unique identifier for a liquidity pool. There cannot be duplicate pools for the same asset pair. Once a pool has been created for the asset pair, another cannot be created. | string | | Yes | There is a good primer on AMMs [here](../../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#liquidity-pools) | +| liquidity_pool_id | Unique identifier for a liquidity pool. There cannot be duplicate pools for the same asset pair. Once a pool has been created for the asset pair, another cannot be created. | string | | Yes | There is a good primer on AMMs [here](../../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#amms) | | type | The mechanism that calculates pricing and division of shares for the pool. With the initial AMM rollout, the only type of liquidity pool allowed to be created is a constant product pool | string | constant_product | Yes | For more information regarding pricing and deposit calculations, read [Cap-38.](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0038.md) | | fee | The number of basis points charged as a percentage of the trade in order to complete the transaction. The fees earned on all trades are divided amongst pool shareholders and distributed as an incentive to keep money in the pools | integer | 30 | Yes | Fees are distributed immediately to accounts as the transaction completes. There is no schedule for fee distribution | | trustline_count | Total number of accounts with trustlines authorized to the pool. To create a trustline, an account must trust both base assets before trusting a pool with the asset pair | integer | | Yes | If the issuer of A or B revokes authorization on the trustline, the account will automatically withdraw from every liquidity pool containing that asset and those pool trustlines will be deleted. | diff --git a/docs/data/apis/horizon/api-reference/aggregations/order-books/README.mdx b/docs/data/apis/horizon/api-reference/aggregations/order-books/README.mdx index 96653d4e99..344a7b6381 100644 --- a/docs/data/apis/horizon/api-reference/aggregations/order-books/README.mdx +++ b/docs/data/apis/horizon/api-reference/aggregations/order-books/README.mdx @@ -5,9 +5,9 @@ sidebar_position: 10 An order book is a collection of offers for a specific pair of assets. -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. -Learn more about [order books](../../../../../../learn/glossary.mdx#decentralized-exchange). +_See_ [Liquidity on Stellar Encyclopedia Entry: the Unified Orderbook](../../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#orderbook). diff --git a/docs/data/apis/horizon/api-reference/aggregations/order-books/object.mdx b/docs/data/apis/horizon/api-reference/aggregations/order-books/object.mdx index 188c9a93bd..945129b6aa 100644 --- a/docs/data/apis/horizon/api-reference/aggregations/order-books/object.mdx +++ b/docs/data/apis/horizon/api-reference/aggregations/order-books/object.mdx @@ -73,7 +73,7 @@ When Horizon returns information about an order book, it uses the following form -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. diff --git a/docs/data/apis/horizon/api-reference/aggregations/paths/README.mdx b/docs/data/apis/horizon/api-reference/aggregations/paths/README.mdx index b7fe0ce321..e8e0cfc273 100644 --- a/docs/data/apis/horizon/api-reference/aggregations/paths/README.mdx +++ b/docs/data/apis/horizon/api-reference/aggregations/paths/README.mdx @@ -5,9 +5,9 @@ sidebar_position: 20 Paths provide information about potential path payments. A path can be used to populate the necessary fields for a path payment operation. -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The HTTP header in the response includes the [latest ledger](../../structure/consistency.mdx) known to Horizon. -Learn more about the two types of path payment: [`path payment strict send`](../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-send) and [`path payment strict receive`](../../../../../../learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-receive) +Learn more about path usage: [Path Payments Encyclopedia Entry](../../../../../../build/guides/transactions/path-payments.mdx) diff --git a/docs/data/apis/horizon/api-reference/aggregations/paths/object.mdx b/docs/data/apis/horizon/api-reference/aggregations/paths/object.mdx index 8e1f467a8f..c5805c8223 100644 --- a/docs/data/apis/horizon/api-reference/aggregations/paths/object.mdx +++ b/docs/data/apis/horizon/api-reference/aggregations/paths/object.mdx @@ -49,7 +49,7 @@ When Horizon returns information about a path, it uses the following format: -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. diff --git a/docs/data/apis/horizon/api-reference/errors/README.mdx b/docs/data/apis/horizon/api-reference/errors/README.mdx index 73c42fec92..cfbd08168d 100644 --- a/docs/data/apis/horizon/api-reference/errors/README.mdx +++ b/docs/data/apis/horizon/api-reference/errors/README.mdx @@ -5,15 +5,15 @@ sidebar_position: 40 import { MethodTable } from "@site/src/components/MethodTable"; -After processing a request, Horizon returns a success or error response to the client. A success response will return a Status Code of 200, and an error response will return a Status Code in the range of 4XX - 5XX along with additional information about why the request could not complete successfully. +After processing a request, Horizon returns a success or error response to the client. A success response will return a Status Code of 200, and an error response will return a Status Code in the range of `4XX` – `5XX` along with additional information about why the request could not complete successfully. -There are two categories of errors: [HTTP Status Codes](./http-status-codes/README.mdx) and [Result Codes](./result-codes/README.mdx). Result Codes only follow a Transaction Failed (400) HTTP Status Code. +There are two categories of errors: [HTTP Status Codes](./http-status-codes/README.mdx) and [Result Codes](./result-codes/README.mdx). Result Codes only follow an HTTP Status Code `400` (Transaction Failed). | | | | --- | --- | -| [HTTP Status Codes](./http-status-codes/README.mdx) | Errors that occur at the Horizon Server level. | +| [HTTP Status](./http-status-codes/README.mdx) | Errors that occur at the Horizon Server level. | | [Result Codes](./result-codes/README.mdx) | Errors that occur at the Stellar Core level. | diff --git a/docs/data/apis/horizon/api-reference/resources/accounts/README.mdx b/docs/data/apis/horizon/api-reference/resources/accounts/README.mdx index 58b9dec08f..120bc25425 100644 --- a/docs/data/apis/horizon/api-reference/resources/accounts/README.mdx +++ b/docs/data/apis/horizon/api-reference/resources/accounts/README.mdx @@ -5,7 +5,7 @@ order: 0 Users interact with the Stellar network through accounts. Everything else in the ledger—assets, offers, trustlines, etc.—are owned by accounts, and accounts must authorize all changes to the ledger through signed transactions. -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. Learn more about [accounts](../../../../../../learn/glossary.mdx#account). diff --git a/docs/data/apis/horizon/api-reference/resources/accounts/object.mdx b/docs/data/apis/horizon/api-reference/resources/accounts/object.mdx index 0436efbb98..72e7988146 100644 --- a/docs/data/apis/horizon/api-reference/resources/accounts/object.mdx +++ b/docs/data/apis/horizon/api-reference/resources/accounts/object.mdx @@ -130,7 +130,7 @@ When Horizon returns information about an account, it uses the following format: -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. diff --git a/docs/data/apis/horizon/api-reference/resources/assets/README.mdx b/docs/data/apis/horizon/api-reference/resources/assets/README.mdx index 86392fe783..3d42dbda45 100644 --- a/docs/data/apis/horizon/api-reference/resources/assets/README.mdx +++ b/docs/data/apis/horizon/api-reference/resources/assets/README.mdx @@ -5,7 +5,7 @@ order: 0 Assets are representations of value issued on the Stellar network. An asset consists of a type, code, and issuer. -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. Learn more about [assets](../../../../../../learn/glossary.mdx#asset). diff --git a/docs/data/apis/horizon/api-reference/resources/assets/object.mdx b/docs/data/apis/horizon/api-reference/resources/assets/object.mdx index 08bbb0fcbb..fb549e674e 100644 --- a/docs/data/apis/horizon/api-reference/resources/assets/object.mdx +++ b/docs/data/apis/horizon/api-reference/resources/assets/object.mdx @@ -61,7 +61,7 @@ When Horizon returns information about an asset, it uses the following format: -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. diff --git a/docs/data/apis/horizon/api-reference/resources/claimablebalances/README.mdx b/docs/data/apis/horizon/api-reference/resources/claimablebalances/README.mdx index 786b83a6b6..6d117682fb 100644 --- a/docs/data/apis/horizon/api-reference/resources/claimablebalances/README.mdx +++ b/docs/data/apis/horizon/api-reference/resources/claimablebalances/README.mdx @@ -5,7 +5,7 @@ order: 0 A Claimable Balance represents the transfer of ownership of some amount of an asset. Claimable balances provide a mechanism for setting up a payment which can be claimed in the future. This allows you to make payments to accounts which are currently not able to accept them. -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. diff --git a/docs/data/apis/horizon/api-reference/resources/claimablebalances/object.mdx b/docs/data/apis/horizon/api-reference/resources/claimablebalances/object.mdx index d1cb8685cb..5bcd55d7ee 100644 --- a/docs/data/apis/horizon/api-reference/resources/claimablebalances/object.mdx +++ b/docs/data/apis/horizon/api-reference/resources/claimablebalances/object.mdx @@ -64,7 +64,7 @@ When Horizon returns information about a claimable balance, it uses the followin -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. diff --git a/docs/data/apis/horizon/api-reference/resources/liquiditypools/README.mdx b/docs/data/apis/horizon/api-reference/resources/liquiditypools/README.mdx index adb801abac..e9a759a2e8 100644 --- a/docs/data/apis/horizon/api-reference/resources/liquiditypools/README.mdx +++ b/docs/data/apis/horizon/api-reference/resources/liquiditypools/README.mdx @@ -3,9 +3,11 @@ title: Liquidity Pools order: 0 --- -Liquidity Pools provide a simple, non-interactive way to trade large amounts of capital and enable high volumes of trading. +Liquidity Pools provide a simple, non-interactive way to trade large amounts of capital and automatically trade high volumes. -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. + +Learn more about pool participation: [Liquidity on Stellar](../../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#amms) section. diff --git a/docs/data/apis/horizon/api-reference/resources/offers/README.mdx b/docs/data/apis/horizon/api-reference/resources/offers/README.mdx index d88cea336d..9b9628a6c2 100644 --- a/docs/data/apis/horizon/api-reference/resources/offers/README.mdx +++ b/docs/data/apis/horizon/api-reference/resources/offers/README.mdx @@ -5,9 +5,7 @@ order: 0 Offers are statements about how much of an asset an account wants to buy or sell. -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. - -Learn more about [offers](../../../../../../learn/glossary.mdx#decentralized-exchange). +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. diff --git a/docs/data/apis/horizon/api-reference/resources/offers/object.mdx b/docs/data/apis/horizon/api-reference/resources/offers/object.mdx index 78dd39d83e..5f7e434312 100644 --- a/docs/data/apis/horizon/api-reference/resources/offers/object.mdx +++ b/docs/data/apis/horizon/api-reference/resources/offers/object.mdx @@ -52,7 +52,7 @@ When Horizon returns information about an offer, it uses the following format: -The [latest ledger](../../structure/consistency.mdx) known to Horizon is included as an HTTP header in the response. +The response includes the [latest ledger](../../structure/consistency.mdx) known in an HTTP header. diff --git a/docs/data/apis/horizon/api-reference/resources/operations/object/buy-offer.mdx b/docs/data/apis/horizon/api-reference/resources/operations/object/buy-offer.mdx index f38a78cd29..18ebc91238 100644 --- a/docs/data/apis/horizon/api-reference/resources/operations/object/buy-offer.mdx +++ b/docs/data/apis/horizon/api-reference/resources/operations/object/buy-offer.mdx @@ -5,6 +5,8 @@ order: 60 Creates, updates, or deletes a buy offer to trade assets. A buy offer specifies a certain amount of the buying asset that should be sold in exchange for the minimum quantity of the selling asset. +If an entire buy offer isn't fulfilled by existing liquidity, the remaining order volume gets converted to a sell offer representation internally. + See the [`Manage Buy Offer` errors](../../../errors/result-codes/operation-specific/manage-buy-offer.mdx). diff --git a/docs/data/apis/horizon/api-reference/resources/trades/README.mdx b/docs/data/apis/horizon/api-reference/resources/trades/README.mdx index e98a5b59a2..1427bb7199 100644 --- a/docs/data/apis/horizon/api-reference/resources/trades/README.mdx +++ b/docs/data/apis/horizon/api-reference/resources/trades/README.mdx @@ -5,7 +5,7 @@ order: 0 When an offer is fully or partially fulfilled, a trade happens. Trades can also be caused by successful path payments, because path payments involve fulfilling offers. -A trade occurs between two parties—`base` and `counter`. Which is which is either arbitrary or determined by the calling query. +A trade occurs between two parties: `base` and `counter`. Name assignment is either arbitrary or determined by the calling query. Learn more about [trades](../../../../../../learn/glossary.mdx#decentralized-exchange). diff --git a/docs/data/apis/horizon/api-reference/retrieve-an-order-book.api.mdx b/docs/data/apis/horizon/api-reference/retrieve-an-order-book.api.mdx index d5417fe305..b554e90433 100644 --- a/docs/data/apis/horizon/api-reference/retrieve-an-order-book.api.mdx +++ b/docs/data/apis/horizon/api-reference/retrieve-an-order-book.api.mdx @@ -1,7 +1,7 @@ --- id: retrieve-an-order-book title: "Retrieve an Order Book" -description: "The order book endpoint provides an order book's bids and asks and can be used in [streaming](https://developers.stellar.org/docs/data/apis/horizon/api-reference/structure/streaming) mode." +description: "The order book endpoint provides an order book's bids and asks and can be used in [streaming](./structure/streaming.mdx) mode." sidebar_label: "Retrieve an Order Book" hide_title: true hide_table_of_contents: true @@ -36,7 +36,7 @@ import Heading from "@theme/Heading"; -The order book endpoint provides an order book's bids and asks and can be used in [streaming](https://developers.stellar.org/docs/data/apis/horizon/api-reference/structure/streaming) mode. +The order book endpoint provides an order book's bids and asks and can be used in [streaming](./structure/streaming.mdx) mode. When filtering for a specific order book, you must use use all six of these arguments: `base_asset_type`, `base_asset_issuer`, `base_asset_code`, `counter_asset_type`, `counter_asset_issuer`, and `counter_asset_code`. If the base or counter asset is XLM, you only need to indicate the asset type as `native` and do not need to designate the code or the issuer. diff --git a/docs/data/apis/horizon/api-reference/structure/README.mdx b/docs/data/apis/horizon/api-reference/structure/README.mdx index e300c71d90..42cc6dc912 100644 --- a/docs/data/apis/horizon/api-reference/structure/README.mdx +++ b/docs/data/apis/horizon/api-reference/structure/README.mdx @@ -6,8 +6,20 @@ description: Horizon is an API for interacting with the Stellar network. sidebar_key: horizon-api-structure --- -import DocCardList from "@theme/DocCardList"; +import { MethodTable } from "@site/src/components/MethodTable"; How Horizon is structured. - + + +| | | +| ------------------------------------------------- | --- | +| [Response Format](./response-format.mdx) | | +| [Page Arguments](./pagination/page-arguments.mdx) | | +| [Streaming](./streaming.mdx) | | +| [Rate Limiting](./rate-limiting.mdx) | | +| [XDR](./xdr.mdx) | | +| [Consistency](./consistency.mdx) | | +| [Pagination](./pagination/README.mdx) | | + + diff --git a/docs/data/apis/horizon/api-reference/structure/pagination/README.mdx b/docs/data/apis/horizon/api-reference/structure/pagination/README.mdx index 1758ee6fdd..ad98fec4c6 100644 --- a/docs/data/apis/horizon/api-reference/structure/pagination/README.mdx +++ b/docs/data/apis/horizon/api-reference/structure/pagination/README.mdx @@ -9,6 +9,8 @@ Each individual transaction, operation, ledger, etc. is returned as a record, an To move between pages of a collection of records, use the links in the `next` and `prev` attributes nested under the top-level `_links` attribute. +--- + - ATTRIBUTE @@ -35,7 +37,15 @@ To move between pages of a collection of records, use the links in the `next` an - +--- + +## Larger Queries + +For fewer round trips, you can specify the `limit` parameter up to `200` to receive more items in each response. You can also lower it for calls with a smaller scope. Learn more in the [call arguments](./page-arguments.mdx). + +## Example Request + + ```js var StellarSdk = require("stellar-sdk"); diff --git a/docs/data/apis/horizon/api-reference/structure/pagination/page-arguments.mdx b/docs/data/apis/horizon/api-reference/structure/pagination/page-arguments.mdx index ea50800fe2..07a576a1af 100644 --- a/docs/data/apis/horizon/api-reference/structure/pagination/page-arguments.mdx +++ b/docs/data/apis/horizon/api-reference/structure/pagination/page-arguments.mdx @@ -26,7 +26,7 @@ import { MethodTable } from "@site/src/components/MethodTable"; - A designation of the order in which records should appear. Options include `asc`(ascending) or `desc` (descending). If this argument isn’t set, it defaults to `asc`. - limit - optional - - The maximum number of records returned. The limit can range from 1 to 200 - an upper limit that is hardcoded in Horizon for performance reasons. If this argument isn’t designated, it defaults to 10. + - The maximum number of records returned. The limit can range from 1 to 200—an upper limit that is hardcoded in Horizon for performance reasons. If this argument isn’t designated, it defaults to 10. diff --git a/docs/data/apis/rpc/api-reference/methods/README.mdx b/docs/data/apis/rpc/api-reference/methods/README.mdx index 3d8a016cad..2bcff88c7a 100644 --- a/docs/data/apis/rpc/api-reference/methods/README.mdx +++ b/docs/data/apis/rpc/api-reference/methods/README.mdx @@ -11,8 +11,8 @@ All you need to know about available RPC methods, parameters and responses, and Don't know which endpoint you need to get what you want? A lot of the returned fields are deeply-nested [XDR structures](../../../../../learn/fundamentals/data-format/xdr.mdx#more-about-xdr), so it can be hard to figure out what kind of information is available in each of these. Here's a bit of a dive into what the "workhorse" endpoints provide, in decreasing order of granularity: -- [`getLedgers`](./getLedgers.mdx) operates at the block level, providing you with the full, complete details of what occurred during application of that ledger (known as "ledger metadata", defined in the protocol by the [`LedgerCloseMeta`](https://github.com/stellar/stellar-xdr/blob/v22.0/Stellar-ledger.x#L539) union, specifically the `V1` iteration). Each of the subsequent endpoints is just a microscope into a subset of the data available provided by this endpoint. Metadata includes things like: - - Details for recreating the blockchain's state (see [Ledger Headers](../../../../../learn/fundamentals/stellar-data-structures/ledgers.mdx#ledger-headers) for more). +- [`getLedgers`](./getLedgers.mdx) operates at the block level, providing you with the full, complete details of what occurred during application of that ledger (known as "ledger metadata", defined in the protocol by the [`LedgerCloseMeta`](https://github.com/stellar/stellar-xdr/blob/v23.0/Stellar-ledger.x#L604) union, specifically the `V1` iteration). Each of the subsequent endpoints is just a microscope into a subset of the data available provided by this endpoint. Metadata includes things like: + - Details for recreating the blockchain's state (see [Ledger Headers](../../../../../learn/fundamentals/stellar-data-structures/ledgers/headers.mdx) for more). - The consensus information that led to the block closing (see [Stellar Consensus Protocol](../../../../../learn/fundamentals/stellar-consensus-protocol.mdx)). - The set of transactions, their respective operations, and the results of applying those transactions in this block (see [Transactions](../../../../../learn/fundamentals/transactions/operations-and-transactions.mdx)). - [`getTransaction(s)`](./getTransactions.mdx) operates across a span of ledgers or on a single transaction hash depending on the variant. The structured data here includes details such as: diff --git a/docs/data/apis/rpc/api-reference/methods/getLedgerEntries.mdx b/docs/data/apis/rpc/api-reference/methods/getLedgerEntries.mdx index c522e99f0d..277d1a7ee2 100644 --- a/docs/data/apis/rpc/api-reference/methods/getLedgerEntries.mdx +++ b/docs/data/apis/rpc/api-reference/methods/getLedgerEntries.mdx @@ -10,28 +10,15 @@ import rpcSpec from "@site/static/stellar-rpc.openrpc.json"; method={rpcSpec.methods.filter((meth) => meth.name === "getLedgerEntries")[0]} /> -# Building ledger keys +## Constructing Calls -The Stellar ledger is, on some level, essentially a key-value store. The keys are instances of [`LedgerKey`](https://github.com/stellar/stellar-xdr/blob/v22.0/Stellar-ledger-entries.x#L600) and the values are instances of [`LedgerEntry`](https://github.com/stellar/stellar-xdr/blob/v22.0/Stellar-ledger-entries.x#L560). An interesting product of the store's internal design is that the key is a _subset_ of the entry: we'll see more of this later. +The `getLedgerEntries` method returns the "values" (or "entries") for a given set of "keys." Ledger keys come in a lot of forms, and we'll go over the commonly used ones on this page alongside tutorials on how to build and use them. -The `getLedgerEntries` method returns the "values" (or "entries") for a given set of "keys". Ledger keys come in a lot of forms, and we'll go over the commonly used ones on this page alongside tutorials on how to build and use them. +The source of truth should always be the XDR defined in the protocol. `LedgerKey`s are a union type defined in [Stellar-ledger-entries.x](https://github.com/stellar/stellar-xdr/blob/v23.0/Stellar-ledger-entries.x#L548). -## Types of `LedgerKey`s +An interesting product of the store's internal design is that the key is a _subset_ of the entry: we'll see more of this later. -The source of truth should always be the XDR defined in the protocol. `LedgerKey`s are a union type defined in [Stellar-ledger-entries.x](https://github.com/stellar/stellar-xdr/blob/v22.0/Stellar-ledger-entries.x#L600). There are 10 different forms a ledger key can take: - -1. **Account:** holistically defines a Stellar account, including its balance, signers, etc. (see [Accounts](../../../../../learn/fundamentals/stellar-data-structures/accounts.mdx)) -2. **Trustline:** defines a balance line to a non-native asset issued on the network (see [`changeTrustOp`](../../../../../learn/fundamentals/transactions/list-of-operations.mdx#change-trust)) -3. **Offer:** defines an offer made on the Stellar DEX (see [Liquidity on Stellar](../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx)) -4. **Account Data:** defines key-value data entries attached to an account (see [`manageDataOp`](../../../../../learn/fundamentals/transactions/list-of-operations.mdx#manage-data)) -5. **Claimable Balance:** defines a balance that may or may not actively be claimable (see [Claimable Balances](../../../../../build/guides/transactions/claimable-balances.mdx)) -6. **Liquidity Pool:** defines the configuration of a native constant liquidity pool between two assets (see [Liquidity on Stellar](../../../../../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx)) -7. **Contract Data:** defines a piece of data being stored in a contract under a key -8. **Contract Code:** defines the Wasm bytecode of a contract -9. **Config Setting:** defines the currently active network configuration -10. **TTL:** defines the time-to-live of an associated contract data or code entry - -We're going to focus on a subset of these for maximum value, but once you understand how to build and parse some keys and entries, you can extrapolate to all of them. +For more on the types of `LedgerKey` entries and their purposes, see [Ledger Entries](../../../../../learn/fundamentals/stellar-data-structures/ledgers/entries.mdx). ### Accounts @@ -55,19 +42,81 @@ console.log(accountLedgerKey.toXDR("base64")); from stellar_sdk import Keypair, xdr public_key = "GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO" -account_ledger_key = xdr.LedgerKey( - type=xdr.LedgerEntryType.ACCOUNT, - account=xdr.LedgerKeyAccount( - account_id=Keypair.from_public_key(public_key).xdr_account_id() +accountLedgerKey = xdr.LedgerKey( + type = xdr.LedgerEntryType.ACCOUNT, + account = xdr.LedgerKeyAccount( + account_id = Keypair.from_public_key(public_key).xdr_account_id() ), ) -print(account_ledger_key.to_xdr()) +print(accountLedgerKey.to_xdr()) +``` + +```java +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import org.stellar.sdk.KeyPair; +import org.stellar.sdk.xdr.LedgerEntryType; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.LedgerKeyAccount; +import org.stellar.sdk.xdr.XdrDataOutputStream; + +public class AccountLedgerKeyExample { + public static void main(String[] args) throws IOException { + String publicKey = "GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO"; + + LedgerKeyAccount ledgerKeyAccount = new LedgerKeyAccount(); + ledgerKeyAccount.setAccountID(KeyPair.fromAccountId(publicKey).getXdrAccountId()); + + LedgerKey accountLedgerKey = new LedgerKey(); + accountLedgerKey.setDiscriminant(LedgerEntryType.ACCOUNT); + accountLedgerKey.setAccount(ledgerKeyAccount); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + XdrDataOutputStream xdrOutput = new XdrDataOutputStream(output); + LedgerKey.encode(xdrOutput, accountLedgerKey); + String base64 = Base64.getEncoder().encodeToString(output.toByteArray()); + System.out.println(base64); + } +} +``` + +```go +package main + +import ( + "bytes" + "encoding/base64" + "fmt" + + "github.com/stellar/go-stellar-sdk/xdr" +) + +func main() { + publicKey := "GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO" + + accountLedgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{ + AccountId: xdr.MustAddress(publicKey), + }, + } + + var buffer bytes.Buffer + if _, err := xdr.Marshal(&buffer, accountLedgerKey); err != nil { + panic(err) + } + + fmt.Println(base64.StdEncoding.EncodeToString(buffer.Bytes())) +} ``` This will give you the full account details. +{/* ⚠ RPC server var `s` not clearly defined */} + ```typescript @@ -78,10 +127,65 @@ const accountEntryData = ( ```python account_entry_data = xdr.LedgerEntryData.from_xdr( - server.get_ledger_entries([account_ledger_key]).entries[0].xdr + server.get_ledger_entries([accountLedgerKey]).entries[0].xdr ).account ``` +```java +import java.util.Collections; +import org.stellar.sdk.SorobanServer; +import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse; +import org.stellar.sdk.xdr.AccountEntry; + +SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); +GetLedgerEntriesResponse response = + server.getLedgerEntries(Collections.singleton(accountLedgerKey)); +AccountEntry accountEntryData = response.getEntries().get(0).parseXdr().getAccount(); +``` + +```go +package main + +import ( + "context" + + rpcclient "github.com/stellar/go-stellar-sdk/clients/rpcclient" + rpctypes "github.com/stellar/go-stellar-sdk/protocols/rpc" + "github.com/stellar/go-stellar-sdk/xdr" +) + +func fetchAccountEntry() xdr.AccountEntry { + publicKey := "GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO" + accountLedgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{ + AccountId: xdr.MustAddress(publicKey), + }, + } + + keyB64, err := xdr.MarshalBase64(accountLedgerKey) + if err != nil { + panic(err) + } + + client := rpcclient.NewClient("https://soroban-testnet.stellar.org", nil) + defer client.Close() + + resp, err := client.GetLedgerEntries(context.Background(), rpctypes.GetLedgerEntriesRequest{ + Keys: []string{keyB64}, + }) + if err != nil { + panic(err) + } + + var entry xdr.LedgerEntryData + if err := xdr.SafeUnmarshalBase64(resp.Entries[0].DataXDR, &entry); err != nil { + panic(err) + } + return *entry.Account +} +``` + If you just want to take a look at the structure, you can pass the raw base64 value we logged above to the [Laboratory](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$xdrFormat=json;;) (or via `curl` if you pass `"xdrFormat": "json"` as an additional parameter to `getLedgerEntries`) and see all of the possible fields. You can also dig into them in code, of course: @@ -100,7 +204,24 @@ console.log( ```python print( - f"Account {public_key} has {account_entry_data.balance.int64} stroops of XLM and is on sequence number {account_entry_data.seq_num.sequence_number.int64}" + f"Account {public_key} has {account_entry_data.balance.int64} stroops of XLM and is on sequence number {account_entry_data.seq_num.sequence_number.int64}" +) +``` + +```java +System.out.printf( + "Account %s has %d stroops of XLM and is on sequence number %d%n", + publicKey, + accountEntryData.getBalance().getInt64(), + accountEntryData.getSeqNum().getSequenceNumber().getInt64()); +``` + +```go +fmt.Printf( + "Account %s has %d stroops of XLM and is on sequence number %d\n", + publicKey, + accountEntryData.Balance, + int64(accountEntryData.SeqNum), ) ``` @@ -108,7 +229,7 @@ print( ### Trustlines -A trustline is a balance entry for any non-native asset (such as [Circle's USDC](https://stellar.expert/explorer/public/asset/USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN)). To fetch one, you need the trustline owner (a public key like for [Accounts](#accounts)) and the asset in question: +A trustline is a balance entry for any non-native asset like AstroDollars. To fetch one, you need the trustline owner (a public key like for [Accounts](#accounts)) and the asset in question: @@ -117,29 +238,111 @@ const trustlineLedgerKey = xdr.LedgerKey.ledgerKeyTrustLine( new xdr.LedgerKeyTrustLine({ accountId: Keypair.fromPublicKey(publicKey).xdrAccountId(), asset: new Asset( - "USDC", - "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "AstroDollar", + "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7", ).toTrustLineXDRObject(), }), ); ``` ```python -trustline_ledger_key = xdr.LedgerKey( - type=xdr.LedgerEntryType.TRUSTLINE, - trust_line=xdr.LedgerKeyTrustLine( - account_id=Keypair.from_public_key(public_key).xdr_account_id(), - asset=Asset( - "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" +trustlineLedgerKey = xdr.LedgerKey( + type = xdr.LedgerEntryType.TRUSTLINE, + trust_line = xdr.LedgerKeyTrustLine( + account_id = Keypair.from_public_key(public_key).xdr_account_id(), + asset = Asset( + "AstroDollar", "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7" ).to_trust_line_asset_xdr_object(), ), ) trustline_entry_data = xdr.LedgerEntryData.from_xdr( - server.get_ledger_entries([trustline_ledger_key]).entries[0].xdr + server.get_ledger_entries([trustlineLedgerKey]).entries[0].xdr ).trust_line ``` +```java +import java.util.Collections; +import org.stellar.sdk.Asset; +import org.stellar.sdk.KeyPair; +import org.stellar.sdk.SorobanServer; +import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse; +import org.stellar.sdk.xdr.LedgerEntryType; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.LedgerKeyTrustLine; +import org.stellar.sdk.xdr.TrustLineEntry; + +SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); + +Asset astroDollar = Asset.createNonNativeAsset( + "AstroDollar", "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7"); +LedgerKeyTrustLine trustLineKey = new LedgerKeyTrustLine(); +trustLineKey.setAccountID(KeyPair.fromAccountId(publicKey).getXdrAccountId()); +trustLineKey.setAsset(astroDollar.toXdr()); + +LedgerKey trustlineLedgerKey = new LedgerKey(); +trustlineLedgerKey.setDiscriminant(LedgerEntryType.TRUSTLINE); +trustlineLedgerKey.setTrustLine(trustLineKey); + +GetLedgerEntriesResponse trustlineResponse = + server.getLedgerEntries(Collections.singleton(trustlineLedgerKey)); +TrustLineEntry trustlineEntryData = + trustlineResponse.getEntries().get(0).parseXdr().getTrustLine(); +``` + +```go +package main + +import ( + "context" + + rpcclient "github.com/stellar/go/clients/rpcclient" + rpctypes "github.com/stellar/go/protocols/rpc" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" +) + +func fetchTrustlineEntry(publicKey string) xdr.TrustLineEntry { + asset := txnbuild.CreditAsset{ + Code: "AstroDollar", + Issuer: "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7", + } + trustlineAsset, err := asset.MustToTrustLineAsset().ToXDR() + if err != nil { + panic(err) + } + + ledgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress(publicKey), + Asset: trustlineAsset, + }, + } + + keyB64, err := xdr.MarshalBase64(ledgerKey) + if err != nil { + panic(err) + } + + client := rpcclient.NewClient("https://soroban-testnet.stellar.org", nil) + defer client.Close() + + resp, err := client.GetLedgerEntries(context.Background(), rpctypes.GetLedgerEntriesRequest{ + Keys: []string{keyB64}, + }) + if err != nil { + panic(err) + } + + var entry xdr.LedgerEntryData + if err := xdr.SafeUnmarshalBase64(resp.Entries[0].DataXDR, &entry); err != nil { + panic(err) + } + return *entry.TrustLine +} +``` + Much like an [account](#accounts), the resulting entry has a balance, but it also has a limit and flags to control how much of that asset can be held. The asset, however, can be either an issued asset or a liquidity pool: @@ -198,6 +401,822 @@ print( ) ``` +```java +import java.util.Locale; +import javax.xml.bind.DatatypeConverter; +import org.stellar.sdk.Asset; +import org.stellar.sdk.xdr.TrustLineAsset; + +TrustLineAsset rawAsset = trustlineEntryData.getAsset(); +String assetDescription; + +switch (rawAsset.getDiscriminant()) { + case ASSET_TYPE_CREDIT_ALPHANUM4: + case ASSET_TYPE_CREDIT_ALPHANUM12: + org.stellar.sdk.xdr.Asset assetXdr = new org.stellar.sdk.xdr.Asset(); + assetXdr.setDiscriminant(rawAsset.getDiscriminant()); + if (rawAsset.getDiscriminant() + == org.stellar.sdk.xdr.AssetType.ASSET_TYPE_CREDIT_ALPHANUM4) { + assetXdr.setAlphaNum4(rawAsset.getAlphaNum4()); + } else { + assetXdr.setAlphaNum12(rawAsset.getAlphaNum12()); + } + assetDescription = Asset.fromXdr(assetXdr).toString(); + break; + case ASSET_TYPE_POOL_SHARE: + byte[] poolIdBytes = rawAsset.getLiquidityPoolID().getPoolID().getHash(); + assetDescription = + DatatypeConverter.printHexBinary(poolIdBytes).toLowerCase(Locale.ROOT); + break; + default: + throw new IllegalStateException("Unsupported trustline asset type"); +} + +System.out.printf( + "Account %s has %d stroops of %s with a limit of %d%n", + publicKey, + trustlineEntryData.getBalance().getInt64(), + assetDescription, + trustlineEntryData.getLimit().getInt64()); +``` + +```go +package main + +import ( + "encoding/hex" + "fmt" + "strings" + + "github.com/stellar/go/strkey" + "github.com/stellar/go/xdr" +) + +func describeTrustline(publicKey string, trustlineEntry xdr.TrustLineEntry) { + rawAsset := trustlineEntry.Asset + var asset string + + switch rawAsset.Type { + case xdr.AssetTypeAssetTypeCreditAlphanum4: + a4 := rawAsset.MustAlphaNum4() + code := strings.TrimRight(string(a4.AssetCode[:]), "\x00") + issuer := a4.Issuer.MustEd25519() + issuerAddr, err := strkey.Encode(strkey.VersionByteAccountID, issuer[:]) + if err != nil { + panic(err) + } + asset = fmt.Sprintf("%s:%s", code, issuerAddr) + case xdr.AssetTypeAssetTypeCreditAlphanum12: + a12 := rawAsset.MustAlphaNum12() + code := strings.TrimRight(string(a12.AssetCode[:]), "\x00") + issuer := a12.Issuer.MustEd25519() + issuerAddr, err := strkey.Encode(strkey.VersionByteAccountID, issuer[:]) + if err != nil { + panic(err) + } + asset = fmt.Sprintf("%s:%s", code, issuerAddr) + case xdr.AssetTypeAssetTypePoolShare: + poolID := rawAsset.MustLiquidityPoolId() + hash := xdr.Hash(poolID) + asset = hex.EncodeToString(hash[:]) + default: + panic("unsupported trustline asset") + } + + fmt.Printf( + "Account %s has %d stroops of %s with a limit of %d\n", + publicKey, + trustlineEntry.Balance, + asset, + trustlineEntry.Limit, + ) +} +``` + + + +### Offers + +An offer represents a live order on the DEX. Each offer is identified by its `offerID`. To construct a `LedgerKey` for an offer, you only need the offer ID: + + + +```typescript +const offerLedgerKey = xdr.LedgerKey.ledgerKeyOffer( + new xdr.LedgerKeyOffer({ + offerID: 123456789n, + }), +); +console.log(offerLedgerKey.toXDR("base64")); +``` + +```java +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import org.stellar.sdk.KeyPair; +import org.stellar.sdk.xdr.Int64; +import org.stellar.sdk.xdr.LedgerEntryType; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.LedgerKeyOffer; +import org.stellar.sdk.xdr.XdrDataOutputStream; + +String sellerPublicKey = "GBSD6VAFS2Z3PUSF4BOACMF4NFKWY5I3Q2ZQ2CJQEXAMPLE00000000"; + +LedgerKeyOffer offerKey = new LedgerKeyOffer(); +offerKey.setSellerID(KeyPair.fromAccountId(sellerPublicKey).getXdrAccountId()); +offerKey.setOfferID(new Int64(123456789L)); + +LedgerKey offerLedgerKey = new LedgerKey(); +offerLedgerKey.setDiscriminant(LedgerEntryType.OFFER); +offerLedgerKey.setOffer(offerKey); + +ByteArrayOutputStream output = new ByteArrayOutputStream(); +XdrDataOutputStream xdrOutput = new XdrDataOutputStream(output); +LedgerKey.encode(xdrOutput, offerLedgerKey); +System.out.println(Base64.getEncoder().encodeToString(output.toByteArray())); +``` + +```go +package main + +import ( + "fmt" + + "github.com/stellar/go/xdr" +) + +func offerLedgerKeyBase64() string { + seller := "GBSD6VAFS2Z3PUSF4BOACMF4NFKWY5I3Q2ZQ2CJQEXAMPLE00000000" + + ledgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.LedgerKeyOffer{ + SellerId: xdr.MustAddress(seller), + OfferId: xdr.Int64(123456789), + }, + } + + encoded, err := xdr.MarshalBase64(ledgerKey) + if err != nil { + panic(err) + } + return encoded +} + +func main() { + fmt.Println(offerLedgerKeyBase64()) +} +``` + + + +Once you have the ledger entry, you can extract its fields: + + + +```typescript +const offerEntryData = ( + await s.getLedgerEntries(offerLedgerKey) +).entries[0].offer(); + +console.log(`Offer ID: ${offerEntryData.offerId().toString()}`); +console.log(`Seller: ${offerEntryData.sellerId().accountId()}`); +console.log(`Amount: ${offerEntryData.amount().toString()}`); // of selling +console.log( + `Price: ${offerEntryData.price().n().toString()} / ${offerEntryData.price().d().toString()}`, +); +``` + +```java +import java.util.Collections; +import org.stellar.sdk.KeyPair; +import org.stellar.sdk.SorobanServer; +import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse; +import org.stellar.sdk.xdr.OfferEntry; + +SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); +GetLedgerEntriesResponse response = + server.getLedgerEntries(Collections.singleton(offerLedgerKey)); +OfferEntry offerEntryData = response.getEntries().get(0).parseXdr().getOffer(); + +System.out.printf("Offer ID: %d%n", offerEntryData.getOfferID().getInt64()); +System.out.printf( + "Seller: %s%n", + KeyPair.fromXdrPublicKey(offerEntryData.getSellerID().getAccountID()).getAccountId()); +System.out.printf("Amount (selling): %d%n", offerEntryData.getAmount().getInt64()); +System.out.printf( + "Price: %d / %d%n", + offerEntryData.getPrice().getN().getInt32(), + offerEntryData.getPrice().getD().getInt32()); +``` + +```go +package main + +import ( + "context" + "fmt" + + rpcclient "github.com/stellar/go/clients/rpcclient" + rpctypes "github.com/stellar/go/protocols/rpc" + "github.com/stellar/go/xdr" +) + +func fetchOfferEntry(key xdr.LedgerKey) xdr.OfferEntry { + keyB64, err := xdr.MarshalBase64(key) + if err != nil { + panic(err) + } + + client := rpcclient.NewClient("https://soroban-testnet.stellar.org", nil) + defer client.Close() + + resp, err := client.GetLedgerEntries(context.Background(), rpctypes.GetLedgerEntriesRequest{ + Keys: []string{keyB64}, + }) + if err != nil { + panic(err) + } + + var data xdr.LedgerEntryData + if err := xdr.SafeUnmarshalBase64(resp.Entries[0].DataXDR, &data); err != nil { + panic(err) + } + return *data.Offer +} + +func describeOffer(entry xdr.OfferEntry) { + fmt.Printf("Offer ID: %d\n", entry.OfferId) + seller, _ := entry.SellerId.GetAddress() + fmt.Printf("Seller: %s\n", seller) + fmt.Printf("Amount (selling): %d\n", entry.Amount) + fmt.Printf("Price: %d / %d\n", entry.Price.N, entry.Price.D) +} +``` + + + +You can then decode the selling and buying assets the same way you do for Trustlines, by inspecting `offerEntryData.selling()` and `offerEntryData.buying()` using the standard `xdr.Asset` parsing logic. + +### Claimable Balances + +A claimable balance represents assets that have been locked under certain claim conditions. Each claimable balance is identified by its `balanceId`, which is a 32-byte hash. + +To construct a `LedgerKey` for a claimable balance, you need the `balanceId` as a `Buffer` or `Uint8Array`: + + + +```typescript +const balanceIdHex = + "407a334017a508fb2bf41952c74f977b46147ed70b175717f8bacc0ca3f2cc5b"; +const balanceIdBytes = Buffer.from(balanceIdHex, "hex"); + +const claimableBalanceLedgerKey = xdr.LedgerKey.ledgerKeyClaimableBalance( + new xdr.LedgerKeyClaimableBalance({ + balanceID: xdr.ClaimableBalanceID.claimableBalanceIdTypeV0(balanceIdBytes), + }), +); +console.log(claimableBalanceLedgerKey.toXDR("base64")); +``` + +```java +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import javax.xml.bind.DatatypeConverter; +import org.stellar.sdk.xdr.ClaimableBalanceID; +import org.stellar.sdk.xdr.ClaimableBalanceIDType; +import org.stellar.sdk.xdr.Hash; +import org.stellar.sdk.xdr.LedgerEntryType; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.LedgerKeyClaimableBalance; +import org.stellar.sdk.xdr.XdrDataOutputStream; + +String balanceIdHex = + "407a334017a508fb2bf41952c74f977b46147ed70b175717f8bacc0ca3f2cc5b"; +byte[] balanceIdBytes = DatatypeConverter.parseHexBinary(balanceIdHex); + +Hash balanceHash = new Hash(balanceIdBytes); +ClaimableBalanceID balanceId = + ClaimableBalanceID.builder() + .discriminant(ClaimableBalanceIDType.CLAIMABLE_BALANCE_ID_TYPE_V0) + .v0(balanceHash) + .build(); + +LedgerKeyClaimableBalance balanceKey = new LedgerKeyClaimableBalance(); +balanceKey.setBalanceID(balanceId); + +LedgerKey ledgerKey = new LedgerKey(); +ledgerKey.setDiscriminant(LedgerEntryType.CLAIMABLE_BALANCE); +ledgerKey.setClaimableBalance(balanceKey); + +ByteArrayOutputStream output = new ByteArrayOutputStream(); +XdrDataOutputStream xdrOutput = new XdrDataOutputStream(output); +LedgerKey.encode(xdrOutput, ledgerKey); +System.out.println(Base64.getEncoder().encodeToString(output.toByteArray())); +``` + +```go +package main + +import ( + "encoding/hex" + "fmt" + + "github.com/stellar/go/xdr" +) + +func buildClaimableBalanceKey() xdr.LedgerKey { + balanceIdHex := + "407a334017a508fb2bf41952c74f977b46147ed70b175717f8bacc0ca3f2cc5b" + raw, err := hex.DecodeString(balanceIdHex) + if err != nil { + panic(err) + } + + var balanceHash xdr.Hash + copy(balanceHash[:], raw) + + return xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &xdr.LedgerKeyClaimableBalance{ + BalanceId: xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &balanceHash, + }, + }, + } +} + +func main() { + key := buildClaimableBalanceKey() + b64, err := xdr.MarshalBase64(key) + if err != nil { + panic(err) + } + fmt.Println(b64) +} +``` + + + +Once you have the ledger entry, you can extract its fields: + + + +```typescript +const claimableBalanceEntryData = ( + await s.getLedgerEntries(claimableBalanceLedgerKey) +).entries[0].claimableBalance(); + +console.log(`Amount: ${claimableBalanceEntryData.amount().toString()} stroops`); + +const asset = claimableBalanceEntryData.asset(); +// Decode asset similar to as in trustlines + +const claimants = claimableBalanceEntryData.claimants(); + +if (claimableBalanceEntryData.ext().switch().value === 1) { + const flags = claimableBalanceEntryData.ext().v1().flags(); + const clawbackEnabled = + (flags & + xdr.ClaimableBalanceFlags.claimableBalanceClawbackEnabledFlag()) !== + 0; + console.log(`Clawback Enabled: ${clawbackEnabled}`); +} +``` + +```java +import java.util.Collections; +import org.stellar.sdk.Asset; +import org.stellar.sdk.SorobanServer; +import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse; +import org.stellar.sdk.xdr.ClaimableBalanceEntry; +import org.stellar.sdk.xdr.ClaimableBalanceFlags; +import org.stellar.sdk.xdr.Claimant; + +SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); +GetLedgerEntriesResponse response = + server.getLedgerEntries(Collections.singleton(claimableBalanceLedgerKey)); +ClaimableBalanceEntry claimableBalanceEntryData = + response.getEntries().get(0).parseXdr().getClaimableBalance(); + +System.out.printf( + "Amount: %d stroops%n", claimableBalanceEntryData.getAmount().getInt64()); + +Asset asset = claimableBalanceEntryData.getAsset(); // decode like trustlines +Claimant[] claimants = claimableBalanceEntryData.getClaimants(); + +if (claimableBalanceEntryData.getExt().getDiscriminant() == 1) { + long flags = claimableBalanceEntryData.getExt().getV1().getFlags().getInt32(); + boolean clawbackEnabled = + (flags + & ClaimableBalanceFlags.CLAIMABLE_BALANCE_CLAWBACK_ENABLED_FLAG.getValue()) + != 0; + System.out.printf("Clawback Enabled: %s%n", clawbackEnabled); +} +``` + +```go +package main + +import ( + "context" + "fmt" + + rpcclient "github.com/stellar/go/clients/rpcclient" + rpctypes "github.com/stellar/go/protocols/rpc" + "github.com/stellar/go/xdr" +) + +func fetchClaimableBalance(key xdr.LedgerKey) xdr.ClaimableBalanceEntry { + keyB64, err := xdr.MarshalBase64(key) + if err != nil { + panic(err) + } + + client := rpcclient.NewClient("https://soroban-testnet.stellar.org", nil) + defer client.Close() + + resp, err := client.GetLedgerEntries(context.Background(), rpctypes.GetLedgerEntriesRequest{ + Keys: []string{keyB64}, + }) + if err != nil { + panic(err) + } + + var data xdr.LedgerEntryData + if err := xdr.SafeUnmarshalBase64(resp.Entries[0].DataXDR, &data); err != nil { + panic(err) + } + return *data.ClaimableBalance +} + +func describeClaimableBalance(entry xdr.ClaimableBalanceEntry) { + fmt.Printf("Amount: %d stroops\n", entry.Amount) + fmt.Printf("Claimants: %d\n", len(entry.Claimants)) + + if entry.Ext.V1 != nil { + flags := uint32(entry.Ext.V1.Flags) + clawbackEnabled := (flags & uint32(xdr.ClaimableBalanceFlagsClaimableBalanceClawbackEnabledFlag)) != 0 + fmt.Printf("Clawback Enabled: %t\n", clawbackEnabled) + } +} +``` + + + +#### Claimants and Predicates + +Claimable balances contain one or more claimants, each with a claim predicate. You can iterate through them as follows: + + + +```typescript +for (let i = 0; i < claimants.length; i++) { + const claimant = claimants[i].v0(); + const destination = claimant.destination().accountId(); + console.log(`Claimant ${i + 1}: ${destination}`); + + const predicate = claimant.predicate(); + const predicateType = predicate.switch().value; + + switch (predicateType) { + case xdr.ClaimPredicateType.claimPredicateUnconditional().value: + console.log("Predicate: Unconditional"); + break; + case xdr.ClaimPredicateType.claimPredicateBeforeAbsoluteTime().value: + console.log(`Predicate: Before Absolute Time = ${predicate.absBefore().toString()}`); + break; + case xdr.ClaimPredicateType.claimPredicateBeforeRelativeTime().value: + console.log(`Predicate: Before Relative Time = ${predicate.relBefore().toString()} seconds`); + break; + default: + console.log("Predicate: Complex predicate type (AND/OR/NOT)"); + break; +} +``` + +```java +import org.stellar.sdk.KeyPair; +import org.stellar.sdk.xdr.Claimant; +import org.stellar.sdk.xdr.ClaimPredicateType; + +for (int i = 0; i < claimants.length; i++) { + Claimant.ClaimantV0 claimant = claimants[i].getV0(); + String destination = + KeyPair.fromXdrPublicKey(claimant.getDestination().getAccountID()).getAccountId(); + System.out.printf("Claimant %d: %s%n", i + 1, destination); + + switch (claimant.getPredicate().getDiscriminant()) { + case CLAIM_PREDICATE_UNCONDITIONAL: + System.out.println("Predicate: Unconditional"); + break; + case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME: + System.out.printf( + "Predicate: Before Absolute Time = %d%n", + claimant.getPredicate().getAbsBefore().getInt64()); + break; + case CLAIM_PREDICATE_BEFORE_RELATIVE_TIME: + System.out.printf( + "Predicate: Before Relative Time = %d seconds%n", + claimant.getPredicate().getRelBefore().getInt64()); + break; + default: + System.out.println("Predicate: Complex predicate type (AND/OR/NOT)"); + break; + } +} +``` + +```go +package main + +import ( + "fmt" + + "github.com/stellar/go/xdr" +) + +func printClaimants(claimants []xdr.Claimant) { + for i, claimant := range claimants { + dest, _ := claimant.V0.Destination.GetAddress() + fmt.Printf("Claimant %d: %s\n", i+1, dest) + + switch claimant.V0.Predicate.Type { + case xdr.ClaimPredicateTypeClaimPredicateUnconditional: + fmt.Println("Predicate: Unconditional") + case xdr.ClaimPredicateTypeClaimPredicateBeforeAbsoluteTime: + if claimant.V0.Predicate.AbsBefore != nil { + fmt.Printf( + "Predicate: Before Absolute Time = %d\n", + int64(*claimant.V0.Predicate.AbsBefore), + ) + } + case xdr.ClaimPredicateTypeClaimPredicateBeforeRelativeTime: + if claimant.V0.Predicate.RelBefore != nil { + fmt.Printf( + "Predicate: Before Relative Time = %d seconds\n", + int64(*claimant.V0.Predicate.RelBefore), + ) + } + default: + fmt.Println("Predicate: Complex predicate type (AND/OR/NOT)") + } + } +} +``` + + + +#### Flags + +If the claimable balance has flags set (not `v0`), you can read them like this: + + + +```typescript +switch (claimableBalanceEntryData.ext().switch().value) { + case 0: // No flags + break; + case 1: + const flags = claimableBalanceEntryData.ext().v1().flags(); + const clawbackEnabled = (flags & 1) !== 0; // claw = 0x1 + console.log(`Clawback Enabled: ${clawbackEnabled}`); + break; +} +``` + +```java +switch (claimableBalanceEntryData.getExt().getDiscriminant()) { + case 0: + break; + case 1: + long flags = claimableBalanceEntryData.getExt().getV1().getFlags().getInt32(); + boolean clawbackEnabled = + (flags + & ClaimableBalanceFlags.CLAIMABLE_BALANCE_CLAWBACK_ENABLED_FLAG.getValue()) + != 0; + System.out.printf("Clawback Enabled: %s%n", clawbackEnabled); + break; +} +``` + +```go +switch claimableBalanceEntry.Ext.V1 { +case nil: + // No flags +default: + flags := uint32(claimableBalanceEntry.Ext.V1.Flags) + clawbackEnabled := (flags & uint32(xdr.ClaimableBalanceFlagsClaimableBalanceClawbackEnabledFlag)) != 0 + fmt.Printf("Clawback Enabled: %t\n", clawbackEnabled) +} +``` + + + +### Liquidity Pools + +A liquidity pool represents an Automated Market Maker holding two assets. Each liquidity pool is identified by its `liquidityPoolID`, which is a 32-byte hash. + +To construct a `LedgerKey` for an AMM, you need the liquidity-pool ID as a `Buffer` or `Uint8Array`. Core deterministicly stores this as a 64-char hex string. + + + +```typescript +const liquidityPoolIdHex = + "82f857462d5304e1ad7d5308fb6d90ff3e70ad8fb07b81d04b12d2cc867fc735"; +const liquidityPoolIdBytes = Buffer.from(liquidityPoolIdHex, "hex"); + +const liquidityPoolLedgerKey = xdr.LedgerKey.ledgerKeyLiquidityPool( + new xdr.LedgerKeyLiquidityPool({ + liquidityPoolID: liquidityPoolIdBytes, + }), +); +console.log(liquidityPoolLedgerKey.toXDR("base64")); +``` + +```java +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import javax.xml.bind.DatatypeConverter; +import org.stellar.sdk.xdr.Hash; +import org.stellar.sdk.xdr.LedgerEntryType; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.LedgerKeyLiquidityPool; +import org.stellar.sdk.xdr.XdrDataOutputStream; + +String liquidityPoolIdHex = + "82f857462d5304e1ad7d5308fb6d90ff3e70ad8fb07b81d04b12d2cc867fc735"; +byte[] liquidityPoolIdBytes = DatatypeConverter.parseHexBinary(liquidityPoolIdHex); + +Hash liquidityPoolHash = new Hash(liquidityPoolIdBytes); +LedgerKeyLiquidityPool liquidityPoolKey = new LedgerKeyLiquidityPool(); +liquidityPoolKey.setLiquidityPoolID(liquidityPoolHash); + +LedgerKey liquidityPoolLedgerKey = new LedgerKey(); +liquidityPoolLedgerKey.setDiscriminant(LedgerEntryType.LIQUIDITY_POOL); +liquidityPoolLedgerKey.setLiquidityPool(liquidityPoolKey); + +ByteArrayOutputStream output = new ByteArrayOutputStream(); +XdrDataOutputStream xdrOutput = new XdrDataOutputStream(output); +LedgerKey.encode(xdrOutput, liquidityPoolLedgerKey); +System.out.println(Base64.getEncoder().encodeToString(output.toByteArray())); +``` + +```go +package main + +import ( + "encoding/hex" + "fmt" + + "github.com/stellar/go/xdr" +) + +func buildLiquidityPoolKey() xdr.LedgerKey { + liquidityPoolIdHex := + "82f857462d5304e1ad7d5308fb6d90ff3e70ad8fb07b81d04b12d2cc867fc735" + raw, err := hex.DecodeString(liquidityPoolIdHex) + if err != nil { + panic(err) + } + + var poolID xdr.Hash + copy(poolID[:], raw) + + return xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LedgerKeyLiquidityPool{ + LiquidityPoolId: poolID, + }, + } +} + +func main() { + key := buildLiquidityPoolKey() + b64, err := xdr.MarshalBase64(key) + if err != nil { + panic(err) + } + fmt.Println(b64) +} +``` + + + +Once you have the ledger entry, you can extract its fields: + + + +```typescript +const liquidityPoolEntryData = ( + await s.getLedgerEntries(liquidityPoolLedgerKey) +).entries[0].liquidityPool(); + +console.log( + `Total Pool Shares: ${liquidityPoolEntryData.totalPoolShares().toString()}`, +); +console.log( + `Total Trustlines: ${liquidityPoolEntryData.poolSharesTrustLineCount().toString()}`, +); + +const AMM = liquidityPoolEntryData.body(); +switch (AMM.switch().value) { + case xdr.LiquidityPoolType.liquidityPoolConstantProduct().value: { + const params = AMM.constantProduct().params(); + const fee = params.fee(); // basis points + const assetA = params.assetA(); + const assetB = params.assetB(); + console.log(`Constant-Product Between: ${assetA} and ${assetB}`); + break; + } +} +``` + +```java +import java.util.Collections; +import org.stellar.sdk.Asset; +import org.stellar.sdk.SorobanServer; +import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse; +import org.stellar.sdk.xdr.LiquidityPoolEntry; +import org.stellar.sdk.xdr.LiquidityPoolType; + +GetLedgerEntriesResponse response = + server.getLedgerEntries(Collections.singleton(liquidityPoolLedgerKey)); +LiquidityPoolEntry liquidityPoolEntryData = + response.getEntries().get(0).parseXdr().getLiquidityPool(); + +System.out.printf( + "Total Pool Shares: %d%n", + liquidityPoolEntryData.getTotalPoolShares().getInt64()); +System.out.printf( + "Total Trustlines: %d%n", + liquidityPoolEntryData.getPoolSharesTrustLineCount().getInt64()); + +switch (liquidityPoolEntryData.getBody().getDiscriminant()) { + case LIQUIDITY_POOL_CONSTANT_PRODUCT: + var params = liquidityPoolEntryData.getBody().getConstantProduct().getParams(); + System.out.printf( + "Constant-Product Between: %s and %s%n", + Asset.fromXdr(params.getAssetA()).toString(), + Asset.fromXdr(params.getAssetB()).toString()); + System.out.printf("Fee (basis points): %d%n", params.getFee().getInt32()); + break; + default: + break; +} +``` + +```go +package main + +import ( + "context" + "fmt" + + rpcclient "github.com/stellar/go/clients/rpcclient" + rpctypes "github.com/stellar/go/protocols/rpc" + "github.com/stellar/go/xdr" +) + +func fetchLiquidityPoolEntry(key xdr.LedgerKey) xdr.LiquidityPoolEntry { + keyB64, err := xdr.MarshalBase64(key) + if err != nil { + panic(err) + } + client := rpcclient.NewClient("https://soroban-testnet.stellar.org", nil) + defer client.Close() + + resp, err := client.GetLedgerEntries(context.Background(), rpctypes.GetLedgerEntriesRequest{ + Keys: []string{keyB64}, + }) + if err != nil { + panic(err) + } + + var data xdr.LedgerEntryData + if err := xdr.SafeUnmarshalBase64(resp.Entries[0].DataXDR, &data); err != nil { + panic(err) + } + return *data.LiquidityPool +} + +func describeLiquidityPool(entry xdr.LiquidityPoolEntry) { + fmt.Printf("Total Pool Shares: %d\n", entry.TotalPoolShares) + fmt.Printf("Total Trustlines: %d\n", entry.PoolSharesTrustLineCount) + + switch entry.Body.Type { + case xdr.LiquidityPoolTypeLiquidityPoolConstantProduct: + params := entry.Body.ConstantProduct.Params + fmt.Printf("Constant-Product Between: %v and %v\n", params.AssetA, params.AssetB) + fmt.Printf("Fee (basis points): %d\n", params.Fee) + } +} +``` + ### Contract Data @@ -234,25 +1253,85 @@ const ledgerKey = getLedgerKeySymbol( ```python from stellar_sdk import xdr, scval, Address -def get_ledger_key_symbol(contract_id: str, symbol_text: str) -> str: +def getLedgerKeySymbol(contract_id: str, symbol_text: str) -> str: ledger_key = xdr.LedgerKey( - type=xdr.LedgerEntryType.CONTRACT_DATA, - contract_data=xdr.LedgerKeyContractData( - contract=Address(contract_id).to_xdr_sc_address(), - key=scval.to_symbol(symbol_text), - durability=xdr.ContractDataDurability.PERSISTENT + type = xdr.LedgerEntryType.CONTRACT_DATA, + contract_data = xdr.LedgerKeyContractData( + contract = Address(contract_id).to_xdr_sc_address(), + key = scval.to_symbol(symbol_text), + durability = xdr.ContractDataDurability.PERSISTENT ), ) return ledger_key.to_xdr() print( - get_ledger_key_symbol( + getLedgerKeySymbol( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI", "COUNTER" ) ) ``` +```java +import org.stellar.sdk.Address; +import org.stellar.sdk.scval.Scv; +import org.stellar.sdk.xdr.ContractDataDurability; +import org.stellar.sdk.xdr.LedgerEntryType; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.LedgerKeyContractData; + +LedgerKey getLedgerKeySymbol(String contractId, String symbolText) { + LedgerKeyContractData contractData = new LedgerKeyContractData(); + contractData.setContract(new Address(contractId).toSCAddress()); + contractData.setKey(Scv.toSymbol(symbolText)); + contractData.setDurability(ContractDataDurability.PERSISTENT); + + LedgerKey ledgerKey = new LedgerKey(); + ledgerKey.setDiscriminant(LedgerEntryType.CONTRACT_DATA); + ledgerKey.setContractData(contractData); + return ledgerKey; +} +``` + +```go +package main + +import ( + "github.com/stellar/go/strkey" + "github.com/stellar/go/xdr" +) + +func ledgerKeySymbol(contractID, symbol string) xdr.LedgerKey { + raw, err := strkey.Decode(strkey.VersionByteContract, contractID) + if err != nil { + panic(err) + } + + var hash xdr.Hash + copy(hash[:], raw) + contract := xdr.ContractId(hash) + + scAddress := xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contract, + } + symbolVal := xdr.ScSymbol(symbol) + key := xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &symbolVal, + } + + return xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: scAddress, + Key: key, + Durability: xdr.ContractDataDurabilityPersistent, + }, + } +} +``` + ### Contract Wasm Code @@ -288,24 +1367,83 @@ console.log( ```python from stellar_sdk import xdr, Address -def get_ledger_key_contract_code(contract_id: str) -> xdr.LedgerKey: +def getLedgerKeyContractCode(contract_id: str) -> xdr.LedgerKey: return xdr.LedgerKey( - type=xdr.LedgerEntryType.CONTRACT_DATA, - contract_data=xdr.LedgerKeyContractData( - contract=Address(contract_id).to_xdr_sc_address(), - key=xdr.SCVal(xdr.SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE), - durability=xdr.ContractDataDurability.PERSISTENT + type = xdr.LedgerEntryType.CONTRACT_DATA, + contract_data = xdr.LedgerKeyContractData( + contract = Address(contract_id).to_xdr_sc_address(), + key = xdr.SCVal(xdr.SCValType.SCV_LEDGER_KEY_CONTRACT_INSTANCE), + durability = xdr.ContractDataDurability.PERSISTENT ) ) -print(get_ledger_key_contract_code( +print(getLedgerKeyContractCode( "CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI" )) ``` +```java +import org.stellar.sdk.Address; +import org.stellar.sdk.scval.Scv; +import org.stellar.sdk.xdr.ContractDataDurability; +import org.stellar.sdk.xdr.LedgerEntryType; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.LedgerKeyContractData; + +LedgerKey getLedgerKeyContractCode(String contractId) { + LedgerKeyContractData key = new LedgerKeyContractData(); + key.setContract(new Address(contractId).toSCAddress()); + key.setKey(Scv.toLedgerKeyContractInstance()); + key.setDurability(ContractDataDurability.PERSISTENT); + + LedgerKey ledgerKey = new LedgerKey(); + ledgerKey.setDiscriminant(LedgerEntryType.CONTRACT_DATA); + ledgerKey.setContractData(key); + return ledgerKey; +} + +System.out.println( + getLedgerKeyContractCode("CCPYZFKEAXHHS5VVW5J45TOU7S2EODJ7TZNJIA5LKDVL3PESCES6FNCI")); +``` + +```go +package main + +import ( + "github.com/stellar/go/strkey" + "github.com/stellar/go/xdr" +) + +func contractInstanceLedgerKey(contractID string) xdr.LedgerKey { + raw, err := strkey.Decode(strkey.VersionByteContract, contractID) + if err != nil { + panic(err) + } + + var hash xdr.Hash + copy(hash[:], raw) + contract := xdr.ContractId(hash) + + address := xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contract, + } + key := xdr.ScVal{Type: xdr.ScValTypeScvLedgerKeyContractInstance} + + return xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: address, + Key: key, + Durability: xdr.ContractDataDurabilityPersistent, + }, + } +} +``` + -Once we have the ledger entry (via `getLedgerEntries`, see [below](#actually-fetching-the-ledger-entry-data)), we can extract the Wasm hash: +Once we have the ledger entry (via `getLedgerEntries`, see [below](#fetching-the-ledger-entry-data)), we can extract the Wasm hash: #### 2. Request the `ContractCode` using the retrieved `LedgerKey` @@ -332,7 +1470,7 @@ function getLedgerKeyWasmId( ```python from stellar_sdk import xdr -def get_ledger_key_wasm_id( +def getLedgerKeyWasmId( # received from getLedgerEntries and decoded contract_data: xdr.ContractDataEntry ) -> xdr.LedgerKey: @@ -341,14 +1479,50 @@ def get_ledger_key_wasm_id( # Now, we can create the `LedgerKey` as we've done in previous examples ledger_key = xdr.LedgerKey( - type=xdr.LedgerEntryType.CONTRACT_CODE, - contract_code=xdr.LedgerKeyContractCode( - hash=wasm_hash + type = xdr.LedgerEntryType.CONTRACT_CODE, + contract_code = xdr.LedgerKeyContractCode( + hash = wasm_hash ), ) return ledger_key ``` +```java +import org.stellar.sdk.xdr.ContractDataEntry; +import org.stellar.sdk.xdr.LedgerEntryType; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.LedgerKeyContractCode; + +LedgerKey getLedgerKeyWasmId(ContractDataEntry contractData) { + LedgerKeyContractCode codeKey = new LedgerKeyContractCode(); + codeKey.setHash(contractData.getVal().getInstance().getExecutable().getWasmHash()); + + LedgerKey ledgerKey = new LedgerKey(); + ledgerKey.setDiscriminant(LedgerEntryType.CONTRACT_CODE); + ledgerKey.setContractCode(codeKey); + return ledgerKey; +} +``` + +```go +package main + +import ( + "github.com/stellar/go/xdr" +) + +func ledgerKeyWasmId(contractData xdr.ContractDataEntry) xdr.LedgerKey { + wasmHash := contractData.Val.MustInstance().Executable.MustWasmHash() + + return xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractCode, + ContractCode: &xdr.LedgerKeyContractCode{ + Hash: wasmHash, + }, + } +} +``` + Now, finally we have a `LedgerKey` that correspond to the Wasm byte-code that has been deployed under the `contractId` we started out with so very long ago. This `LedgerKey` can be used in a final request to `getLedgerEntries`. In that response we will get a `LedgerEntryData` corresponding to a `ContractCodeEntry` which will contain the actual, deployed, real-life contract byte-code: @@ -367,17 +1541,92 @@ const theCode: Buffer = await getLedgerEntries(getLedgerKeyWasmId(theHashData)) ```python the_hash_data = xdr.LedgerEntryData.from_xdr( - server.get_ledger_entries([get_ledger_key_contract_code("C...")]).entries[0].xdr + server.get_ledger_entries([getLedgerKeyContractCode("C...")]).entries[0].xdr ).contract_data the_code = xdr.LedgerEntryData.from_xdr( - server.get_ledger_entries([get_ledger_key_wasm_id(the_hash_data)]).entries[0].xdr + server.get_ledger_entries([getLedgerKeyWasmId(the_hash_data)]).entries[0].xdr ).contract_code.code ``` +```java +import java.util.Collections; +import org.stellar.sdk.SorobanServer; +import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse; +import org.stellar.sdk.xdr.ContractCodeEntry; +import org.stellar.sdk.xdr.ContractDataEntry; +import org.stellar.sdk.xdr.LedgerEntryData; + +SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); +GetLedgerEntriesResponse hashResponse = + server.getLedgerEntries(Collections.singleton(getLedgerKeyContractCode("C..."))); +ContractDataEntry theHashData = + hashResponse.getEntries().get(0).parseXdr().getContractData(); + +GetLedgerEntriesResponse codeResponse = + server.getLedgerEntries(Collections.singleton(getLedgerKeyWasmId(theHashData))); +ContractCodeEntry theCode = + codeResponse.getEntries().get(0).parseXdr().getContractCode(); +byte[] wasmBytes = theCode.getCode(); +``` + +```go +package main + +import ( + "context" + + rpcclient "github.com/stellar/go/clients/rpcclient" + rpctypes "github.com/stellar/go/protocols/rpc" + "github.com/stellar/go/xdr" +) + +func fetchContractCode(contractID string) ([]byte, error) { + client := rpcclient.NewClient("https://soroban-testnet.stellar.org", nil) + defer client.Close() + + hashKey := contractInstanceLedgerKey(contractID) + hashKeyB64, err := xdr.MarshalBase64(hashKey) + if err != nil { + return nil, err + } + + hashResp, err := client.GetLedgerEntries(context.Background(), rpctypes.GetLedgerEntriesRequest{ + Keys: []string{hashKeyB64}, + }) + if err != nil { + return nil, err + } + + var hashData xdr.LedgerEntryData + if err := xdr.SafeUnmarshalBase64(hashResp.Entries[0].DataXDR, &hashData); err != nil { + return nil, err + } + + codeKey := ledgerKeyWasmId(*hashData.ContractData) + codeKeyB64, err := xdr.MarshalBase64(codeKey) + if err != nil { + return nil, err + } + + codeResp, err := client.GetLedgerEntries(context.Background(), rpctypes.GetLedgerEntriesRequest{ + Keys: []string{codeKeyB64}, + }) + if err != nil { + return nil, err + } + + var codeData xdr.LedgerEntryData + if err := xdr.SafeUnmarshalBase64(codeResp.Entries[0].DataXDR, &codeData); err != nil { + return nil, err + } + return codeData.ContractCode.Code, nil +} +``` + -## Actually fetching the ledger entry data +## Fetching the ledger entry data Once we've learned to _build_ and _parse_ these (which we've done above at length), the process for actually fetching them is always identical. If you know the type of key you fetched, you apply the accessor method accordingly once you've received them from the `getLedgerEntries` method: @@ -404,6 +1653,66 @@ trustline = xdr.LedgerEntryData.from_xdr(response.entries[1].xdr).trust_line contract_data = xdr.LedgerEntryData.from_xdr(response.entries[2].xdr).contract_data ``` +```java +import java.util.Arrays; +import org.stellar.sdk.SorobanServer; +import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse; +import org.stellar.sdk.xdr.AccountEntry; +import org.stellar.sdk.xdr.ContractDataEntry; +import org.stellar.sdk.xdr.TrustLineEntry; + +SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org"); +GetLedgerEntriesResponse response = server.getLedgerEntries(Arrays.asList(key1, key2, key3)); + +AccountEntry account = + response.getEntries().get(0).parseXdr().getAccount(); +TrustLineEntry trustline = + response.getEntries().get(1).parseXdr().getTrustLine(); +ContractDataEntry contractData = + response.getEntries().get(2).parseXdr().getContractData(); +``` + +```go +package main + +import ( + "context" + + rpcclient "github.com/stellar/go/clients/rpcclient" + rpctypes "github.com/stellar/go/protocols/rpc" + "github.com/stellar/go/xdr" +) + +func fetchLedgerEntries(keys []xdr.LedgerKey) ([]xdr.LedgerEntryData, error) { + client := rpcclient.NewClient("https://soroban-testnet.stellar.org", nil) + defer client.Close() + + base64Keys := make([]string, len(keys)) + for i, key := range keys { + b64, err := xdr.MarshalBase64(key) + if err != nil { + return nil, err + } + base64Keys[i] = b64 + } + + resp, err := client.GetLedgerEntries(context.Background(), rpctypes.GetLedgerEntriesRequest{ + Keys: base64Keys, + }) + if err != nil { + return nil, err + } + + results := make([]xdr.LedgerEntryData, len(resp.Entries)) + for i, entry := range resp.Entries { + if err := xdr.SafeUnmarshalBase64(entry.DataXDR, &results[i]); err != nil { + return nil, err + } + } + return results, nil +} +``` + Now, finally we have a `LedgerKey` that correspond to the Wasm byte-code that has been deployed under the `ContractId` we started out with so very long ago. This `LedgerKey` can be used in a final request to the Stellar-RPC endpoint. @@ -423,7 +1732,7 @@ Now, finally we have a `LedgerKey` that correspond to the Wasm byte-code that ha } ``` -Then you can inspect them accordingly. Each of the above entries follows the XDR for that `LedgerEntryData` structure precisely. For example, the `AccountEntry` is in [`Stellar-ledger-entries.x#L191`](https://github.com/stellar/stellar-xdr/blob/v22.0/Stellar-ledger-entries.x#L191) and you can use `.seqNum()` to access its current sequence number, as we've shown. In JavaScript, you can see the appropriate methods in the [type definition](https://github.com/stellar/js-stellar-base/blob/6930a70d7fbde675514b5933baff605d97453ba7/types/curr.d.ts#L3034). +Then you can inspect them accordingly. Each of the above entries follows the XDR for that `LedgerEntryData` structure precisely. For example, the `AccountEntry` is in [`Stellar-ledger-entries.x#L191`](https://github.com/stellar/stellar-xdr/blob/v23.0/Stellar-ledger-entries.x#L190) and you can use `.seqNum()` to access its current sequence number, as we've shown. In JavaScript, you can see the appropriate methods in the [type definition](https://github.com/stellar/js-stellar-base/blob/6930a70d7fbde675514b5933baff605d97453ba7/types/curr.d.ts#L3034). ## Viewing and understanding XDR @@ -455,7 +1764,7 @@ Using the [Stellar XDR to JSON library](https://github.com/stellar/js-stellar-xd `AAAABgAAAAHMA/50/Q+w3Ni8UXWm/trxFBfAfl6De5kFttaMT0/ACwAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAASAAAAAAAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAAE=` -Try it in [the Lab](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$ledgerKeyEntries=%5B%22AAAABgAAAAHMA//50//Q+w3Ni8UXWm//trxFBfAfl6De5kFttaMT0//ACwAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAASAAAAAAAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAAE=%22%5D;;) +[Simulate it](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$ledgerKeyEntries=%5B%22AAAABgAAAAHMA//50//Q+w3Ni8UXWm//trxFBfAfl6De5kFttaMT0//ACwAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAASAAAAAAAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAAE=%22%5D;;) in the Lab. ![Lab: getledgerentries](/assets/api/rpc/getledgerentries-01.gif) @@ -463,6 +1772,6 @@ Let's submit `getLedgerEntries` for the following XDR string: `AAAABgAAAAGUvl2TPOjIsxuZgSyt3Lf0d6R2iNYu4rKDuULTaMKUSgAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAABdOuyYDwLteYrby3aOykd5c12LYrui/nhbXOgtejCSYAAAAAE=` -Try it in [the Lab](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$ledgerKeyEntries=%5B%22AAAABgAAAAGUvl2TPOjIsxuZgSyt3Lf0d6R2iNYu4rKDuULTaMKUSgAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAABdOuyYDwLteYrby3aOykd5c12LYrui//nhbXOgtejCSYAAAAAE=%22%5D&xdrFormat=json;;) +[Simulate it](https://lab.stellar.org/endpoints/rpc/get-ledger-entries?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&endpoints$params$ledgerKeyEntries=%5B%22AAAABgAAAAGUvl2TPOjIsxuZgSyt3Lf0d6R2iNYu4rKDuULTaMKUSgAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAABdOuyYDwLteYrby3aOykd5c12LYrui//nhbXOgtejCSYAAAAAE=%22%5D&xdrFormat=json;;) in the Lab. ![Lab: getledgerentries-02](/assets/api/rpc/getledgerentries-02.gif) diff --git a/docs/data/apis/rpc/api-reference/structure/pagination.mdx b/docs/data/apis/rpc/api-reference/structure/pagination.mdx index 7cb88df96e..27631615b0 100644 --- a/docs/data/apis/rpc/api-reference/structure/pagination.mdx +++ b/docs/data/apis/rpc/api-reference/structure/pagination.mdx @@ -3,7 +3,11 @@ sidebar_position: 50 title: Pagination --- -For methods which support it, the pagination arguments are passed as a final object argument with two values: +Pagination in stellar-rpc is similar to [pagination in Horizon](../../../horizon/api-reference/structure/pagination/README.mdx). + +======= + +> > > > > > > origin/main For methods which support it, the pagination arguments are passed as a final object argument with two values: - `cursor`: string - (optional) An opaque string which acts as a paging token. Each response will include a `cursor` field which can be included in a subsequent request to obtain the next page of results. - `limit`: number - (optional) The maximum number of records returned. The limit for [getEvents](../methods/getEvents.mdx) can range from 1 to 10000 - an upper limit that is hardcoded in Stellar-RPC for performance reasons. If this argument isn't designated, it defaults to 100. diff --git a/docs/data/indexers/build-your-own/ingest-sdk/README.mdx b/docs/data/indexers/build-your-own/ingest-sdk/README.mdx index 23a8f08a72..e4630081ea 100644 --- a/docs/data/indexers/build-your-own/ingest-sdk/README.mdx +++ b/docs/data/indexers/build-your-own/ingest-sdk/README.mdx @@ -9,13 +9,13 @@ The SDK is composed of several published Golang packages under `github.com/stell ## Why use the Ingest SDK? -Applications can leverage the SDK to rapidly develop ingestion pipelines capable of acquiring real-time or historical Stellar network data and deriving custom data models. The SDK enables applications to traverse the hierarchal data structures of the network: [history archives](../../../../validators/admin-guide/environment-preparation.mdx#history-archives), [ledgers](../../../../learn/fundamentals/stellar-data-structures/ledgers.mdx), transactions, operations, ledger state changes, and events. +Applications can leverage the SDK to rapidly develop ingestion pipelines capable of acquiring real-time or historical Stellar network data and deriving custom data models. The SDK enables applications to traverse the hierarchal data structures of the network: [history archives](../../../../validators/admin-guide/environment-preparation.mdx#history-archives), [ledgers](../../../../learn/fundamentals/stellar-data-structures/ledgers/README.mdx), transactions, operations, ledger state changes, and events. Use the SDK for an intuitive, compile-time, type-safe developer experience to work with the main types of network data: ### Ledger Entries -Obtain the final state of [ledger entries](../../../../learn/fundamentals/stellar-data-structures/ledgers.mdx) on the network at close of any recent or historically aged checkpoint ledger sequence. A Checkpoint ledger occurs once every 64 ledgers, during which the network will publish this data to [history archives](../../../../validators/admin-guide/environment-preparation.mdx#history-archives) in the format of compressed files which contain lists of `BucketEntry`, wherein each contains one `LedgerEntry` and the `LedgerKey`. +Obtain the final state of [ledger entries](../../../../learn/fundamentals/stellar-data-structures/ledgers/entries.mdx) on the network at close of any recent or historically aged checkpoint ledger sequence. A Checkpoint ledger occurs once every 64 ledgers, during which the network will publish this data to [history archives](../../../../validators/admin-guide/environment-preparation.mdx#history-archives) in the format of compressed files which contain lists of `BucketEntry`, wherein each contains one `LedgerEntry` and the `LedgerKey`. Ledger entries are cryptographically signed as part of each ledger and therefore represent the trusted, cumulative state at a point in time for [assets](../../../../learn/fundamentals/stellar-data-structures/assets.mdx) related to an [account](../../../../learn/fundamentals/stellar-data-structures/accounts.mdx) or [contract](../../../../learn/fundamentals/contract-development/storage/persisting-data.mdx). Examples of asset types: diff --git a/docs/learn/fundamentals/anchors.mdx b/docs/learn/fundamentals/anchors.mdx index 3d3488dade..b88568a749 100644 --- a/docs/learn/fundamentals/anchors.mdx +++ b/docs/learn/fundamentals/anchors.mdx @@ -7,7 +7,9 @@ description: "Learn about the on and off ramps on Stellar called anchors and the # Anchors -## Overview +import YouTube from "@site/src/components/YouTube"; + + An anchor is a Stellar-specific term for the on and off-ramps that connect the Stellar network to traditional financial rails, such as financial institutions or fintech companies. Anchors accept deposits of fiat currencies (such as the US dollar, Argentine peso, or Nigerian naira) via existing rails (such as bank deposits or cash-in points), then sends the user the equivalent digital tokens on the Stellar network. The equivalent digital tokens can either represent that same fiat currency or another digital token altogether. Alternatively, anchors allow token holders to redeem their tokens for the real-world assets they represent. diff --git a/docs/learn/fundamentals/contract-development/_category_.json b/docs/learn/fundamentals/contract-development/_category_.json new file mode 100644 index 0000000000..421dc264c5 --- /dev/null +++ b/docs/learn/fundamentals/contract-development/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Smart Contracts", + "position": 84, + "link": { + "type": "doc", + "id": "learn/fundamentals/contract-development/README" + }, + "description": "Dive into Soroban smart-contract concepts, lifecycle guidance, and authorization patterns so you can ship production-ready code on Stellar." +} diff --git a/docs/learn/fundamentals/contract-development/authorization.mdx b/docs/learn/fundamentals/contract-development/authorization.mdx index abe178d21b..a6629bfbe6 100644 --- a/docs/learn/fundamentals/contract-development/authorization.mdx +++ b/docs/learn/fundamentals/contract-development/authorization.mdx @@ -71,9 +71,9 @@ Note though, that contracts that deal with multiple authorized `Address`es need Account abstraction is a way to decouple the authentication logic from the contract-specific authorization rules. The `Address` defined above is in fact an identifier of an 'abstract' account. That is, the contracts know the `Address` and can require authorization from it, but they don't know how exactly it is implemented. -For example, imagine a token contract. Its responsibilities are to manage the balances of multiple users (transfer, mint, burn etc.). There is really nothing about these responsibilities that has anything to do with _how exactly_ the user authorized the balance-modifying transaction. The users may want to use some hardware key that supports a new generation of crypto algorithms(which don't even have to exist today) or they may want to have bespoke multisig scheme and none of this really has anything to do with the token logic. +For example, imagine a token contract. Its responsibilities are to manage the balances of multiple users (transfer, mint, burn etc.). There is really nothing about these responsibilities that has anything to do with _how exactly_ the user authorized the balance-modifying transaction. The users may want to use some hardware key that supports a new generation of crypto algorithms (which don't even have to exist today) or they may want to have bespoke multisig scheme, and none of this really has anything to do with the token logic. -Account abstraction provides a convenient extension point for every contract that uses `Address` for authorization. It doesn't solve all the issues automatically - client-side tooling may still need to be adapted to support different authentication schemes or different wallets. But the on-chain state doesn't need to be modified and modifying the on-chain state is a much harder problem. +Account abstraction provides a convenient extension point for every contract that uses `Address` for authorization. It doesn't solve all the issues automatically; client-side tooling may still need to be adapted to support different authentication schemes or different wallets. But the on-chain state doesn't need to be modified and modifying the on-chain state is a much harder problem. #### Types of Account Implementations @@ -119,7 +119,7 @@ For the exact interface and more details, see the [Simple Account example]. [Simple Account example]: ../../../build/smart-contracts/example-contracts/simple-account.mdx -### Secp256r1, passkeys and contract accounts +### Secp256r1, passkeys, and contract accounts After a successful public validator vote to upgrade Stellar's Mainnet to Protocol 21, the secp256r1 signature scheme was enabled for smart contract transactions. This allows developers to implement passkeys to sign transactions instead of using secret keys or seed phrases. For guidance, see the [passkey wallet guide](../../../build/guides/contract-accounts/smart-wallets.mdx). diff --git a/docs/learn/fundamentals/data-format/_category_.json b/docs/learn/fundamentals/data-format/_category_.json new file mode 100644 index 0000000000..bba43ef4d4 --- /dev/null +++ b/docs/learn/fundamentals/data-format/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Data Format", + "position": 85, + "link": { + "type": "doc", + "id": "learn/fundamentals/data-format/README" + }, + "description": "Understand Stellar's XDR and JSON encodings so you can inspect transactions, debug payloads, and interchange data with other systems." +} diff --git a/docs/learn/fundamentals/data-format/xdr-json.mdx b/docs/learn/fundamentals/data-format/xdr-json.mdx index efb13a1778..73a18e6908 100644 --- a/docs/learn/fundamentals/data-format/xdr-json.mdx +++ b/docs/learn/fundamentals/data-format/xdr-json.mdx @@ -1,6 +1,7 @@ --- title: XDR-JSON sidebar_position: 2 +description: "Use the XDR-JSON schema to round-trip Stellar XDR payloads through JSON tools without losing fidelity." --- The XDR-JSON schema is defined by the [stellar-xdr crate](https://docs.rs/stellar-xdr) and provides a round-trippable means for converting Stellar [XDR] values to JSON and converting that JSON back to the identical XDR. diff --git a/docs/learn/fundamentals/data-format/xdr.mdx b/docs/learn/fundamentals/data-format/xdr.mdx index 01938c6ac2..08386323de 100644 --- a/docs/learn/fundamentals/data-format/xdr.mdx +++ b/docs/learn/fundamentals/data-format/xdr.mdx @@ -1,6 +1,7 @@ --- title: XDR sidebar_position: 1 +description: "Understand Stellar's XDR binary format, why it exists, and how to inspect or parse it safely." --- Stellar stores and communicates ledger data, transactions, results, history, and messages in a binary format called External Data Representation (XDR). XDR is defined in [RFC4506]. XDR is optimized for network performance but not human readable. The Stellar SDKs convert XDRs into friendlier formats. diff --git a/docs/learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx b/docs/learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx index 7d32852fec..b793c7f6d4 100644 --- a/docs/learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx +++ b/docs/learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx @@ -1,193 +1,1023 @@ --- title: "Liquidity Pools on the Stellar DEX: Provide Liquidity and Enable Asset Swaps" -sidebar_label: "SDEX" -description: "Learn how liquidity pools enable trading on the Stellar DEX. Understand how they work, provide liquidity, and enable decentralized asset swaps on the network." -sidebar_position: 100 +sidebar_label: "Stellar Decentralized Exchange" +description: "Learn how conversion liquidity enables direct trading. Understand how markets work, provide liquidity, and enable decentralized asset swaps on the network." +sidebar_position: 42 --- -# Liquidity on Stellar: SDEX & Liquidity Pools +import YouTube from "@site/src/components/YouTube"; + +# Liquidity on Stellar: the DEX & Liquidity Pools + + + +Stellar uses two protocols to help users trade: + +1. [Unified orderbook](#orderbook) for priced offers and +2. [Native AMMs](#amms) through liquidity pools. + +Collectively, these connected systems create the SDEX, a decentralized market for everyone. + +The SDEX works for all [assets](./stellar-data-structures/assets.mdx) on the network from the moment they're created . Its accessable to any [account](./stellar-data-structures/accounts.mdx) , and its liquidity can be seen by all as peers trade balances. You can iuse the exchange to pay someone in their preferred currency, rack prices, or even go public. :::note -This section is scoped specifically to liquidity regarding the AMM and SDEX built into the Stellar protocol and does not include information regarding smart contracts. +You can deploy any kind of smart contract, including a liquidity pool ([example](../../build/smart-contracts/example-contracts/liquidity-pool.mdx)). However, these isolated silos can fragment the core network's layer-1 [pathfinding](../glossary.mdx#pathfinding). This page focuses on integrated infrastructure that optimizes prermissionless, equitible, and free markets. ::: -Users can trade and convert assets on the Stellar network with the use of path payments through Stellar’s decentralized exchange and liquidity pools. +interoperability + +KO'd: + +- operation complexity +- offer aggregation +- atomic-swap -In this section, we will talk about the SDEX and liquidity pools. To learn about how these work together to execute transactions, see our [Path Payments Guide](../../build/guides/transactions/path-payments.mdx). +NO'd: -## SDEX +- accessibility +- transparency +- inclusion -The Stellar network acts as a decentralized distributed exchange that allows users to trade and convert assets with the [Manage Buy Offer](./transactions/list-of-operations#manage-buy-offer) and [Manage Sell Offer](./transactions/list-of-operations#manage-sell-offer) operations. The Stellar ledger stores both the balances held by user accounts and orders that user accounts make to buy or sell assets. +## The Price-Time Orderbook {#orderbook} -### Order books +Markets form on the basis of a [limit order book](https://wikipedia.org/wiki/Order_book) where individuals decide how much they will bid to purchase or ask to sell an asset. By keeping these orders concentrated on the SDEX's public [Horizon endpoints](../../data/apis/horizon/api-reference/resources/README.mdx), everyone can see the going market exchange rate. This transparent universal marketplace has the added benefit of obviating price oracles, proprietary contracts, or off-chain systems. -Stellar uses order books to operate its decentralized exchange. +A priced order only expires when you remove it from the ledger, offering supporting liquidity in down markets or rational selling in exuberant ones. More offers between pairs add volume to the market depth, promoting stable conversion rates. You can rely on stable trades because offers are funded from wallet assets on a debit-basis; creating common, distributed, peer-to-peer trading interests. + +The ledger's innovation lies in _where_ these offers get stored, matched, and disclosed. To submit directly to network nodes, you can use the [Manage Buy Offer object](../../data/apis/horizon/api-reference/resources/operations/object/buy-offer.mdx) operation, specifying how much $\mathcal{A}$ you want to purchase with $\mathcal{A}$ at $\mathcal{z}$ price. The protocol calculates the cost of the trade in terms of `amount * price` and locks up the `selling` asset you offer in exchange as collateral, until the trade is (partially) executed or cancelled. + +Alternatively, you can specify exactly how much of an asset you want to sell at a specific price using the [Manage Sell Offer object](../../data/apis/horizon/api-reference/resources/operations/object/sell-offer.mdx) operation. Anyone viewing this public Horizon interface to the ledger will see your orders as validators first based on price. For orders placed at the same price, the order that was received earlier is given priority and is executed before the newer one. + +#### "Order bookski" An order book is a record of outstanding orders on a network, and each record sits between two assets (wheat and sheep, for example). The order book for this asset pair records every account wanting to sell wheat for sheep and every account wanting to sell sheep for wheat. In traditional finance, buying is expressed as a “bid” order, and selling is expressed as an “ask” order (ask orders are also called offers). A couple of notes on order books on Stellar: -- The term “offers” usually refers specifically to ask orders. In Stellar, however, all orders are stored as selling- i.e., the system automatically converts bids to asks. Because of this, the terms “offer” and “order” are used interchangeably in the Stellar ecosystem. +- The term “offers” usually refers specifically to ask orders. However, in the protocol all orders are stored as selling—i.e., the system automatically converts bids to asks. Because of this, the terms “offer” and “order” are used interchangeably in the Stellar ecosystem.[^buys] - Order books contain all orders that are acceptable to parties on either side to make a trade. - Some assets will have a small or nonexistent order book between them. In these cases, Stellar facilitates path payments, which we’ll discuss later. +[^buys]: When you create a buy offer using the [`createBuyOffer`](../../data/apis/horizon/api-reference/resources/operations/object/buy-offer.mdx) operation, it is internally converted and stored as a sell offer. + To view an order book chart, see the [Order Book Wikipedia Page](https://en.wikipedia.org/wiki/Order_book). In addition, there are also plenty of video tutorials and articles out there that can help you understand how order books work in greater detail. -### Orders +### Order Execution -An account can create orders to buy or sell assets using the Manage Buy Offer, Manage Sell Offer, or Passive Order operations. The account must hold the asset it wants to exchange, and it must trust the issuer of the asset it is trying to buy. +An account can create orders to buy or sell assets using the [`Manage Buy Offer`](./transactions/list-of-operations.mdx#manage-buy-offer), [`Manage Sell Offer`](./transactions/list-of-operations.mdx#manage-sell-offer), or [`Create Passive Sell Offer`](./transactions/list-of-operations.mdx#create-passive-sell-offer) operations. The account must hold the asset it wants to exchange, and it must trust the issuer of the asset it is trying to buy. -Orders in Stellar behave like limit orders in traditional markets. When an account initiates an order, it is checked against the existing orderbook for that asset pair. If the submitted order is a marketable order (for a marketable buy limit order, the limit price is at or above the ask price; for a marketable sell limit order, the limit price is at or below the bid price), it is filled at the existing order price for the available quantity at that price. If the order is not marketable (i.e., does not cross an existing order), the order is saved on the orderbook until it is either consumed by another order, consumed by a path payment, or canceled by the account that created the order. +Orders behave like limit orders in traditional markets. When an account initiates an order, it is checked against the existing order book for that asset pair. If the submitted order is a marketable order (for a marketable buy limit order, the limit price is at or above the ask price; for a marketable sell limit order, the limit price is at or below the bid price), it is filled at the existing order price for the available quantity at that price. If the order is not marketable (i.e., does not cross an existing order), the order is saved on the order book until it is either consumed by another order, consumed by a path payment, or canceled by the account that created the order. -Each order constitutes a selling obligation for the selling asset and buying obligation for the buying asset. These obligations are stored in the account (for lumens) or trustline (for other assets) owned by the account creating the order. Any operation that would cause an account to be unable to satisfy its obligations — such as sending away too much balance — will fail. This guarantees that any order in the orderbook can be executed entirely. +Each order constitutes a selling obligation for the selling asset and buying obligation for the buying asset. These obligations are stored in the account (for lumens) or trustline (for other assets) owned by the account creating the order. Any operation that would cause an account to be unable to satisfy its obligations (such as sending away too much balance) will fail. This guarantees that any order in the order book can be executed entirely. -Orders are executed on a price-time priority, meaning orders will be executed based first on price; for orders placed at the same price, the order that was entered earlier is given priority and is executed before the newer one. +#### Purchase Assets (Manage Buy Offer) -### Price and operations +When creating a buy order via the Manage Buy Offer operation, the price is specified as 1 unit of the base currency (the asset being bought), in terms of the quote asset (the asset that is being sold). For example, if you’re buying 100 XLM in exchange for 20 USD, you would specify the price as `{20, 100}`, which would be the equivalent of 5 XLM for 1 USD (or \$.20 per XLM). -Each order in Stellar is quoted with an associated price and is represented as a ratio of the two assets in the order, one being the “quote asset” and the other being the “base asset”. This is to ensure there is no loss of precision when representing the price of the order (as opposed to storing the fraction as a floating-point number). +#### Manage Sell Offer -Prices are specified as a {`numerator`, `denominator`} pair with both components of the fraction represented as 32-bit signed integers. The numerator is considered the base asset, and the denominator is considered the quote asset. When expressing a price of “Asset A in terms of Asset B”, the amount of B is the denominator (and therefore the quote asset), and A is the numerator (and therefore the base asset). As a good rule of thumb, it’s generally correct to be thinking about the base asset that is being bought/sold (in terms of the quote asset). +When creating a sell order via the Manage Sell Offer operation, the price is specified as 1 unit of base currency (the asset being sold), in terms of the quote asset (the asset that is being bought). For example, if you’re selling 100 XLM in exchange for 40 USD, you would specify the price as `{40, 100}`, which would be the equivalent of 2.5 XLM for 1 USD (or \$.40 per XLM). -#### Manage Buy Offer +### Market Data -When creating a buy order in Stellar via the Manage Buy Offer operation, the price is specified as 1 unit of the base currency (the asset being bought), in terms of the quote asset (the asset that is being sold). For example, if you’re buying 100 XLM in exchange for 20 USD, you would specify the price as {20, 100}, which would be the equivalent of 5 XLM for 1 USD (or \$.20 per XLM). +> The SDEX facilitates seamless interoperability between different assets by combining orders in a unified order book. This design ensures that liquidity is concentrated, reducing spreads and improving execution for all participants. -#### Manage Sell Offer +No matter how an offer gets there, you can view your trading effects on the network. Let's go through an example of how to interact with the order book between two assets. First, we'll set up some basic primitives used throughout the page: -When creating a sell order in Stellar via the Manage Sell Offer operation, the price is specified as 1 unit of base currency (the asset being sold), in terms of the quote asset (the asset that is being bought). For example, if you’re selling 100 XLM in exchange for 40 USD, you would specify the price as {40, 100}, which would be the equivalent of 2.5 XLM for 1 USD (or \$.40 per XLM). + -#### Passive Order +```python +from stellar_sdk import Keypair, Server, TransactionBuilder, Network, Asset -Passive orders allow markets to have zero spread. If you want to exchange USD from anchor A for USD from anchor B at a 1:1 price, you can create two passive orders so the two orders don’t fill each other. +server = Server("https://horizon-testnet.stellar.org") -A passive order is an order that does not execute against a marketable counter order with the same price. It will only fill if the prices are not equal. For example, if the best order to buy BTC for XLM has a price of 100XLM/BTC, and you make a passive offer to sell BTC at 100XLM/BTC, your passive offer does not take that existing offer. If you instead make a passive offer to sell BTC at 99XLM/BTC it would cross the existing offer and fill at 100XLM/BTC. +# Account setup +privateKey = "SAXBT6KO6NJ6SJXHBO6EBC7I5ZB7DZFYNPQOLXZJOKQ2LSGY5FU7ZJZB" +publicKey = "GBRPYHIL2CI3R5N4A7WMBETDZQ24DXFQGNCJWHXPFRGFWZHJZZBDTWR2" -An account can place a passive sell order via the Create Passive Sell Offer operation. +# Asset setup +astroDollar = Asset("AstroDollar", "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7") +astroPeso = Asset("AstroPeso", "GBHNGLLIE3KWGKCHIKMHJ5HVZHYIK7WTBE4QF5PLAKL4CJGSEU7HZIW5") -### Fees +def newTxBuilder(publicKey): + return TransactionBuilder( + source_account = server.load_account(publicKey), + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = server.fetch_base_fee() + ).set_timeout(360) +``` -The order price you set is independent of the fee you pay for submitting that order in a transaction. Fees are always paid in XLM, and you specify them as a separate parameter when submitting the order to the network. +```js +const { + Keypair, + Server, + TransactionBuilder, + Asset, + Networks, +} = require("stellar-sdk"); + +const server = new Server("https://horizon-testnet.stellar.org"); + +// Account setup +const privateKey = "SAXBT6KO6NJ6SJXHBO6EBC7I5ZB7DZFYNPQOLXZJOKQ2LSGY5FU7ZJZB"; +const publicKey = "GBRPYHIL2CI3R5N4A7WMBETDZQ24DXFQGNCJWHXPFRGFWZHJZZBDTWR2"; +var account = await server.loadAccount(publicKey); + +// Asset setup +const astroDollar = new Asset( + "AstroDollar", + "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7", +); +const astroPeso = new Asset( + "AstroPeso", + "GBHNGLLIE3KWGKCHIKMHJ5HVZHYIK7WTBE4QF5PLAKL4CJGSEU7HZIW5", +); -To learn more about transaction fees, see our section on [Fees section](./fees-resource-limits-metering.mdx). +function newTxBuilder(source, signer, ...ops) { + let tx = new sdk.TransactionBuilder(source, { + fee: sdk.BASE_FEE, + networkPassphrase: sdk.Networks.TESTNET, + }); + ops.forEach((op) => tx.addOperation(op)); + tx = tx.setTimeout(360).build(); + tx.sign(signer); + return tx; +} +``` -## Liquidity pools +```java +import org.stellar.sdk.*; +import org.stellar.sdk.responses.AccountResponse; +import org.stellar.sdk.responses.SubmitTransactionResponse; -Liquidity pools enable automated market making on the Stellar network. Liquidity refers to how easily and cost-effectively one asset can be converted to another. +public class Liquidity { + static final Server server = new Server("https://horizon-testnet.stellar.org"); -### Automated Market Makers (AMMs) + // Account setup + static String privateKey = "SAXBT6KO6NJ6SJXHBO6EBC7I5ZB7DZFYNPQOLXZJOKQ2LSGY5FU7ZJZB"; + static String publicKey = "GBRPYHIL2CI3R5N4A7WMBETDZQ24DXFQGNCJWHXPFRGFWZHJZZBDTWR2"; + static AccountResponse account = server.accounts().account(publicKey); -Instead of relying on the buy and sell orders of decentralized exchanges, AMMs keep assets in an ecosystem liquid 24/7 using liquidity pools. + // Asset setup + Asset astroDollar = new AssetTypeCreditAlphaNum4( + "AstroDollar", + "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7" + ); + Asset astroPeso = new AssetTypeCreditAlphaNum4( + "AstroPeso", + "GBHNGLLIE3KWGKCHIKMHJ5HVZHYIK7WTBE4QF5PLAKL4CJGSEU7HZIW5" + ); -Automated market makers provide liquidity using a mathematical equation. AMMs hold two different assets in a liquidity pool, and the quantities of those assets (or reserves) are inputs for that equation (Asset A \* Asset B = k). If an AMM holds more of the reserve assets, the asset prices move less in response to a trade. + public static Transaction.Builder newTxBuilder(KeyPair source) { + AccountResponse sourceAccount = server.accounts().account(source.getAccountId()); + return new Transaction.Builder(sourceAccount, Network.TESTNET) + .setBaseFee(Transaction.MIN_BASE_FEE) + .setTimeout(360); + } +} +``` -#### AMM pricing +```go +import ( + "fmt" + "math" + "strconv" + "github.com/stellar/go-stellar-sdk/network" + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/txnbuild" + "github.com/stellar/go-stellar-sdk/clients/horizonclient" +) + +const server = "https://horizon-testnet.stellar.org" + +// Account setup +const privateKey = "SAXBT6KO6NJ6SJXHBO6EBC7I5ZB7DZFYNPQOLXZJOKQ2LSGY5FU7ZJZB" +const publicKey = "GBRPYHIL2CI3R5N4A7WMBETDZQ24DXFQGNCJWHXPFRGFWZHJZZBDTWR2" + +// Asset config +const astroDollar := txnbuild.CreditAsset{ + Code: "AstroDollar", + Issuer: "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7", +} +const astroPeso := txnbuild.CreditAsset{ + Code: "AstroPeso", + Issuer: "GBHNGLLIE3KWGKCHIKMHJ5HVZHYIK7WTBE4QF5PLAKL4CJGSEU7HZIW5", +} -AMMs are willing to make some trades and unwilling to make others. For example, if 1 EUR = 1.17 USD, then the AMM might be willing to sell 1 EUR for 1.18 USD and unwilling to sell 1 EUR for 1.16 USD. To determine what trades are acceptable, the AMM enforces an invariant. There are many possible invariants, and Stellar enforces a constant product invariant and so is known as a constant product market maker. This means that AMMs on Stellar must never allow the product of the reserves to decrease. +func newTxBuilder(publicKey string) (*txnbuild.Transaction, error) { + account, err := server.AccountDetail{ + horizonclient.AccountRequest{AccountID: publicKey} + } + check(err) // Confirms account & sequence number + txEnvelopeParams := &txnbuild.TransactionParams{ + SourceAccount: &account, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewTimeout(360), + IncrementSequenceNum: true, + } + return txnbuild.NewTransaction(txEnvelopeParams) +} +``` -For example, suppose the current reserves in the liquidity pool are 1000 EUR and 1170 USD which implies a product of 1,170,000. Selling 1 EUR for 1.18 USD would be acceptable because that would leave reserves of 999 EUR and 1171.18 USD, which implies a product of 1,170,008.82. But selling 1 EUR for 1.16 USD would not be acceptable because that would leave reserves of 999 EUR and 1171.16 USD, which implies a product of 1,169,988.84. + -AMMs decide exchange rates based on the ratio of reserves in the liquidity pool. If this ratio is different than the true exchange rate, arbitrageurs will come in and trade with the AMM at a favorable price. This arbitrage trade moves the ratio of the reserves back toward the true exchange rate. +{/* RPC change transition here */} -AMMs charge fees on every trade, which is a fixed percentage of the amount bought by the AMM. For example, if an automated market maker sells 100 EUR for 118 USD then the fee is charged on the USD. The fee is 30 bps, which is equal to 0.30%. If you actually wanted to make this trade, you would need to pay about 118.355 USD for 100 EUR. The automated market maker factors the fees into the constant product invariant, so in reality, the product of the reserves grows after every trade. +This setup gives us a simple keypair, transaction constructor, and trading assets. You cancel swap the [server URL](../../data/apis/rpc/providers.mdx) or [asset codes](./stellar-data-structures/assets.mdx#asset-code) to follow along with live exchange markets. Next, we'll read the current market price between AstroDollars and AstroPesos: -### Liquidity pool participation + -Any eligible participant can deposit assets into a liquidity pool, and in return, receive pool shares representing their ownership of that asset. If there are 150 total pool shares and one user owns 30, they are entitled to withdraw 20% of the liquidity pool asset at any time. +```python +orderbook = server.orderbook( + selling = astroPeso, + buying = astroDollar +).call() + +print("Bids:") +for bid in orderbook['bids']: + print(f"Price: {bid['price']}, Amount: {bid['amount']}") + +print("\nAsks:") +for ask in orderbook['asks']: + print(f"Price: {ask['price']}, Amount: {ask['amount']}") +``` -Pool shares are similar to other assets on Stellar but they cannot be transferred. You can only increase the number of pool shares you hold by depositing into a liquidity pool with the `LiquidityPoolDespositOp` and decrease the number of pool shares you hold by withdrawing from a liquidity pool with `LiquidityPoolWithdrawOp`. +```js +const orderbook = await server.orderbook(astroPeso, astroDollar).call(); -A pool share has two representations. The full representation is used with `ChangeTrustOp`, and the hashed representation is used in all other cases. When constructing the asset representation of a pool share, the assets must be in lexicographical order. For example, A-B is in the correct order but B-A is not. This results in a canonical representation of a pool share. +console.log("Bids:"); +orderbook.bids.forEach((bid) => { + console.log(`Price: ${bid.price}, Amount: ${bid.amount}`); +}); -AMMs charge a fee on all trades and the participants in the liquidity pool receive a share of the fee proportional to their share of the assets in the liquidity pool. Participants collect these fees when they withdraw their assets from the pool. The fee rate on Stellar is 30 bps, which is equal to 0.30%. These fees are completely separate from the network fees. +console.log("\nAsks:"); +orderbook.asks.forEach((ask) => { + console.log(`Price: ${ask.price}, Amount: ${ask.amount}`); +}); +``` -### Trustlines +```java +import java.util.List; + +class orderbook { + public static void main(String[] args) { + OrderBookResponse orderbook = server.orderbook(astroPeso, astroDollar).execute(); + + System.out.println("Bids:"); + List bids = orderbook.getBids(); + for (OrderBookResponse.Row bid : bids) { + System.out.println("Price: " + bid.getPrice() + ", Amount: " + bid.getAmount()); + } + + System.out.println("\nAsks:"); + List asks = orderbook.getAsks(); + for (OrderBookResponse.Row ask : asks) { + System.out.println("Price: " + ask.getPrice() + ", Amount: " + ask.getAmount()); + } + } +} +``` -Users need to establish trustlines to three different assets to participate in a liquidity pool: both the reserve assets (unless one of them is XLM) and the pool share itself. +```go +func main() { + orderbook, err := server.OrderBook(horizonclient.OrderBookRequest{ + Selling: astroPeso, + Buying: astroDollar, + }) + check(err) + + fmt.Println("Bids:") + for _, bid := range orderbook.Bids { + fmt.Printf("Price: %s, Amount: %s\n", bid.Price, bid.Amount) + } + + fmt.Println("\nAsks:") + for _, ask := range orderbook.Asks { + fmt.Printf("Price: %s, Amount: %s\n", ask.Price, ask.Amount) + } +} +``` -An account needs a trustline for every pool share it wants to own. It is not possible to deposit into a liquidity pool without a trustline for the corresponding pool share. Pool share trustlines differ from trustlines for other assets in a few ways: + -1. A pool share trustline cannot be created unless the account already has trustlines that are authorized or authorized to maintain liabilities for the assets in the liquidity pool. See below for more information about how authorization impacts pool share trustlines. -2. A pool share trustline requires 2 base reserves instead of 1. For example, an account (2 base reserves) with a trustline for asset A (1 base reserve), a trustline for asset B (1 base reserve), and a trustline for the A-B pool share (2 base reserves) would have a reserve requirement of 6 base reserves. +The more orders available to transact against, the more currency you can convert at any time without moving the market. Stellar stores these open orders as `offer` [objects](../../data/apis/horizon/api-reference/resources/offers/object.mdx) directly on chain. This transparency allows anyone to analyze the order book and understand the trading activity for any asset pair. -### Authorization +~~The network and by extension its [validators](https://stellarbeat.io) match orders based on the protocol rules of [Stellar Core](https://github.com/stellar/stellar-core/blob/fbb53d8ad42dcc12a046c9be949d654821a24d38/src/transactions/OfferExchange.cpp#L227-L550).~~ -Pool share trustlines cannot be authorized or de-authorized independently. Instead, the authorization of a pool share trustline is derived from the trustlines for the assets in the liquidity pool. This design is necessary because a liquidity pool may contain assets from two different issuers, and both issuers should have a say in whether the pool share trustline is authorized. +> ? +> +> Orders are filled at the same price or better than specified, ensuring fair execution. This means you receive the best possible price available in the market at that moment. -There are a few possibilities with regard to authorization. The behavior of the A-B pool share trustline is determined according to the following table: +Let's pretend you just got paid in Mexican Pesos, but you'd prefer to hold your savings in U.S. Dollars. After querying the [exchange rate](#reading-prices), we can send a new offer to the network, swapping Pesos for Dollars at our desired rate. By submitting a sell offer that matches the current market price, we can ensure our Pesos are converted to Dollars promptly. -| SCENARIO | BEHAVIOR | -| --- | --- | -| Trustlines for A and B are fully authorized | No restrictions on deposit and withdrawal | -| Trustline for A is fully authorized but trustline for B is authorized to maintain liabilities | Trustlines for A and B are authorized to maintain liabilities | -| Trustline for B is fully authorized but trustline for A is authorized to maintain liabilities | Trustlines for A and B are authorized to maintain liabilities | -| Trustlines for A and B are authorized to maintain liabilities | Trustlines for A and B are authorized to maintain liabilities | -| Trustline for A is not authorized or doesn’t exist | Pool share trustline does not exist | -| Trustline for B is not authorized or doesn’t exist | Pool share trustline does not exist | +#### Best Execution -If the issuer of A or B revokes authorization, then the account will automatically withdraw from every liquidity pool containing that asset and those pool share trustlines will be deleted. We say that these pool shares have been redeemed. For example, if the account participates in the A-B, A-C, and B-C liquidity pools and the issuer of A revokes authorization then the account will redeem from A-B and A-C but not B-C. For each redeemed pool share trustline, a Claimable Balance will be created for each asset contained in the pool if there is a balance being withdrawn and the redeemer is not the issuer of that asset. The claimant of the Claimable Balance will be the owner of the deleted pool share trustline, and the sponsor of the Claimable Balance will be the sponsor of the deleted pool share trustline. The BalanceID of each Claimable Balance is the SHA-256 hash of the `revokeID`. +The order book only matches offers at the price specified or better, when available. For instance, say there are four buyers offering 10 bananas per apple. You can sell an apple for 7 bananas, and the offer automatically exchanges for the higher 10 bananas. -### Operations +In traditional markets, this is precisely how market orders allow instant trades. They simply execute against the best available prices in the common order book. And since we can see every offer, the DEX also gives us the handy ability to know if there's enough orders at the prevailing price. -There are two operations that facilitate participation in a liquidity pool: `LiquidityPoolDeposit` and `LiquidityPoolWithdraw`. Use `LiquidityPoolDeposit` to start providing liquidity to the market. Use `LiquidityPoolWithdraw` to stop providing liquidity to the market. +When you submit an order, the protocol will also check whether an AMM (below) offers a better rate than priced orders. If so, your trade executes against the pool at the better conversion rate without priced DEX liquidity. To better understand the market, let's uncover the spread between the best AstroDollar offers, calculating a midpoint trading price. -However, users don’t need to participate in the pool to take advantage of what it’s offering: an easy way to exchange two assets. For that, just use `PathPaymentStrictReceive` or `PathPaymentStrictSend`. If your application is already using path payments, then you don’t need to change anything for users to take advantage of the prices available in liquidity pools. + -### Examples +```python +def getMidpointPrice(orderbook); + try: + highestBid = float(orderbook["bids"][0]["price"]) + lowestAsk = float(orderbook["asks"][0]["price"]) + midpointPrice = (highestBid + lowestAsk) / 2 + return round(midpointPrice, 7) + except IndexError: + print("Missing existing buy or sell offers.") + return null +``` -Here we will cover basic liquidity pool participation and querying. +```js +function getMidpointPrice(orderbook) { + try { + const highestBid = parseFloat(orderbook.bids[0].price); + const lowestAsk = parseFloat(orderbook.asks[0].price); + const midpoint = (highestBid + lowestAsk) / 2; + return parseFloat(midpoint.toFixed(7)); + } catch (error) { + console.error("Missing existing buy or sell offers."); + return null; + } +} +``` -#### Preamble +```java +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class orderbook { + public static Double getMidpointPrice(OrderBookResponse orderbook) { + List bids = orderbook.getBids(); + List asks = orderbook.getAsks(); + if (bids.isEmpty() || asks.isEmpty()) { + throw new IllegalStateException("Missing existing buy or sell offers."); + } + + double highestBid = Double.parseDouble(bids.get(0).getPrice()); + double lowestAsk = Double.parseDouble(asks.get(0).getPrice()); + double midpoint = (highestBid + lowestAsk) / 2.0; + + return new BigDecimal(midpoint) + .setScale( + 7, + RoundingMode.HALF_UP + ).doubleValue(); + } +} +``` -For all of the following examples, we’ll be working with three funded Testnet accounts. If you’d like to follow along, generate some keypairs and fund them via the friendbot. +```go +func getMidpointPrice(orderbook horizon.OrderBookSummary) (*float64, error) { + if len(orderbook.Bids) == 0 || len(orderbook.Asks) == 0 { + return nil, fmt.Errorf("Missing existing buy or sell offers") + } -The following code sets up the accounts and defines some helper functions. These should be familiar if you’ve played around with other examples like clawbacks. + highestBid, err := orderbook.Bids[0].PriceAsFloat() + check(err) + lowestAsk, err := orderbook.Asks[0].PriceAsFloat() + check(err) + midpointPrice := (highestBid + lowestAsk) / 2 + + rounded := math.Round(midpointPrice*1e7) / 1e7 + return &rounded, nil +} +``` + + + +:::note + +We have now found the price of `AstroDollar` as the response item's `base` asset. We can invert `price_r` from the [Horizon response](../../data/apis/horizon/api-reference/retrieve-an-order-book.api.mdx) to find the `counter` (`AstroPeso`) exchange rate. You can also [stream](../../data/apis/horizon/api-reference/structure/streaming) server updates to keep a view of the orderbook maerket up to date. + +::: + +### Recent Trades + +A more complete understanding of market liquitiy might incldue volume analysis. To affirm the fairness of this example price, we can also check up to 25 recent trade rates: +```python +def getAverageRecentPrice(): + trades = server.trades().for_asset_pair( + selling = astroPeso, + buying = astroDollar + ).limit(25).call() + + recentPrices = [ + float(trade["price"]) + for trade in trades["_embedded"]["records"] + ] + if recentPrices: + return sum(recentPrices) / len(recentPrices) + else: + return None + +def sanityCheck(midpointPrice, averageRecentPrice): + differencePercent = abs(midpointPrice - averageRecentPrice) / midpointPrice + return differencePercent <= 0.05 # arbitrary maximum slippage of 5% +``` + ```js -const sdk = require("stellar-sdk"); -const BigNumber = require("bignumber.js"); +async function getAverageRecentPrice() { + const trades = await server + .trades() + .forAssetPair(astroPeso, astroDollar) + .limit(25) + .call(); + const recentPrices = trades.records.map((trade) => parseFloat(trade.price)); + if (recentPrices.length > 0) { + return ( + recentPrices.reduce((sum, price) => sum + price, 0) / recentPrices.length + ); + } else { + return null; + } +} -let server = new sdk.Server("https://horizon-testnet.stellar.org"); +function sanityCheck(midpointPrice, averageRecentPrice) { + const differencePercent = + Math.abs(midpointPrice - averageRecentPrice) / midpointPrice; + return differencePercent <= 0.05; // arbitrary maximum slippage of 5% +} +``` -/// Helps simplify creating & signing a transaction. -function buildTx(source, signer, ...ops) { - let tx = new sdk.TransactionBuilder(source, { - fee: sdk.BASE_FEE, - networkPassphrase: sdk.Networks.TESTNET, +```java +public class Orderbook { + public static Double getAverageRecentPrice() { + Page trades = server.trades() + .forAssetPair(astroPeso, astroDollar) + .limit(25) + .execute(); + List tradeRecords = trades.getRecords(); + if (tradeRecords.isEmpty()) { + return null; + } + double totalPrice = tradeRecords.stream() + .mapToDouble( + trade -> Double.parseDouble( + trade.getPrice().toString() + ) + ).sum(); + return totalPrice / tradeRecords.size(); + } + + public static boolean sanityCheckOK(Double midpointPrice, Double averageRecentPrice) { + double differencePercent = Math.abs(midpointPrice - averageRecentPrice) / midpointPrice; + return differencePercent <= 0.05; // arbitrary maximum slippage of 5% + } +} +``` + +```go +func getAverageRecentPrice() (*float64, error) { + tradesRequest := horizonclient.TradeRequest{ + BaseAsset: astroPeso, + CounterAsset: astroDollar, + Limit: 25, + } + trades, err := server.Trades(tradesRequest) + check(err) + + if len(trades.Embedded.Records) == 0 { + return nil, nil + } + var total float64 + for _, trade := range trades.Embedded.Records { + price, err := strconv.ParseFloat(trade.Price, 64) + check(err) + total += price + } + + average := total / float64( + len(trades.Embedded.Records) + ) + return &average, nil +} + +func sanityCheck(midpointPrice, averageRecentPrice *float64) bool { + differencePercent := math.Abs(*midpointPrice - *averageRecentPrice) / *midpointPrice + return differencePercent <= 0.05 // arbitrary maximum slippage of 5% +} +``` + + + +:::tip Scanning + +This example examines orders using the default [paging token limit](../../data/apis/horizon/api-reference/structure/pagination/README.mdx#larger-queries). For more insight into the depth of a market, you can increase the request limit and even continue paging through results. For very liquid assets, there can be thousands of active offers awaiting exchange. + +::: + +### Limit-Order Example {#new-order} + +We can confidently convert our AstroPesos now that we know the going rate paid for them: + + + +```python +offerPrice = round( getMidpointPrice(orderbook), 7 ) + +transaction = ( + newTxBuilder(publicKey) + .append_manage_sell_offer_op( + selling = astroPeso, + buying = astroDollar, + amount = "1000", + price = str(getMidpointPrice(orderbook)) + ) + .build() +) + +keypair = Keypair.from_secret(privateKey) +transaction.sign(keypair) +response = server.submit_transaction(transaction) +``` + +```js +const transaction = newTxBuilder(account, Keypair.fromSecret(privateKey)) + .addOperation({ + type: "manageSellOffer", + selling: astroPeso, + buying: astroDollar, + amount: "1000", + price: midpointPrice.toString(), + }) + .build(); + +transaction.sign(Keypair.fromSecret(privateKey)); +const response = await server.submitTransaction(transaction); +``` + +```java +class firstTrade { + public static void main(String[] args) { + KeyPair signer = KeyPair.fromSecretSeed(Liquidity.privateKey); + Transaction.Builder transaction = Liquidity.newTxBuilder(signer); + + transaction.addOperation( + new ManageSellOfferOperation.Builder( + Liquidity.astroPeso, // Selling + Liquidity.astroDollar, // Buying + "1000", // Amount + midpointPrice.toString() + ).build() + ); + + Transaction transaction = transaction.build(); + transaction.sign(signer); + SubmitTransactionResponse response = Liquidity.server.submitTransaction(transaction); + } +} +``` + +```go +func main() { + keypair, err := keypair.ParseFull(privateKey) + check(err) + account, err := server.AccountDetail( + horizonclient.AccountRequest{AccountID: publicKey} + ) + check(err) + + price, err := getMidpointPrice(orderbook) + check(err) + + transaction, err := newTxBuilder(publicKey) + check(err) + transaction, err = transaction.AddOperation(&txnbuild.ManageSellOffer{ + Selling: astroPeso, + Buying: astroDollar, + Amount: "1000", + Price: *price, + }) + check(err) + + signedTx, err := transaction.Sign(network.TestNetworkPassphrase, keypair) + check(err) + resp, err := server.SubmitTransaction(signedTx) + check(err) +} +``` + + + +ocne hte network receives your order, the protocol ueries against avalibe liquidity. If our trades mathes iwth exists orders, our `TransactionResult` immediately claims offers, like the example below. Remainingn asetsd create a new [OfferID](../../data/apis/horizon/api-reference/get-offer-by-offer-id.api.mdx) from the account. + + + +```json +{ ... + + "offers_claimed": [ + { + "order_book": { + "seller_id": "GDAVYIICLHJIQACEC3FQFQAZDMIR4IRUJMURN446DD6SLILZS2FPUSDC", + "offer_id": 1634174581, // existing account trades fully + "asset_sold": { + "credit_alphanum4": { + "asset_code": "AstroPeso", + "issuer": "GBHNGLLIE3KWGKCHIKMHJ5HVZHYIK7WTBE4QF5PLAKL4CJGSEU7HZIW5" + } + }, + "amount_sold": 4863901365, + "asset_bought": { + "credit_alphanum4": { + "asset_code": "AstroDollar", + "issuer": "GDRM3MK6KMHSYIT4E2AG2S2LWTDBJNYXE4H72C7YTTRWOWX5ZBECFWO7" + } + }, + "amount_bought": 438292475 + } + }, ... + ] + +... } +``` + + + +The first, most basic option is when a new order crosses the price set by an outstanding order. The two instantly cross once the new order gets accepted, and the transaction generates a taker `contraID` rather than an `offerID` for the "buyer" of the existing liquidity. This counter ID is a design scheme choice to return an offer ID to orders immediately executed by taking liquidity away from existing order books. + +In actual queries, you can ignore the arbitrarily large simulated ID in deference to the `OfferEntry` owned by a selling account making the market with standing liquidity. This standing [ledger entry](./stellar-data-structures/ledgers/ledger-entries) will update over time until all offers are filled or deleted. Each offer can be thought of as a trade with a cumulative price which we can easily find with its ID. + +Both path payments and liquidity pools are constantly interacting with the DEX to check for (partial) offer execution. Comparatively, [path payments](../../build/guides/transactions/path-payments.mdx) immediately execute in full or fail to send. Indeed, every new path payment and liquidity pool operation can only occur because of existing order book offers or new user swap requests. + +:::important Claiming + +If our trade does take from existing offers in full, we receive a `ClaimAtom` result with the offers or AMMs used. If it went through an AMM pool, we receive a different report based on asset identifiers, shown below. This flexibility allowed best execution for our offer. + + + +```json +{ ... + +"offers": [ + { + "liquidity_pool": { + "liquidity_pool_id": "63268ced073f689a5b0b45aa5dc515190acc5f4c0b15d15894c4bb78403e517f", + "asset_sold": { + "credit_alphanum4": { + "asset_code": "AstroPeso", + "issuer": "GBHNGLLIE3KWGKCHIKMHJ5HVZHYIK7WTBE4QF5PLAKL4CJGSEU7HZIW5" + } + }, + "amount_sold": 4863901, + "asset_bought": { + "credit_alphanum4": { + "asset_code": "AstroDollar", + "issuer": "AstroDollar" + } + }, + "amount_bought": 438292 + } + }, ... +], + +... } +``` + + + +::: + +> Now that we know how to submit an order to the SDEX, let's walk through reading the current order book. This entails collecting all the buyers for a specific currency pair and comparing this demand to all the sellers of that pair. We'll stick with our pesos-dollars example and crossing/implementing potential passive offers. + +Since all orders for a currency pair fall into the same SDEX order book, you can know that you're getting the best exchange rate between two explicit assets. Accordingly, you can analyze the past valuation of a currency by reading its exchanged trades feed. We'll continue our example and set up a recent trading price query: + + + +```python +response = server.trades().for_asset_pair(astroPeso, astroDollar).call() + +for trade in response['_embedded']['records']: + price = int(trade['price_r']['n']) / int(trade['price_r']['d']) + print(f"Trade ID: {trade['id']}, Price: {price}, Amount: {trade['base_amount']}") + +``` + +```js +(async function () { + const response = await server + .trades() + .forAssetPair(astroPeso, astroDollar) + .call(); + + response.records.forEach((trade) => { + const price = parseInt(trade.price_r.n) / parseInt(trade.price_r.d); + console.log( + `Trade ID: ${trade.id}, Price: ${price}, Amount: ${trade.base_amount}`, + ); }); - ops.forEach((op) => tx.addOperation(op)); - tx = tx.setTimeout(30).build(); - tx.sign(signer); - return tx; +})(); +``` + +```java +public static void main(String[] args) throws Exception { + Page trades = server.trades() + .forAssetPair(astroPeso, astroDollar) + .execute(); + + for (TradeResponse trade : trades.getRecords()) { + double price = (double) trade.getPrice().getN() / trade.getPrice().getD(); + System.out.println("Trade ID: " + trade.getId() + ", Price: " + price + ", Amount: " + trade.getBaseAmount()); + } } -/// Returns the given asset pair in "protocol order." -function orderAssets(A, B) { - return sdk.Asset.compare(A, B) <= 0 ? [A, B] : [B, A]; +``` + +```go +func main() { + trades, err := server.Trades(horizonclient.TradeRequest{ + BaseAsset: astroPeso, + CounterAsset: astroDollar, + }) + check(err) + + for _, trade := range trades.Embedded.Records { + price := float64(trade.PriceR.N) / float64(trade.PriceR.D) + fmt.Printf("Trade ID: %s, Price: %f, Amount: %s\n", trade.ID, price, trade.BaseAmount) + } } +``` + + + +## Automated Market Makers {#amms} + +Market makers are businesses that traditionally help establish liquidity on exchanges. They are historically a party willing to buy or sell an asset at any time. They maintain an “inventory” of said asset (to sell) alongside a stockpile of cash (to buy). + +A market maker hopes to get a buyer real soon when they purchase an asset, or a seller if they run out. They profit off the difference between what they buy an asset at and what they can sell it for—called the “spread.” The more volatile an asset, the less competition there might be between buyers and sellers to narrow the spread. + +In other words, it becomes more expensive to trade an asset the less liquid it becomes, as you will need to “cross the spread” more often to fill sizable orders in reasonable time. AMMs democratize the process of market-making by encoding the maximum spread into a pool of capital, creating liquidity. When users want to trade, their order can execute against the AMM in place of explicit SDEX order book offers, should no bids or asks exist inside the spread. + +:::info + +Liquidity refers to how easily and cost-effectively one asset can be converted to another. Market-making businesses accept the inherent risk that an asset will move in one direction or another before a closing trade can cancel out any positions (or lack thereof), and they expect that the “fees” they can extract from the spread will exceed any trade losses over time. These small costs accrue in AMM pools, and gains become shared by users depositing their capital. + +::: + +#### Passive Orders {#passive-offer} + +Creation only There are plenty of MM bots deployed on the DDEX. Outside the scope of this[^mm-bots] + +[^mm-bots]: hrefs are https://github.com/stellar-deprecated/kelp and https://github.com/JFWooten4/trading-algos/blob/main/mm-yUSDC-USDC.py + +Note that regular offers made later than your passive sell offer can act on and take your passive sell offer, even if the regular offer is of the same price as your passive sell offer. + +Passive sell offers allow market makers to have zero spread. If you want to trade EUR for USD at 1:1 price and USD for EUR also at 1:1, you can create two passive sell offers so the two offers don’t immediately act on each other. + +Once the passive sell offer is created, you can manage it like any other offer using the manage offer operation — see ManageBuyOffer for more details. + +PY src check + +Passive orders allow markets to have zero spread. They come in handy if you're making a market to exchange USD from `[anchor](./anchors.mdx) A` for USD from `[anchor](./anchors.mdx) B` at a 1:1 price. A passive bid and ask let you create two sides of a book that don’t fill each other. + +Our example so far used assets with presumably different values, but we might use this order as an asset issuer to peg alike anchored assets. + +A passive order is an order that does not execute against a marketable counter order with the same price. It will only fill if the prices are not equal. For example, + +if the best order to buy BTC for XLM has a price of 100 XLM/BTC, and you make a passive offer to sell BTC at 100 XLM/BTC, + +your passive offer does not take that existing offer. + +If you instead make a passive offer to sell BTC at 99 XLM/BTC it would cross the existing offer and fill at 100 XLM/BTC. + +An account can place a passive sell order via the [Create Passive Sell Offer](./transactions/list-of-operations.mdx#create-passive-sell-offer) operation. + +Setup here as comp between someone who might be associated with an issuer or have an interest in market -/// Returns all of the accounts we'll be using. -function getAccounts() { - return Promise.all(kps.map((kp) => server.loadAccount(kp.publicKey()))); +They can manually use a bot to update prices (link kelp archive and yUSD bot if repo public — dir) + +Example of passive sell setup as a means of setting to not cross self, otherwise natural trade intent over + +Parallel automation as using smart contracts on other networks. Explain here the liquidity singularity need + +The one page transition to native setup with 18 ref to difference between stable curves, protocol interoperability bonds + +The rest of this example will presume you are an individual seeking to invest on a native pool + +### Liquidity Pools + +Stabled curve transition to industry-standard V2 pricing + +#### Authorization Setup \\ + +#### \\ Operations + +There are two operations that facilitate participation in a liquidity pool: [`LiquidityPoolDeposit`](./transactions/list-of-operations.mdx#liquidity-pool-deposit) and [`LiquidityPoolWithdraw`](./transactions/list-of-operations.mdx#liquidity-pool-withdraw). Use `LiquidityPoolDeposit` to start providing liquidity to the market. Use `LiquidityPoolWithdraw` to stop providing liquidity to the market. + +However, users don’t need to participate in the pool to take advantage of what it’s offering: an easy way to exchange two assets. For that, just use `PathPaymentStrictReceive` or `PathPaymentStrictSend`. If your application is already using path payments, then you don’t need to change anything for users to take advantage of the prices available in liquidity pools. ✅ + +### Deterministic Pricing + +Instead of relying on the buy and sell orders of the SDEX, AMMs keep assets liquid 24/7 using pooled capital and a mathematical equation. AMMs hold two different assets in a liquidity pool, and the quantities of those assets (or reserves) are inputs for that equation (Asset $\mathcal{A}$ \* Asset $\mathcal{B}$ = k). If an AMM holds more of the reserve assets, the asset prices move less in response to a trade. + +When you submit an.. + +#### AMM Calculations + +AMMs are willing to make some trades and unwilling to make others. For example, if 1 EUR = 1.17 USD, then the AMM might be willing to sell 1 EUR for 1.18 USD and unwilling to sell 1 EUR for 1.16 USD. To determine what trades are acceptable, the AMM enforces a [trading function](https://en.wikipedia.org/wiki/Constant_function_market_maker). The protocol supports arbitrary functions, although it presently only adopts a constant-product market maker. This means that AMMs presently never allow the product of the reserves to decrease, although the protocol is configured to use other bonding curves. + +For example, suppose the current reserves in the liquidity pool are 1000 EUR and 1170 USD which implies a product of 1,170,000. Selling 1 EUR for 1.18 USD would be acceptable because that would leave reserves of 999 EUR and 1171.18 USD, which implies a product of 1,170,008.82. But selling 1 EUR for 1.16 USD would not be acceptable because that would leave reserves of 999 EUR and 1171.16 USD, which implies a product of 1,169,988.84. + +AMMs decide exchange rates based on the ratio of reserves in the liquidity pool. If this ratio is different than the true exchange rate, arbitrageurs will come in and trade with the AMM at a favorable price. This arbitrage trade moves the ratio of the reserves back toward the true exchange rate. + +AMMs charge fees on every trade, which is a fixed percentage of the amount bought by the AMM. For example, if an automated market maker sells 100 EUR for 118 USD then the fee is charged on the USD. The fee is 0.30%. If you actually wanted to make this trade, you would need to pay about 118.355 USD for 100 EUR. The automated market maker factors the fees into the trading function, so the product of the reserves grows after every trade. + +##### Viewing Activity + +You can access the transactions, operations, and effects related to a liquidity pool if you want to track its activity. Let’s see how we can track the latest deposits in a pool (suppose `poolId` is defined as before): + + + +```python +def watch_liquidity_pool_activity(): + for op in ( + server.operations() + .for_liquidity_pool(liquidity_pool_id=pool_id) + .cursor("now") + .stream() + ): + if op["type"] == "liquidity_pool_deposit": + print("Reserves deposited:") + for r in op["reserves_deposited"]: + print(f" {r['amount']} of {r['asset']}") + print(f" for pool shares: {op['shares_received']}") + # ... +``` + +```js +server + .operations() + .forLiquidityPool(poolId) + .call() + .then((ops) => { + ops.records + .filter((op) => op.type == "liquidity_pool_deposit") + .forEach((op) => { + console.log("Reserves deposited:"); + op.reserves_deposited.forEach((r) => + console.log(` ${r.amount} of ${r.asset}`), + ); + console.log(" for pool shares: ", op.shares_received); + }); + }); +``` + +```java + + + +tbd + + + +public void onEvent(OperationResponse op) { + if ("liquidity_pool_deposit".equals(op.getType())) { + System.out.println("Reserves deposited:"); + op.getExtras().get("reserves_deposited").forEach(r -> { + System.out.println(" " + r.get("amount").getAsString() + + " of " + r.get("asset").getAsString()); + }); + System.out.println(" for pool shares: " + + op.getExtras().get("shares_received").getAsString()); + } } +``` + +```go + + -const kps = [ + +if op.Type == "liquidity_pool_deposit" { + fmt.Println("Reserves deposited:") + if reserves, ok := op.Extras["reserves_deposited"].([]interface{}); ok { + for _, r := range reserves { + if res, ok := r.(map[string]interface{}); ok { + fmt.Printf(" %s of %s\n", res["amount"], res["asset"]) + } + } + } + fmt.Printf(" for pool shares: %v\n", op.Extras["shares_received"]) +} +``` + + + +#### AMM Participation + +A pool of deposits from accounts allows trades to execute against the predefined market-making algorithm. + +Any eligible participant can deposit assets into an AMM and receive pool shares representing their AMM ownership in return. If there are 150 total pool shares and one user owns 30, they are entitled to withdraw 20% of the liquidity pool asset at any time. + +Pool shares are similar to other assets, but they cannot be transferred yet. You can only increase the number of pool shares you hold by depositing into a liquidity pool with the `LiquidityPoolDepositOp` and decrease the number of pool shares you hold by withdrawing from a liquidity pool with `LiquidityPoolWithdrawOp`. Accordingly, pool shares cannot be sent in payments, sold using offers, or within claimable balances. + +A pool share has two representations. The full representation is used with [`ChangeTrustOp`](./transactions/list-of-operations.mdx#change-trust), and the hashed representation is used in all other cases. When constructing the asset representation of a pool share, the assets must be in lexicographical order. For example, $\mathcal{A}$–$\mathcal{B}$ is in the correct order but $\mathcal{B}$–$\mathcal{A}$ is not. This results in a canonical representation of a pool share. + +AMMs charge a fee on all trades and the participants in the liquidity pool receive a share of the fee proportional to their share of the assets in the liquidity pool. Participants collect these fees when they withdraw their assets from the pool. The [community agreed](https://groups.google.com/g/stellar-dev/c/Ofb2KXwzva0/m/kyYI8Es9AQAJ) on the current fixed rate of 0.30%, the fee used in Uniswap V2. These charges are completely separate from the network fees. + +#### AMM Trustlines + +Users need to establish trustlines to three different assets to participate in a liquidity pool: both the reserve assets (unless one of them is XLM) and the pool share itself. + +An account needs a trustline for every pool share it wants to own. It is not possible to deposit into a liquidity pool without a trustline for the corresponding pool share. Pool share trustlines differ from trustlines for other assets in a few ways: + +1. A pool share trustline cannot be created unless the account already has trustlines that are authorized or authorized to maintain liabilities for the assets in the liquidity pool. See below for more information about how authorization impacts pool share trustlines. +2. A pool share trustline requires 2 base reserves instead of 1. For example, an account (2 base reserves) with a trustline for asset $\mathcal{A}$ (1 base reserve), a trustline for asset $\mathcal{B}$ (1 base reserve), and a trustline for the $\mathcal{A}$–$\mathcal{B}$ pool share (2 base reserves) would have a reserve requirement of 6 base reserves. + +### Liquidity-Pool Example + +Here we will cover basic AMM participation and querying. + +For all of the following examples, we’ll be working with three funded Testnet accounts. If you’d like to follow along, generate some keypairs and fund them via the friendbot. + +The following code sets up the accounts and defines some helper functions. These should be familiar if you’ve played around with other examples like clawbacks.[^revoking] + +[^revoking]: The protocol handles compliance edge cases if an account has trustlines revoked for one or more of the assets in an AMM. Once this happens, the account is forced to redeem all AMM pool shares. Since they by definition are not authorized to hold at least one asset, it gets returned to them as an unconditional claimable balance, available once the issuer authorizes the account again. + + + +```python +from decimal import Decimal +from stellar_sdk import * + +server = Server("https://horizon-testnet.stellar.org") + +secrets = [ + "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", + "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", + "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P", +] +keypairs = [Keypair.from_secret(secret = secrets) for secrets in secretsList] + +# Returns the given asset pair in "protocol order" +def orderAsset(a, b): + return [a, b] if LiquidityPoolAsset.is_valid_lexicographic_order(a, b) else [b, a] + +# kp0 issues the assets +kp0 = keypairs[0] +assetA, assetB = orderAsset( + Asset("A", kp0.public_key), + Asset("B", kp0.public_key) +) + +def distributeAssets(issuerKp, recipientKp, assets): + builder = newTxBuilder(issuerKp.public_key) + for asset in assets: + builder.append_change_trust_op( + asset = asset, + source = recipientKp.public_key + ).append_payment_op( + destination = recipientKp.public_key, + asset = asset, + amount = "100000", + source = issuerKp.public_key + ) + + tx = builder.build() + tx.sign(issuerKp) + tx.sign(recipientKp) + return server.submit_transaction(tx) + +def preamble(): + resp1 = distributeAssets(kp0, keypairs[1], [assetA, assetB]) + resp2 = distributeAssets(kp0, keypairs[2], [assetA, assetB]) + # ... +``` + +```js +const sdk = require("stellar-sdk"); + +let server = new sdk.Server("https://horizon-testnet.stellar.org"); + +const keypairs = [ "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P", ].map((s) => sdk.Keypair.fromSecret(s)); +// Returns the given asset pair in "protocol order" +function orderAssets(A, B) { + return sdk.Asset.compare(A, B) <= 0 ? [A, B] : [B, A]; +} + // kp0 issues the assets -const kp0 = kps[0]; +const kp0 = keypairs[0]; const [A, B] = orderAssets( - ...[new sdk.Asset("A", kp0.publicKey()), new sdk.Asset("B", kp0.publicKey())], + new sdk.Asset("A", kp0.publicKey()), + new sdk.Asset("B", kp0.publicKey()), ); -/// Establishes trustlines and funds `recipientKps` for all `assets`. +// Establishes trustlines and funds `recipientKps` for all `assets` function distributeAssets(issuerKp, recipientKps, ...assets) { return server.loadAccount(issuerKp.publicKey()).then((issuer) => { const ops = recipientKps @@ -195,7 +1025,6 @@ function distributeAssets(issuerKp, recipientKps, ...assets) { assets.map((asset) => [ sdk.Operation.changeTrust({ source: recipientKp.publicKey(), - limit: "100000", asset: asset, }), sdk.Operation.payment({ @@ -208,143 +1037,259 @@ function distributeAssets(issuerKp, recipientKps, ...assets) { ) .flat(2); - let tx = buildTx(issuer, issuerKp, ...ops); + let tx = newTxBuilder(issuer, issuerKp, ...ops); tx.sign(...recipientKps); return server.submitTransaction(tx); }); } function preamble() { - return distributeAssets(kp0, [kps[1], kps[2]], A, B); + return distributeAssets(kp0, [keypairs[1], keypairs[2]], A, B); } ``` -```python -from decimal import Decimal -from typing import List, Any, Dict +```java +import java.util.ArrayList; +import java.util.List; -from stellar_sdk import * +private static final String[] secrets = { + "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", + "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", + "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P" +}; + +private static final List keypairs = new ArrayList<>(); +static { + for (String secret : secrets) { + keypairs.add(KeyPair.fromSecretSeed(secret)); + } +} -server = Server("https://horizon-testnet.stellar.org") +// Establishes trustlines and funds the recipient for all `assets` +public static void distributeAssets(KeyPair issuer, KeyPair recipient, Asset[] assets) throws Exception { + Transaction.Builder builder = newTxBuilder(issuer); + for (Asset asset : assets) { + builder.addOperation( + new ChangeTrustOperation.Builder(asset) + .setSourceAccount(recipient.getAccountId()) + .build() + ); + builder.addOperation( + new PaymentOperation.Builder( + recipient.getAccountId(), + asset, + "100000") + .setSourceAccount(issuer.getAccountId()) + .build() + ); + } -# Preamble -def new_tx_builder(source: str) -> TransactionBuilder: - network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE - base_fee = 100 - source_account = server.load_account(source) - builder = TransactionBuilder( - source_account=source_account, network_passphrase=network_passphrase, base_fee=base_fee - ).set_timeout(30) - return builder + Transaction transaction = builder.build(); + transaction.sign(issuer); + transaction.sign(recipient); + server.submitTransaction(transaction); +} +// Function to order assets in "protocol order" +public static Asset[] orderAssets(Asset assetA, Asset assetB) { + return LiquidityPoolAsset.isValidLexicographicOrder(assetA, assetB) ? + new Asset[]{assetA, assetB} : + new Asset[]{assetB, assetA}; +} -# Returns the given asset pair in "protocol order." -def order_asset(a: Asset, b: Asset) -> List[Asset]: - return [a, b] if LiquidityPoolAsset.is_valid_lexicographic_order(a, b) else [b, a] +public static void preamble() throws Exception { + KeyPair kp0 = keypairs.get(0); + Asset[] assets = orderAssets( + new AssetTypeCreditAlphaNum4("A", kp0.getAccountId()), + new AssetTypeCreditAlphaNum4("B", kp0.getAccountId()) + ); + distributeAssets(kp0, keypairs.get(1), assets); + distributeAssets(kp0, keypairs.get(2), assets); +} +``` +```go +var secrets = []string{ + "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", + "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", + "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P", +} -secrets = [ - "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", - "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", - "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P", -] -kps = [Keypair.from_secret(secret=secret) for secret in secrets] +func distributeAssets(issuerKp, recipientKp *keypair.Full, assets []txnbuild.Asset) error { + tx, err := newTxBuilder(issuerKp.Address()) + check(err) + + ops := []txnbuild.Operation{} + for _, asset := range assets { + ops = append(ops, + &txnbuild.ChangeTrust{ + Line: asset, + SourceAccount: recipientKp.Address(), + }, + &txnbuild.Payment{ + Destination: recipientKp.Address(), + Asset: asset, + Amount: "100000", + SourceAccount: issuerKp.Address(), + }, + ) + } -# kp0 issues the assets -kp0 = kps[0] -asset_a, asset_b = order_asset(Asset("A", kp0.public_key), Asset("B", kp0.public_key)) - - -def distribute_assets( - issuer_kp: Keypair, recipient_kp: Keypair, assets: List[Asset] -) -> Dict[str, Any]: - builder = new_tx_builder(issuer_kp.public_key) - for asset in assets: - builder.append_change_trust_op( - asset=asset, limit="100000", source=recipient_kp.public_key - ).append_payment_op( - destination=recipient_kp.public_key, - asset=asset, - amount="100000", - source=issuer_kp.public_key, - ) + tx.Params.Operations = ops + tx, err := tx.BuildSignEncode(issuerKp, recipientKp) + check(err) + + _, err = server.SubmitTransactionXDR(tx) + return err +} - tx = builder.build() - tx.sign(issuer_kp) - tx.sign(recipient_kp) - resp = server.submit_transaction(tx) - return resp +// Function to order assets in "protocol order" +func orderAssets(a, b txnbuild.Asset) (txnbuild.Asset, txnbuild.Asset) { + if a.LessThan(b) { + return a, b + } + return b, a +} +func preamble() { + kp0 := keypair.MustParseFull(secrets[0]) + kp1 := keypair.MustParseFull(secrets[1]) + kp2 := keypair.MustParseFull(secrets[2]) -def preamble() -> None: - resp1 = distribute_assets(kp0, kps[1], [asset_a, asset_b]) - resp2 = distribute_assets(kp0, kps[2], [asset_a, asset_b]) - # ... + assetA, assetB := orderAssets( + txnbuild.CreditAsset{"A", kp0.Address()}, + txnbuild.CreditAsset{"B", kp0.Address()}, + ) + + err := distributeAssets(kp0, kp1, []txnbuild.Asset{assetA, assetB}) + check(err) + err = distributeAssets(kp0, kp2, []txnbuild.Asset{assetA, assetB}) + check(err) +} ``` Here, we use `distributeAssets()` to establish trustlines and set up initial balances of two custom assets (`A` and `B`, issued by `kp0`) for two accounts (`kp2` and `kp3`). For someone to participate in the pool, they must establish trustlines to each of the asset issuers and to the pool share asset (explained below). +:::danger TODO + +Case when buying an asset, amount acquired > existing trustline amount. Implicates reserves art. + +::: + Note the `orderAssets()` helper here. Operations related to liquidity pools refer to the asset pair arbitrarily as `A` and `B`; however, they must be “ordered” such that `A` < `B`. This ordering is defined by the protocol, but its details should not be relevant (if you’re curious, it’s essentially lexicographically ordered by asset type, code, then issuer). We can use the comparison methods built into the SDKs (like `Asset.compare`) to ensure we pass them in the right order and avoid errors. -#### Participation: Creation +#### Participant Creation -First, let's create a liquidity pool for the asset pair defined in the preamble. This involves establishing a trustline to the pool itself: +First, let's create an AMM for the asset pair defined in the preamble. This involves establishing a trustline to the pool itself. +```python +poolAsset = LiquidityPoolAsset( + asset_a = assetA, + asset_b = assetB +) + +def establishPoolTrustline(source, poolAsset): + tx = ( + newTxBuilder(source.public_key) + .appendChangeTrustOp(asset = poolAsset) + .build() + ) + tx.sign(source) + return server.submitTransaction(tx) +``` + ```js -const poolShareAsset = new sdk.LiquidityPoolAsset( - A, - B, - sdk.LiquidityPoolFeeV18, -); +const poolAsset = new sdk.LiquidityPoolAsset(A, B, sdk.LiquidityPoolFeeV18); function establishPoolTrustline(account, keypair, poolAsset) { return server.submitTransaction( - buildTx( + newTxBuilder( account, keypair, sdk.Operation.changeTrust({ asset: poolAsset, - limit: "100000", }), ), ); } ``` -```python -pool_share_asset = LiquidityPoolAsset(asset_a=asset_a, asset_b=asset_b) - +```java +import org.stellar.sdk.responses.SubmitTransactionResponse; -def establish_pool_trustline(source: Keypair, pool_asset: LiquidityPoolAsset) -> Dict[str, Any]: - tx = ( - new_tx_builder(source.public_key) - .append_change_trust_op(asset=pool_asset, limit="100000") - .build() +public static SubmitTransactionResponse establishPoolTrustline(KeyPair source, LiquidityPoolAsset poolAsset) throws Exception { + Transaction tx = newTxBuilder(source) + .addOperation( + new ChangeTrustOperation.Builder(poolAsset).build() ) - tx.sign(source) - return server.submit_transaction(tx) + .build(); + + tx.sign(source); + return server.submitTransaction(tx); +} +``` + +```go +func establishPoolTrustline(source *keypair.Full, poolAsset txnbuild.LiquidityPoolAsset) { + tx := newTxBuilder(source.Address()) + tx.Operations = []txnbuild.Operation{ + &txnbuild.ChangeTrust{ + Line: poolAsset, + }, + } + tx, err := txnbuild.NewTransaction(*tx) + check(err) + signedTx, err := tx.Sign(network.TestNetworkPassphrase, source) + check(err) + _, err = server.SubmitTransaction(signedTx) + check(err) +} ``` -This lets the participants hold pool shares, which means now they can perform deposits and withdrawals. +This lets participants hold pool shares, which means they can now perform AMM deposits and withdrawals. -#### Participation: Deposits +#### Participant Deposits To work with a liquidity pool, you need to know its ID beforehand. It’s a deterministic value, and only a single liquidity pool can exist for a particular asset pair, so you can calculate it locally from the pool parameters. +```python +poolId = poolAsset.liquidity_pool_id + +def addLiquidity(source, maxReserveA, maxReserveB): + exactPrice = maxReserveA / maxReserveB + minPrice = exactPrice - (exactPrice * Decimal("0.1")) + maxPrice = exactPrice + (exactPrice * Decimal("0.1")) + + transaction = ( + newTxBuilder(source.public_key) + .append_liquidity_pool_deposit_op( + liquidity_pool_id = poolId, + max_amount_a = f"{maxReserveA:.7f}", + max_amount_b = f"{maxReserveB:.7f}", + min_price = f"{minPrice:.7f}", + max_price = f"{maxPrice:.7f}", + ) + .build() + ) + + transaction.sign(source) + return server.submit_transaction(transaction) +``` + ```js const poolId = sdk .getLiquidityPoolId( "constant_product", - poolShareAsset.getLiquidityPoolParameters(), + poolAsset.getLiquidityPoolParameters(), ) .toString("hex"); @@ -354,7 +1299,7 @@ function addLiquidity(source, signer, poolId, maxReserveA, maxReserveB) { const maxPrice = exactPrice + exactPrice * 0.1; return server.submitTransaction( - buildTx( + newTxBuilder( source, signer, sdk.Operation.liquidityPoolDeposit({ @@ -369,46 +1314,269 @@ function addLiquidity(source, signer, poolId, maxReserveA, maxReserveB) { } ``` -```python -pool_id = pool_share_asset.liquidity_pool_id - - -def add_liquidity( - source: Keypair, - pool_id: str, - max_reserve_a: Decimal, - max_reserve_b: Decimal, -) -> dict[str, Any]: - exact_price = max_reserve_a / max_reserve_b - min_price = exact_price - exact_price * Decimal("0.1") - max_price = exact_price + exact_price * Decimal("0.1") - tx = ( - new_tx_builder(source.public_key) - .append_liquidity_pool_deposit_op( - liquidity_pool_id=pool_id, - max_amount_a=f"{max_reserve_a:.7f}", - max_amount_b=f"{max_reserve_b:.7f}", - min_price=min_price, - max_price=max_price, - ) - .build() +```java +private static final String poolId = poolAsset.getLiquidityPoolId(); + +public static SubmitTransactionResponse addLiquidity( + KeyPair source, + String poolId, + String maxReserveA, + String maxReserveB, +) throws Exception { + double exactPrice = Double.parseDouble(maxReserveA) / Double.parseDouble(maxReserveB); + double minPrice = exactPrice - exactPrice * 0.1; + double maxPrice = exactPrice + exactPrice * 0.1; + + Transaction transaction = newTxBuilder(source) + .addOperation( + new LiquidityPoolDepositOperation.Builder( + poolId, + maxReserveA, + maxReserveB, + String.format("%.7f", minPrice), + String.format("%.7f", maxPrice) + ).build() ) - tx.sign(source) - return server.submit_transaction(tx) + .build(); + + transaction.sign(source); + return server.submitTransaction(transaction); +} + +``` + +```go +var poolId, _ = poolShareAsset.LiquidityPoolID() + +func addLiquidity(source *keypair.Full, maxReserveA, maxReserveB float64) { + exactPrice := maxReserveA / maxReserveB + minPrice := exactPrice - exactPrice*0.1 + maxPrice := exactPrice + exactPrice*0.1 + + tx := newTxBuilder(source.Address()) + tx.Operations = []txnbuild.Operation{ + &txnbuild.LiquidityPoolDeposit{ + LiquidityPoolID: poolId, + MaxAmountA: formatFloat(maxReserveA), + MaxAmountB: formatFloat(maxReserveB), + MinPrice: formatFloat(minPrice), + MaxPrice: formatFloat(maxPrice), + }, + } + + tx, err := txnbuild.NewTransaction(*tx) + check(err) + signedTx, err := tx.Sign(network.TestNetworkPassphrase, source) + check(err) + _, err = server.SubmitTransaction(signedTx) + check(err) +} + ``` -When depositing assets into a liquidity pool, you need to define your acceptable price bounds. In the above function, we allow for a +/-10% margin of error from the “spot price”. This margin is by no means a recommendation and is chosen just for demonstration. +If the pool is empty, then this operation deposits `maxAmountA` of A and `maxAmountB` of B into the pool. If the pool is not empty, then this operation deposits **at most** `maxAmountA` of A and `maxAmountB` of B into the pool. The actual amounts deposited are determined using the current reserves of the pool. You can use these parameters to control a percentage of slippage. + +Withdrawing reduces the number of pool shares in exchange for reserves from a liquidity pool. Parameters to this operation depend on the ordering of assets in the liquidity pool (see the liquidity pool glossary entry for information about how to determine the ordering of assets). “A” refers to the first asset in the liquidity pool, and “B” refers to the second asset in the liquidity pool. Withdrawing reduces the number of pool shares in exchange for reserves from a liquidity pool. + +The `minAmountA` and `minAmountB` parameters can be used to control a percentage of slippage from the "spot price" on the pool. + +--- + +When depositing assets into a liquidity pool, you need to define your acceptable price bounds. In the above function, we allow for a ±10% margin of error from the “spot price.” This margin is by no means a recommendation and is chosen just for demonstration. Notice that we also specify the maximum amount of each reserve we’re willing to deposit. This, alongside the minimum and maximum prices, helps define boundaries for the deposit, since there can always be a change in the exchange rate between submitting the operation and it getting accepted by the network. -#### Participation: Withdrawals +#### Calculating Price + +While the network automatically calculates the AMM price product, this does not show up in the order book itself per se. Rather, your order will execute strictly at the best available limit offer or AMM rate. While limit orders specify volume, AMM prices actually vary in real time based on the pool size, which we can find: + +Also let's see "The pool’s state (reserves, fees, shares, parameters) is stored directly on the Stellar ledger." + +[!unavoidable Horizon reference](../../data/apis/horizon/api-reference/list-liquidity-pools.api.mdx) + +``` + case LIQUIDITY_POOL_CONSTANT_PRODUCT: + struct + { + LiquidityPoolConstantProductParameters params; + + int64 reserveA; // amount of A in the pool + int64 reserveB; // amount of B in the pool + int64 totalPoolShares; // total number of pool shares issued + int64 poolSharesTrustLineCount; // number of trust lines for the + // associated pool shares + } constantProduct; + } + body; +}; +``` + + + +```python +# This proposal only introduces a constant product liquidity pool. +# The invariant for such a liquidity pool is (X + x - Fx) (Y - y) >= XY +# +# X and Y are the initial reserves of the liquidity pool +# F is the fee charged by the liquidity pool +# x is the amount received by the liquidity pool +# y is the amount disbursed by the liquidity pool + +import requests + +def fetch_amm_pool_data(asset_1, asset_2): + url = f"https://horizon.stellar.org/liquidity_pools?reserves={asset_1},{asset_2}" + response = requests.get(url) + if response.status_code != 200: + raise Exception("Error fetching data from Horizon API") + + data = response.json() + + # Assuming we want the first AMM pool found + pool_data = data['_embedded']['records'][0] + + # Extract reserves + reserve_xlm = float(pool_data['reserves'][0]['amount']) # XLM reserves + reserve_usd = float(pool_data['reserves'][1]['amount']) # USD reserves + + return reserve_xlm, reserve_usd + +def amm_price_xlm_usd(reserve_xlm, reserve_usd, trade_usd, xlm_price_usd): + """ + TODO remove this and make CamelCase + Calculate the price impact of a trade worth $100 of XLM using an AMM's constant product formula. + + :param reserve_xlm: Reserve of XLM in the pool. + :param reserve_usd: Reserve of the other token (e.g., USD stablecoin) in the pool. + :param trade_usd: The amount of USD equivalent to be traded. + :param xlm_price_usd: Current price of XLM in USD (e.g., 0.12 for $0.12/XLM). + + :return: The price for the $100 trade in XLM. + """ + # Convert the trade amount in USD to XLM based on the current market price + dx = trade_usd / xlm_price_usd # Amount of XLM to trade + + # Constant product invariant (x * y = k) + k = reserve_xlm * reserve_usd + + # New XLM reserve after the trade + new_reserve_xlm = reserve_xlm + dx + + # Calculate the new reserve of USD after the trade + new_reserve_usd = k / new_reserve_xlm + + # Amount of USD received (dy) + dy = reserve_usd - new_reserve_usd + + # Price of the trade in terms of USD received per XLM traded + price = dy / dx + + return price + +# Example: Fetch reserves and calculate price impact for $100 worth of XLM +# ALSO remoOVE@!@ todo +if __name__ == "__main__": + asset_1 = "XLM" # Asset 1 (XLM) + asset_2 = "USD" # Asset 2 (USD stablecoin) + + + reserve_xlm, reserve_usd = fetch_amm_pool_data(asset_1, asset_2) + + # Define trade and market parameters + trade_usd = 100 # Amount of USD equivalent to trade + xlm_price_usd = 0.12 # Current price of XLM in USD + + # Calculate price impact for $100 worth of XLM + price_impact = amm_price_xlm_usd(reserve_xlm, reserve_usd, trade_usd, xlm_price_usd) + + print(f"Reserves: {reserve_xlm} XLM, {reserve_usd} USD") + print(f"Price for trading $100 worth of XLM is: {price_impact} USD per XLM") + +def getSpotPrice(): + resp = server.liquidity_pools().liquidity_pool(poolId).call() + amountA = resp["reserves"][0]["amount"] + amountB = resp["reserves"][1]["amount"] + spotPrice = Decimal(amountA) / Decimal(amountB) + print(f"Price: {amountA}/{amountB} = {spotPrice:.7f}") # Max network precision +``` + +```js +const BigNumber = require("bignumber.js"); + +async function getSpotPrice() { + const pool = await server.liquidityPools().liquidityPoolId(poolId).call(); + const [a, b] = pool.reserves.map((r) => r.amount); + const spotPrice = new BigNumber(a).div(b); + console.log(`Price: ${a}/${b} = ${spotPrice.toFormat(7)}`); +} +``` + +```java +public static void getSpotPrice(String[] args) throws Exception { + LiquidityPoolResponse poolInfo = server.liquidityPools().liquidityPool(poolId).execute(); + double reserveA = Double.parseDouble(poolInfo.getReserves().get(0).getAmount()); + double reserveB = Double.parseDouble(poolInfo.getReserves().get(1).getAmount()); + double spotPrice = reserveA / reserveB; + System.out.printf("Price: %.7f/%7f = %.7f%n", reserveA, reserveB, spotPrice); +} +``` + +```go +func getSpotPrice(server *horizonclient.Client, poolID string) { + poolRequest := horizonclient.LiquidityPoolRequest{LiquidityPoolID: poolID} + pool, err := server.LiquidityPoolDetail(poolRequest) + check(err) + + reserveA, err := strconv.ParseFloat(pool.Reserves[0].Amount, 64) + check(err) + reserveB, err := strconv.ParseFloat(pool.Reserves[1].Amount, 64) + check(err) + + spotPrice := reserveA / reserveB + fmt.Printf("Price: %.7f/%.7f = %.7f\n", reserveA, reserveB, spotPrice) +} +``` + + + +#### Participant Withdrawals If you own shares of a particular pool, you can withdraw reserves from it. The operation structure mirrors the deposit closely: +```python +def removeLiquidity(source, poolId, sharesAmount): + poolInfo = server.liquidity_pools().liquidity_pool(poolId).call() + totalShares = Decimal(poolInfo["total_shares"]) + minReserveA = ( + sharesAmount + / totalShares + * Decimal(poolInfo["reserves"][0]["amount"]) + * Decimal("0.95") # 95% safety factor + ) + minReserveB = ( + sharesAmount + / totalShares + * Decimal(poolInfo["reserves"][1]["amount"]) + * Decimal("0.95") + ) + tx = ( + newTxBuilder(source.public_key) + .appendLiquidityPoolWithdrawOp( + liquidityPoolId=poolId, + amount=f"{sharesAmount:.7f}", + minAmountA=f"{minReserveA:.7f}", + minAmountB=f"{minReserveB:.7f}", + ) + .build() + ) + tx.sign(source) + return server.submit_transaction(tx) +``` + ```js function removeLiquidity(source, signer, poolId, sharesAmount) { return server @@ -418,12 +1586,12 @@ function removeLiquidity(source, signer, poolId, sharesAmount) { .then((poolInfo) => { let totalShares = poolInfo.total_shares; let minReserveA = - (sharesAmount / totalShares) * poolInfo.reserves[0].amount * 0.95; + (sharesAmount / totalShares) * poolInfo.reserves[0].amount * 0.95; // 95% safety factor let minReserveB = (sharesAmount / totalShares) * poolInfo.reserves[1].amount * 0.95; return server.submitTransaction( - buildTx( + newTxBuilder( source, signer, sdk.Operation.liquidityPoolWithdraw({ @@ -438,36 +1606,68 @@ function removeLiquidity(source, signer, poolId, sharesAmount) { } ``` -```python -def remove_liquidity( - source: Keypair, pool_id: str, shares_amount: Decimal -) -> dict[str, Any]: - pool_info = server.liquidity_pools().liquidity_pool(pool_id).call() - total_shares = Decimal(pool_info["total_shares"]) - min_reserve_a = ( - shares_amount - / total_shares - * Decimal(pool_info["reserves"][0]["amount"]) - * Decimal("0.95") - ) # - min_reserve_b = ( - shares_amount - / total_shares - * Decimal(pool_info["reserves"][1]["amount"]) - * Decimal("0.95") - ) - tx = ( - new_tx_builder(source.public_key) - .append_liquidity_pool_withdraw_op( - liquidity_pool_id=pool_id, - amount=f"{shares_amount:.7f}", - min_amount_a=f"{min_reserve_a:.7f}", - min_amount_b=f"{min_reserve_b:.7f}", - ) - .build() +```java +public Map removeLiquidity(KeyPair source, String poolId, String sharesAmount) throws Exception { + Map poolInfo = server.liquidityPools() + .liquidityPool(poolId) + .call(); + + double totalShares = Double.parseDouble(poolInfo.get("total_shares").toString()); + double reserveA = Double.parseDouble(((Map) ((List) poolInfo.get("reserves")).get(0)).get("amount")); + double reserveB = Double.parseDouble(((Map) ((List) poolInfo.get("reserves")).get(1)).get("amount")); + + double minReserveA = (Double.parseDouble(sharesAmount) / totalShares) * reserveA * 0.95; // 95% safety factor + double minReserveB = (Double.parseDouble(sharesAmount) / totalShares) * reserveB * 0.95; + + Transaction transaction = newTxBuilder(source) + .addOperation( + new LiquidityPoolWithdrawOperation.Builder( + poolId, + sharesAmount, + String.format("%.7f", minReserveA), + String.format("%.7f", minReserveB) + ).build() ) - tx.sign(source) - return server.submit_transaction(tx) + .build(); + + transaction.sign(source); + return server.submitTransaction(transaction); +} +``` + +```go +func removeLiquidity(source *keypair.Full, poolID string, sharesAmount float64) { + poolRequest := horizonclient.LiquidityPoolRequest{LiquidityPoolID: poolID} + poolInfo, err := client.LiquidityPoolDetail(poolRequest) + check(err) + + totalShares, err := strconv.ParseFloat(poolInfo.TotalShares, 64) + check(err) + reserveA, err := strconv.ParseFloat(poolInfo.Reserves[0].Amount, 64) + check(err) + reserveB, err := strconv.ParseFloat(poolInfo.Reserves[1].Amount, 64) + check(err) + + minReserveA := (sharesAmount / totalShares) * reserveA * 0.95 // 95% safety factor + minReserveB := (sharesAmount / totalShares) * reserveB * 0.95 + + tx := newTxBuilder(source.Address()) + tx.Operations = []txnbuild.Operation{ + &txnbuild.LiquidityPoolWithdraw{ + LiquidityPoolID: poolID, + Amount: formatFloat(sharesAmount), + MinAmountA: formatFloat(minReserveA), + MinAmountB: formatFloat(minReserveB), + }, + } + + tx, err := tx.Build() + check(err) + signedTx, err := tx.Sign(txnbuild.NetworkTest, source) + check(err) + resp, err := client.SubmitTransaction(signedTx) + check(err) +} ``` @@ -476,148 +1676,228 @@ Notice here that we specify the minimum amount. Much like with a strict-receive #### Putting it all together -Finally, we can combine these pieces together to simulate some participation in a liquidity pool. We’ll have everyone deposit increasing amounts into the pool, then one participant withdraws their shares. Between each step, we’ll retrieve the spot price. +Finally, we can combine these pieces together to simulate some participation in a liquidity pool. We’ll have everyone deposit increasing amounts into the pool, then one participant withdraws their shares. Between each step, we’ll retrieve the spot price like earlier to see our effects. +```python +# Step 1: kp1 adds liquidity +establishPoolTrustline(keypairs[1], poolAsset) +addLiquidity( + keypairs[1], + poolId, + Decimal(1000), + Decimal(3000) +) # Divides into 1:3 ratio +getSpotPrice() + +# Step 2: kp2 adds liquidity +establishPoolTrustline(keypairs[2], poolAsset) +addLiquidity( + keypairs[2], + poolId, + Decimal(2000), + Decimal(6000) +) # Larger deposit this time +getSpotPrice() + +# Step 3: kp1 removes all liquidity +accountDetails = server.accounts().account_id(keypairs[1].public_key).call() +for bals in accountDetails["balances"]: + if ( + bals["asset_type"] == "liquidity_pool_shares" and + bals["liquidity_pool_id"] == poolId + ): + balance = Decimal(bals["balance"]) + break +if not balance: + raise Exception("No liquidity pool shares found for kp1") +removeLiquidity(keypairs[1], poolId, balance) +getSpotPrice() +``` + ```js -function main() { - return getAccounts() - .then((accounts) => { - return Promise.all( - kps.map((kp, i) => { - const acc = accounts[i]; - const depositA = ((i + 1) * 1000).toString(); - const depositB = ((i + 1) * 3000).toString(); // maintain a 1:3 ratio - - return establishPoolTrustline(acc, kp, poolShareAsset) - .then(() => addLiquidity(acc, kp, poolId, depositA, depositB)) - .then(() => getSpotPrice()); - }), - ).then(() => accounts); - }) - .then((accounts) => { - // kp1 takes all his/her shares out - return server - .accounts() - .accountId(kps[1].publicKey()) - .call() - .then(({ balances }) => { - let balance = 0; - balances.every((bal) => { - if ( - bal.asset_type === "liquidity_pool_shares" && - bal.liquidity_pool_id === poolId - ) { - balance = bal.balance; - return false; - } - return true; - }); - return balance; - }) - .then((balance) => - removeLiquidity(accounts[1], kps[1], poolId, balance), - ); - }) - .then(() => getSpotPrice()); -} +async function main() { + const accounts = await Promise.all( + keypairs.map((kp) => server.loadAccount(kp.publicKey())), + ); -function getSpotPrice() { - return server - .liquidityPools() - .liquidityPoolId(poolId) - .call() - .then((pool) => { - const [a, b] = pool.reserves.map((r) => r.amount); - const spotPrice = new BigNumber(a).div(b); - console.log(`Price: ${a}/${b} = ${spotPrice.toFormat(2)}`); - }); + for (let i = 1; i < keypairs.length; i++) { + const acc = accounts[i]; + const kp = keypairs[i]; + baseAmount = i + 1; // Arbitrary deposit increasing amounts + const depositA = (baseAmount * 1000).toString(); + const depositB = (baseAmount * 3000).toString(); // Scalars maintain a 1:3 ratio + + await establishPoolTrustline(acc, kp, poolAsset); + await addLiquidity(acc, kp, poolId, depositA, depositB); + await getSpotPrice(); + } + + // kp1 removes all liquidity + const { balances } = await server + .accounts() + .accountId(keypairs[1].publicKey()) + .call(); + + const balance = balances.find( + (bal) => + bal.asset_type === "liquidity_pool_shares" && + bal.liquidity_pool_id === poolId, + )?.balance; + if (!balance) throw new Error("No liquidity pool shares found for kp1."); + + await removeLiquidity(accounts[1], keypairs[1], poolId, balance); + await getSpotPrice(); } preamble().then(main); ``` -```python -def main(): - deposit_a = Decimal(1000) - deposit_b = Decimal(3000) # maintain a 1:3 ratio - establish_pool_trustline(kps[1], pool_share_asset) - add_liquidity(kps[1], pool_id, deposit_a, deposit_b) - get_spot_price() - - deposit_a = Decimal(2000) - deposit_b = Decimal(6000) # maintain a 1:3 ratio - establish_pool_trustline(kps[2], pool_share_asset) - add_liquidity(kps[2], pool_id, deposit_a, deposit_b) - get_spot_price() - - # kp1 takes all his/her shares out - balance = 0 - for b in server.accounts().account_id(kps[1].public_key).call()["balances"]: - if ( - b["asset_type"] == "liquidity_pool_shares" - and b["liquidity_pool_id"] == pool_id - ): - balance = Decimal(b["balance"]) - break - if not balance: - raise - remove_liquidity(kps[1], pool_id, balance) - get_spot_price() - -def get_spot_price(): - resp = server.liquidity_pools().liquidity_pool(pool_id).call() - amount_a = resp["reserves"][0]["amount"] - amount_b = resp["reserves"][1]["amount"] - spot_price = Decimal(amount_a) / Decimal(amount_b) - print(f"Price: {amount_a}/{amount_b} = {spot_price:.7f}") - -if __name__ == '__main__': - preamble() - main() +```java +public static void main(String[] args) throws Exception { + List accounts = new ArrayList<>(); + for (KeyPair kp : keypairs) { + accounts.add(server.accounts().account(kp.getAccountId())); + } + + for (int i = 1; i < keypairs.size(); i++) { + establishPoolTrustline(keypairs.get(i), poolAsset); + double depositA = (i + 1) * 1000; // Incremental deposit scalar + double depositB = (i + 1) * 3000; // And maintain 1:3 ratio + addLiquidity(keypairs.get(i), poolId, depositA, depositB); + getSpotPrice(); + } + + // kp1 removes all liquidity + KeyPair kp1 = keypairs.get(1); + double balance = kp1.getBalances().stream() + .filter( + balance -> + "liquidity_pool_shares".equals(balance.getAssetType()) && + poolId.equals(balance.getLiquidityPoolId()) + ) + .mapToDouble(balance -> Double.parseDouble(balance.getBalance())) + .findFirst(); + if (!balance.isPresent()) { + throw new Exception("No liquidity pool shares found for kp1"); + } + removeLiquidity(kp1, poolId, balance); + getSpotPrice(); +} +``` + +```go +func main() { + keypairs := make([]*keypair.Full, len(secrets)) + for i, secret := range secrets { + keypairs[i] = keypair.MustParseFull(secret) + } + + // Simulate deposits from participants + for i := 1; i < len(keypairs); i++ { + kp := keypairs[i] + // Arbitrary deposit increasing amounts + depositA := float64(i+1) * 1000 + depositB := float64(i+1) * 3000 // Scalar maintains 1:3 ratio + + establishPoolTrustline(kp, poolID) + addLiquidity(kp, poolID, depositA, depositB) + getSpotPrice(poolID) + } + + // kp1 removes all liquidity + kp1 := keypairs[1] + account, err := server.AccountDetail( + horizonclient.AccountRequest{AccountID: kp1.Address()} + ) + check(err) + + var balance float64 + for _, bal := range account.Balances { + if bal.AssetType == "liquidity_pool_shares" && + bal.LiquidityPoolID == poolID { + balance, err = strconv.ParseFloat(bal.Balance, 64) + check(err) + break + } + } + if balance == 0 { + log.Fatalf("No liquidity pool shares found for kp1.") + } + removeLiquidity(kp1, poolID, balance) + getSpotPrice(poolID) +} ``` +TODO spell out the changes as the example changes spot price -#### Watching Liquidity Pool Activity +- Passive sell offers (with examples) -You can access the transactions, operations, and effects related to a liquidity pool if you want to track its activity. Let’s see how we can track the latest deposits in a pool (suppose `poolId` is defined as before): +## Trade Operating Principles - +At no point in these examples did assets leave your custody. And, since no other entities control (intermediate) funds, conversions happen with equal preference as offer operations enter [transaction sets](./stellar-consensus-protocol.mdx#nomination-protocol) via [consensus reputation](./stellar-consensus-protocol.mdx#quorum-set). This new breakthrough introduces a few worthwhile design implications. -```js -server - .operations() - .forLiquidityPool(poolId) - .call() - .then((ops) => { - ops.records - .filter((op) => op.type == "liquidity_pool_deposit") - .forEach((op) => { - console.log("Reserves deposited:"); - op.reserves_deposited.forEach((r) => - console.log(` ${r.amount} of ${r.asset}`), - ); - console.log(" for pool shares: ", op.shares_received); - }); - }); -``` +### Regulated compliance -```python -def watch_liquidity_pool_activity(): - for op in ( - server.operations() - .for_liquidity_pool(liquidity_pool_id=pool_id) - .cursor("now") - .stream() - ): - if op["type"] == "liquidity_pool_deposit": - print("Reserves deposited:") - for r in op["reserves_deposited"]: - print(f" {r['amount']} of {r['asset']}") - print(f" for pool shares: {op['shares_received']}") - # ... -``` +Issuers of [regulated assets](../../tokens/control-asset-access.mdx#controlling-access-to-an-asset-with-flags) may need to manage or otherwise oversee trading of their assets. Stellar's [trustline](./stellar-data-structures/accounts.mdx#trustlines) management tools let asset issuers seamlessly handle various trade authorization scenarios. The trading behavior of an account holding regulated asset $\mathcal{A}$ depends on the status of its trustlines: - +| Trustline for $\mathcal{A}$ | Response | +| --- | --- | +| Fully authorized | No restrictions on transactions | +| Authorized to maintain liabilities | Offers to trade $\mathcal{A}$ remain outstanding (and can be cancelled), but no new offers can be created | +| Not authorized or doesn’t exist | New offer operation fails. Existing offers are cancelled at the time of deauthrization from the issuer (mainginting an existing frozen accoiunt balance) | + +In addition to controlling asset use in the orderbook, ssuers can slo enforce authorization and compliance controls on their assets deposited into AMMs (below with details on initial trustline configuration). These optional [trustline flags](./transactions/list-of-operations.mdx#set-trustline-flags) configured before holding an asset ensure smooth compliance by revoking authorization to prevent an account under inestigation from further (automated) trading. The behavior of an $\mathcal{A}$–$\mathcal{B}$ AMM trustline depends on a few authorization possibilities: + +| Permissions | Response | +| --- | --- | +| Trustlines for $\mathcal{A}$ and $\mathcal{B}$ are fully authorized | No restrictions on deposit and withdrawal | +| Trustline for $\mathcal{A}$ is fully authorized but trustline for $\mathcal{B}$ is authorized to maintain liabilities | Trustlines for $\mathcal{A}$ and $\mathcal{B}$ are authorized to maintain liabilities | +| Trustline for $\mathcal{B}$ is fully authorized but trustline for $\mathcal{A}$ is authorized to maintain liabilities | Trustlines for $\mathcal{A}$ and $\mathcal{B}$ are authorized to maintain liabilities | +| Trustlines for $\mathcal{A}$ and $\mathcal{B}$ are authorized to maintain liabilities | Trustlines for $\mathcal{A}$ and $\mathcal{B}$ are authorized to maintain liabilities | +| Trustline for $\mathcal{A}$ is not authorized or doesn’t exist | Pool share trustline cannot exist | +| Trustline for $\mathcal{B}$ is not authorized or doesn’t exist | Pool share trustline cannot exist | + +An AMM trustline which is authorization to maintain liabilities will only allow an account to withdraw existing deposits (with earned pool fees). The account's trustlines for the AMM's assets determine the authorization of an AMM trustline. The resulting pool-share trustline cannot be authorized or de-authorized independently partly because there exists no "issuer" of the AMM. + +This design is necessary because an AMM may contain assets from two different issuers, and both issuers should have a say in whether the pool-share trustline is authorized. The first person to deposit into an AMM with two assets creates a unique identifier [detailed below](#pool-id TODO), based only on the hash of constituent assets. That enjoining process also needs to conform with trust permissions. + +If the issuer of $\mathcal{A}$ or $\mathcal{B}$ fully revokes authorization after deposit, then the account will automatically withdraw from every liquidity pool containing that asset (and those pool-share trustlines will be deleted). We say that these AMM shares have been "redeemed." This action by the issuer also cancels any outstanding limit orders, as described in the first table. + +For example, consider an issuer of $\mathcal{A}$ revokes authorization for an account participating in the $\mathcal{A}$–$\mathcal{B}$, $\mathcal{A}$–$\mathcal{C}$, and $\mathcal{B}$–$\mathcal{C}$ AMMs. The account will redeem from $\mathcal{A}$–$\mathcal{B}$ and $\mathcal{A}$–$\mathcal{C}$, but it will not redeem from $\mathcal{B}$–$\mathcal{C}$. Thus issuers only have authorization control of their assets. + +The ledger creates a [claimable balance](../../build/guides/transactions/claimable-balances.mdx) for each AMM asset in all redeemed pool-share trustlines, so long as there is a balance being withdrawn and the redeemer is not the issuer of that asset. In the latter case, assets are simply burned through return to the issuer. The unconditional claimant of the claimable balance is the owner of the deleted pool-share trustline, but this account may not claim the regulated asset until duly authorized by the issuer. + +The sponsor of the claimable balance is the sponsor of the deleted pool-share trustline. This balances out since the pool-share trustline requires two base reserves for both AMM assets. The `BalanceID` of each returned claimable balance is the SHA-256 hash of the `revokeID`. + +### Minting Tokens + +The issuer of an asset has a special relationship with the SDEX because they create new tokens by decree. Consider an [issuing account](../../tokens/how-to-issue-an-asset.mdx) which wants to create Banana tokens. If this asset has never been issued before, the account can create a new "NANA" sell offer with the base asset token issuer as itself. + +If NANAs already exist in the market, then this trade will generate more NANAs as other accounts purchase the tokens, in exchange for the counter-asset specified by the offer. Thus, the issuer strictly increases the circulating supply by minting new assets, keeping the payment received from a sale to the market. Issuers can do this at any time without a [locked account](../../tokens/how-to-issue-an-asset.mdx#configure-maximum-supply), and their buy offers will similarly burn tokens they originate. + +Additionally, issuers can mint or burn their own tokens through AMMs. Since they cannot hold their own asset, issuing accounts can deposit into AMMs with some or none of their own external assets. If there is a NANA and "apple" AMM with the issuer creating both tokens, then they can deposit as much as they want into the pool to mint the coins. + +Upon withdrawal in this example, the tokens get returned to the issuing account and burned like before. Alternatively, the issuer might consider depositing into the NANA and AstroDollar AMM. Here, the issuer needs only to have an amount of AstroDollar, and they can issue as many NANAs as needed to achieve the conversion ratio for a new AMM deposit, as [detailed below](#amm-participation). + +When withdrawing from the AMM, the issuer will burn returned NANAs while keeping all earned (or lost) AstroDollars. Some projects use this feature of AMMs to conduct a project launch with their own seed capital as the initial half of deposited funds. Contrarily, traditional markets prefer selling new issuances through limit offers which can represent fiat currencies, commodities, or any other assets in exchange for a user's valued token. + +### Burning \\ + +### \\ Price and Operations + +Each order is quoted with an associated price and is represented as a ratio of the two assets in the order, one being the “quote asset” and the other being the “base asset.” This is to ensure there is no loss of precision when representing the price of the order (as opposed to storing the fraction as a floating-point number). + +Prices are specified as a `{numerator, denominator}` pair with both components of the fraction represented as 32-bit signed integers. The numerator is considered the base asset (like bananas), and the denominator is considered the quote asset (like dollars). When expressing a price of "Asset $\mathcal{A}$ in terms of Asset $\mathcal{B}$," the amount of $\mathcal{B}$ is the denominator (and therefore the quote asset), and $\mathcal{A}$ is the numerator (and therefore the base asset). + +:::danger TODO + +> Break out to burn + +Numerator and denominator are stored as signed 32-bit integers, but since one bit is for the sign, only 31 bits are available for the value. + +::: + +The order price you set is independent of the fee you pay for submitting that order in a transaction. Fees are always paid in lumens, and you specify them as a separate parameter when submitting the order to the network. To learn more about transaction fees, see our [section on Fees](./fees-resource-limits-metering.mdx). diff --git a/docs/learn/fundamentals/lumens.mdx b/docs/learn/fundamentals/lumens.mdx index 669ba49b08..25ee4b3141 100644 --- a/docs/learn/fundamentals/lumens.mdx +++ b/docs/learn/fundamentals/lumens.mdx @@ -7,7 +7,7 @@ sidebar_position: 30 # Lumens (XLM) -Lumens (XLM) are the native currency of the Stellar network. The lumen is the only token that doesn’t require an issuer or trustline. They are used to pay all transaction [fees](#transaction-fees), fund [rent](./fees-resource-limits-metering.mdx#resource-fee), and to cover [minimum balance requirements](stellar-data-structures/accounts.mdx#base-reserves-and-subentries) on the network. +Lumens are the native currency of the Stellar network. The lumen is the only token that doesn’t require an issuer or trustline. They are used to pay all transaction [fees](#transaction-fees), fund [rent](./fees-resource-limits-metering.mdx#resource-fee), and to cover [minimum balance requirements](stellar-data-structures/accounts.mdx#base-reserves-and-subentries) on the network. To read up on the basics of lumens, head over to our Stellar Learn site: [Stellar Learn: Lumens](https://www.stellar.org/lumens) @@ -47,25 +47,25 @@ This section explains how lumen supply metrics are calculated and made available Unlike many other blockchains, the native network currency is not created through mining- all XLM that has ever existed and will ever exist was created when the Stellar network went live. -[SDF’s Dashboard API endpoint](https://dashboard.stellar.org/api/v2/lumens) will always have the live totals for the essential numbers around lumens. This guide explains important supply metrics like Original Supply, Total Supply, and Circulating Supply entailed in that data. +[The SDF's Dashboard API endpoint](https://dashboard.stellar.org/api/v3/lumens) will always have the live totals for the essential numbers around lumens. This guide explains important supply metrics like Original Supply, Total Supply, and Circulating Supply entailed in that data. ### Dashboard API -As of May 28th, 2024, the Dashboard API shows: +As of 31 Jan 2025, the Dashboard API shows: ```json { - "updatedAt": "2024-05-28T16:11:14.622Z", + "updatedAt": "2025-01-31T16:20:58.531Z", "originalSupply": "100000000000", "inflationLumens": "5443902087.3472865", - "burnedLumens": "55442115112.9537534", - "totalSupply": "50001786974.3935331", - "upgradeReserve": "259580243.9842749", - "feePool": "4690537.8610771", - "sdfMandate": "20761482987.7713113", - "circulatingSupply": "28976033204.7768698", + "burnedLumens": "55442115192.8967467", + "totalSupply": "50001786894.4505398", + "upgradeReserve": "259580244.9861456", + "feePool": "5197814.2836447", + "sdfMandate": "19192857999.7786541", + "circulatingSupply": "30544150835.4020954", "_details": "https://www.stellar.org/developers/guides/lumen-supply-metrics.html" } ``` @@ -74,23 +74,23 @@ As of May 28th, 2024, the Dashboard API shows: ### Definitions -**originalSupply** One hundred billion lumens [were created](https://stellar.expert/explorer/public/ledger/2) when the Stellar network went live. That’s the Original Supply for the network. +**`originalSupply` -** One hundred billion lumens [were created](https://stellar.expert/explorer/public/ledger/2) when the Stellar network went live. That’s the Original Supply for the network. -**inflationLumens** For the first five or so years of Stellar’s existence, the supply of lumens increased by 1% annually. This “network inflation” was ended by validator vote on October 28, 2019. The total number of lumens generated by inflation was 5,443,902,087.3472865. +**`inflationLumens` -** For the first five or so years of Stellar’s existence, the supply of lumens increased by 1% annually. This “network inflation” was ended by validator vote on October 28, 2019. Inflation generated 5,443,902,087.3472865 lumens during this initial period. Adding this number to the Original Supply, you get the total lumens that have ever existed: 105,443,902,087.3472865. This number is visible on the [List All Ledgers](../../data/apis/horizon/api-reference/list-all-ledgers.api.mdx) Horizon API endpoint as `_embedded.records.total_coins`. See all Stellar Mainnet Horizon data providers [here](../../data/apis/horizon/providers.mdx). -**burnedLumens** These are all the lumens sent to accounts with no signers, meaning the funds are inaccessible and have been removed forever from Stellar’s lumen supply. +**`burnedLumens` -** These are all the lumens sent to accounts with no signers, meaning the funds are inaccessible and have been removed forever from Stellar’s lumen supply. -While any address with no signers is counted here, the vast majority of the lumens in this sum are in a single locked address. On November 4, 2019, SDF [reduced](https://www.stellar.org/blog/sdfs-next-steps/) its lumen holdings to better reflect its mission and the growth of the Stellar ecosystem. To do so, the Foundation sent 55,442,095,285.7418 lumens to [GALA…LUTO](https://stellar.expert/explorer/public/account/GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO). +While any address with no signers is counted here, the vast majority of the lumens in this sum are in a single locked address. On November 4, 2019, the SDF [reduced](https://www.stellar.org/blog/sdfs-next-steps/) its lumen holdings to better reflect its mission and the growth of the Stellar ecosystem. To do so, the Foundation sent 55,442,095,285.7418 lumens to [`GALAXYVOID`](https://stellar.expert/explorer/public/account/GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO). -**totalSupply** The Total Supply is the number of lumens now in existence: 50,001,803,905.97172. The Total Supply includes four major categories of lumens, which the API treats in detail. +**`totalSupply` -** The Total Supply is the number of lumens now in existence: 50,001,803,905.97172. The Total Supply includes four major categories of lumens, which the API treats in detail. -**upgradeReserve** The Upgrade Reserve is a special address that’s neither circulating nor a part of SDF’s mandate. When Stellar [changed its consensus algorithm](https://www.stellar.org/blog/upgraded-network-is-here/) in 2015 and relaunched the network these lumens were set aside, to be claimed, one-for-one, by holders of the old network tokens. The [Upgrade Reserve account](https://stellar.expert/explorer/public/account/GBEZOC5U4TVH7ZY5N3FLYHTCZSI6VFGTULG7PBITLF5ZEBPJXFT46YZM) is essentially an escrow, and we don’t expect many claimants to come and pull those lumens into the circulating supply at this point. +**`upgradeReserve` -** The Upgrade Reserve is a special address that’s neither circulating nor a part of the SDF's mandate. When Stellar [changed its consensus algorithm](https://www.stellar.org/blog/upgraded-network-is-here/) in 2015 and relaunched the network these lumens were set aside, to be claimed, one-for-one, by holders of the old network tokens. The [Upgrade Reserve account](https://stellar.expert/explorer/public/account/GBEZOC5U4TVH7ZY5N3FLYHTCZSI6VFGTULG7PBITLF5ZEBPJXFT46YZM) is essentially an escrow, and we don’t expect many claimants to come and pull those lumens into the circulating supply at this point. -**feePool** The Fee Pool is where network fees collect. The lumens do not belong to any particular account. No one has access to the fee pool, so these lumens are non-circulating. Network validators could theoretically vote for a protocol change that would affect the fee pool, so we include it in the total supply. Stellar’s transaction fees are extremely low so the fee pool grows very slowly. The Fee Pool is tracked by the protocol itself, and the current number is visible on the [List All Ledgers](../../data/apis/horizon/api-reference/list-all-ledgers.api.mdx) Horizon API endpoint as `_embedded.records.fee_pool`. See all Stellar Mainnet Horizon data providers [here](../../data/apis/horizon/providers.mdx). +**`feePool` -** The Fee Pool is where network fees collect. The lumens do not belong to any particular account. No one has access to the fee pool, so these lumens are non-circulating. Network validators could theoretically vote for a protocol change that would affect the fee pool, so we include it in the total supply. Stellar’s transaction fees are extremely low so the fee pool grows very slowly. The Fee Pool is tracked by the protocol itself, and the current number is visible on the [List All Ledgers](../../data/apis/horizon/api-reference/list-all-ledgers.api.mdx) Horizon API endpoint as `_embedded.records.fee_pool`. See all Stellar Mainnet Horizon data providers [here](../../data/apis/horizon/providers.mdx). -**sdfMandate** The SDF Mandate is described in detail [here](https://www.stellar.org/foundation/mandate). The Foundation was funded by lumens generated at Stellar’s inception; all of those lumens will eventually be spent or distributed to enhance and promote Stellar. Here is a complete list of the addresses currently associated with the SDF Mandate: +**`sdfMandate` -** The SDF Mandate is described in detail [here](https://www.stellar.org/foundation/mandate). The Foundation was funded by lumens generated at Stellar’s inception; all of those lumens will eventually be spent or distributed to enhance and promote Stellar. Here is a complete list of the addresses currently associated with the SDF Mandate: - [Direct Development, Available Funds](https://stellar.expert/explorer/public/account/GB6NVEN5HSUBKMYCE5ZOWSK5K23TBWRUQLZY3KNMXUZ3AQ2ESC4MY4AQ) - [Jan 1 2021 Escrow](https://stellar.expert/explorer/public/account/GBA6XT7YBQOERXT656T74LYUVJ6MEIOC5EUETGAQNHQHEPUFPKCW5GYM) @@ -108,4 +108,6 @@ While any address with no signers is counted here, the vast majority of the lume - [In-App Distribution](https://stellar.expert/explorer/public/account/GDKIJJIKXLOM2NRMPNQZUUYK24ZPVFC6426GZAEP3KUK6KEJLACCWNMX) - [In-App Distribution (Hot)](https://stellar.expert/explorer/public/account/GAX3BRBNB5WTJ2GNEFFH7A4CZKT2FORYABDDBZR5FIIT3P7FLS2EFOZZ) -**circulatingSupply** The Circulating Supply is lumens in the hands of individuals and independent companies. These are lumens out in the world, used to pay network fees and fund Stellar accounts. They are also used as a general medium of exchange. We expect Stellar’s Circulating Supply to grow steadily as SDF spends and distributes lumens according to its mandate. Lumens in the Total Supply, but not in the SDF Mandate, Upgrade Reserve, or Fee Pool are assumed to be circulating. +#### `circulatingSupply` + +The Circulating Supply is lumens in the hands of individuals and independent companies. These are lumens out in the world, used to pay network fees and fund Stellar accounts. They are also used as a general medium of exchange. We expect Stellar’s Circulating Supply to grow steadily as the SDF spends and distributes lumens according to its mandate. Lumens in the Total Supply, but not in the SDF Mandate, Upgrade Reserve, or Fee Pool are assumed to be circulating. diff --git a/docs/learn/fundamentals/stellar-consensus-protocol.mdx b/docs/learn/fundamentals/stellar-consensus-protocol.mdx index 36952b1c9a..a5b96bc3d5 100644 --- a/docs/learn/fundamentals/stellar-consensus-protocol.mdx +++ b/docs/learn/fundamentals/stellar-consensus-protocol.mdx @@ -1,11 +1,13 @@ --- -title: "Overview of the Stellar Consensus Protocol (SCP) and Transaction Validation" +title: "Overview of SCP and Transaction Validation" sidebar_label: Stellar Consensus Protocol description: "A brief overview of how the Stellar Consensus Protocol (SCP), a proof of agreement protocol, enables consensus and validates transactions on the network." sidebar_position: 40 --- -# Stellar Consensus Protocol +import YouTube from "@site/src/components/YouTube"; + + Consensus is hugely important in a decentralized payment system. It distributes the monitoring and approval of transactions across many individual nodes (computers) instead of relying on one closed, central system. Nodes are run by organizations or individuals, and the goal is for all nodes to update the ledger in the same way, ensuring each ledger reaches the same state. Consensus is vital for the security of the blockchain, allowing nodes to agree on something safely and preventing double-spend attacks. @@ -23,17 +25,17 @@ There are three desired properties of consensus mechanisms: fault tolerance, saf Consensus mechanisms can typically only prioritize two out of three of these properties. SCP prioritizes fault tolerance and safety over liveness. Because of prioritizing safety, blocks can sometimes get stuck while waiting for nodes to agree. -## SCP components +## SCP Components -### Quorum set +### Quorum Set As mentioned above, each Core node decides on which other nodes it would like to trust to reach agreement. A node’s trusted set of nodes is called a **quorum set**. Validators might add each other to their quorum sets due to innate trust associated with real-world identities. -### Thresholds and quorum slices +### Thresholds and Quorum Slices -In addition to choosing a quorum set, Core nodes must also choose a **threshold**. A threshold is the minimum number of nodes in a quorum set that must agree to reach consensus. For example, let’s say node B has nodes [A, C, D] in its quorum set and sets the threshold to 2. This means that any combination of 2 nodes in the quorum set agreeing is valid: either [A,C], [C,D], or [A,D] must agree for the node to proceed. The combination of agreeing nodes within the quorum set are called **quorum slices**. +In addition to choosing a quorum set, validator nodes must also choose a **threshold**. A threshold is the minimum number of nodes in a quorum set that must agree to reach consensus. For example, let’s say node $\mathbf{B}$ has nodes [$\mathbf{A}$, $\mathbf{C}$, $\mathbf{D}$] in its quorum set and sets the threshold to 2. This means that any combination of 2 nodes in the quorum set agreeing is valid: either [$\mathbf{A}$,$\mathbf{C}$], [$\mathbf{C}$,$\mathbf{D}$], or [$\mathbf{A}$,$\mathbf{D}$] must agree for the node to proceed. The combination of agreeing nodes within the quorum set are called **quorum slices**. -### Node blocking sets +### Node Blocking Sets Nodes can be blocked from reaching consensus by **node blocking sets**. Node blocking sets are any set of nodes in a quorum set that prevent a node from reaching agreement. For example, if a node requires 3 out of 4 of the nodes in its quorum set to agree, any combination of two nodes is considered a node blocking set. @@ -47,46 +49,54 @@ Valid **statements** on Stellar express the different opinions of nodes regardin A node’s opinion on a statement depends on the opinions of its quorum set. -## Federated voting +## Federated Voting + +In the SCP, agreement is achieved using federated voting. A node reasons about the state of the network based on what it learns from its quorum set. Nodes do this by processing three steps of federated voting before statements are 100% agreed upon by every honest node in the network. + +A node can have four opinions on a statement (let’s call the statement $\mathcal{A}$) -In the SCP, agreement is achieved using federated voting. A node reasons about the state of the network based on what it learns from its quorum set- before a statement is 100% agreed upon by every honest node in the network, it goes through three steps of federated voting: (1) Vote, (2) Accept, and (3) Confirm. +- I don’t know anything about $\mathcal{A}$ and have no opinion +- I vote for $\mathcal{A}$, it’s valid, but I don’t know if it’s safe to act on it yet +- I accept $\mathcal{A}$, because enough nodes supported this statement, but I don’t know if it’s safe to act on it yet +- I confirm $\mathcal{A}$, it is safe to act on it. Even if every node in my quorum has not confirmed $\mathcal{A}$, they will not be able to confirm anything else but $\mathcal{A}$. -A node can have four opinions on a statement (let’s call the statement “A”) +To transition between the states above, federated voting has the following steps: -- I don’t know anything about A and have no opinion -- I vote for A, it’s valid, but I don’t know if it’s safe to act on it yet -- I accept A, because enough nodes supported this statement, but I don’t know if it’s safe to act on it yet -- I confirm A, it is safe to act on it. Even if every node in my quorum has not confirmed A, they will not be able to confirm anything else but A. +{/* VAC steps pending subsections */} -To transition between the states above, federated voting has the following rules: +1. _Vote_ for $\mathcal{A}$ if it is consistent with my previous votes +2. _Accept_ $\mathcal{A}$ if either: -- Vote for A if it is consistent with my previous votes -- Accept A if either: - - Every node in my quorum slice voted for or accepted A +- Every node in my quorum slice voted for or accepted $\mathcal{A}$ - OR +OR - - My blocking set accepted A (even if I voted for something that contradicts A in the past, I forget about that vote, and proceed with accepting A) +- My blocking set accepted $\mathcal{A}$ (even if I voted for something that contradicts $\mathcal{A}$ in the past, I forget about that vote, and proceed with accepting $\mathcal{A}$) -- Confirm A if every node in a quorum slice accepted A +3. _Confirm_ $\mathcal{A}$ if every node in a quorum slice accepted $\mathcal{A}$ -## Consensus rounds +## Consensus Rounds Each consensus round is separated into two stages: -### Nomination protocol +### Nomination Protocol In the nomination protocol, candidate transaction sets are selected to be included in a ledger. Once a node confirms its first candidate, it stops voting to nominate any new transaction sets. It may still accept or confirm previously nominated statements. This guarantees that at some point, all nodes will converge on a candidate set. If every node on the network stops introducing new values but continues to confirm what other nodes confirmed, eventually, everyone will end up with the same list of candidates. A node may start the ballot protocol as soon as it confirms a candidate. After it confirms its first candidate and starts the ballot protocol, nomination continues running in the background. -### Ballot protocol +### Ballot Protocol The ballot protocol ensures that the network can unanimously confirm and apply nominated transaction sets. It consists of two steps: -1. Prepare - verifies that a node’s quorum slice has the right value and is willing to commit it -2. Commit - ensures that a node’s quorum slice actually commits the value +#### Prepare + +Verifies that a node’s quorum slice has the right value and is willing to commit it. + +#### Commit + +Ensures that a node’s quorum slice actually commits the value. -## White paper +## Full Whitepaper -Access the SCP white paper [here](https://stellar.org/learn/stellar-consensus-protocol). +Access the SCP whitepaper [here](https://stellar.org/learn/stellar-consensus-protocol). diff --git a/docs/learn/fundamentals/stellar-data-structures/_category_.json b/docs/learn/fundamentals/stellar-data-structures/_category_.json new file mode 100644 index 0000000000..5d21b285a4 --- /dev/null +++ b/docs/learn/fundamentals/stellar-data-structures/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Stellar Data Structures", + "position": 50, + "link": { + "type": "doc", + "id": "learn/fundamentals/stellar-data-structures/README" + }, + "description": "Learn how accounts, assets, contracts, events, and ledgers fit together to describe everything living on the Stellar network." +} diff --git a/docs/learn/fundamentals/stellar-data-structures/accounts.mdx b/docs/learn/fundamentals/stellar-data-structures/accounts.mdx index 59868bf298..9d4f98f2fc 100644 --- a/docs/learn/fundamentals/stellar-data-structures/accounts.mdx +++ b/docs/learn/fundamentals/stellar-data-structures/accounts.mdx @@ -5,15 +5,15 @@ description: "Learn about accounts on the Stellar network, including how they st sidebar_position: 20 --- -# Accounts +import { MethodTable } from "@site/src/components/MethodTable"; Accounts are the central data structure in Stellar—they hold balances, sign transactions, and issue assets. Accounts can only exist with a valid keypair and the required minimum balance of XLM. -To learn about minimum balance requirements, [see our section on Lumens](../lumens.mdx#minimum-balance). +To learn about minimum balance requirements, see our [section on Lumens](../lumens.mdx#minimum-balance). :::note -There are two types of accounts on Stellar: Stellar accounts (`G...` addresses) and contract accounts (`C...` addresses). For a minimal contract account walkthrough, start with the [Simple Account example](../../../build/smart-contracts/example-contracts/simple-account.mdx). This section focuses on Stellar `G...` accounts. +There are two types of accounts on Stellar: Stellar accounts (`G...` addresses) and [contract accounts (`C...` addresses)](../../../build/smart-contracts/example-contracts/complex-account.mdx). This section focuses on Stellar `G...` accounts. ::: @@ -43,10 +43,13 @@ A base reserve is a unit of measurement used to calculate an account’s minimum Account data is stored in subentries, each of which increases an account’s minimum balance by one base reserve (0.5 XLM). An account cannot have more than 1,000 subentries. Possible subentries are: -- Trustlines (includes traditional assets and pool shares) +- Trustlines + - Includes traditional assets and pool shares - Offers - Additional signers -- Data entries (includes data made with the `manageData` operation, not smart contract ledger entries) +- Data entries + - Includes data made with the `manageData` operation + - Does not include smart contract ledger entries ## Trustlines diff --git a/docs/learn/fundamentals/stellar-data-structures/assets.mdx b/docs/learn/fundamentals/stellar-data-structures/assets.mdx index 41d1da2230..9f6ee3182d 100644 --- a/docs/learn/fundamentals/stellar-data-structures/assets.mdx +++ b/docs/learn/fundamentals/stellar-data-structures/assets.mdx @@ -5,15 +5,9 @@ description: "Learn how assets work on the Stellar network, including issuing, t sidebar_position: 30 --- -:::info - -The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). - -::: - # Assets -Accounts on the Stellar network can be used to track, hold, and transfer any type of asset. Assets can represent many things: cryptocurrencies (such as bitcoin or ether), fiat currencies (such as dollars or pesos), other tokens of value (such as NFTs), pool shares, or bonds and equity. +Accounts on the Stellar network can be used to track, hold, and transfer any type of asset. Assets can represent many things: cryptocurrencies (such as bitcoin or ether), fiat currencies (such as dollars or pesos), other tokens of value (such as NFTs, pool shares, or securities). :::note @@ -27,6 +21,12 @@ Learn more about the differences in the [Assets and Tokens section](../../../tok Classic assets on Stellar have two identifying characteristics: the asset code and the issuer. Since more than one organization can issue a credit representing the same asset, asset codes often overlap (for example, multiple companies offer a USD token on Stellar). Assets are uniquely identified by the combination of their asset code and issuer. +:::info + +"Contract token" is the preferred syntax over "custom token," as discussed in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). + +::: + ## Asset components ### Asset code @@ -95,7 +95,7 @@ For example, the integer amount value 25,123,456 equals 2.5123456 units of the a The smallest non-zero amount unit, also known as a stroop, is 0.0000001 (one ten-millionth) represented as an integer value of one. The largest amount unit possible is $\frac{2^{63}-1}{10^7}$ (derived from the maximum 64-bit integer, scaled down) which is 922,337,203,685.4775807. -The numbers are represented as int64s. Amount values are stored as only signed integers to avoid bugs that arise from mixing signed and unsigned integers. +The numbers are represented as `int64s`. Amount values are stored as only signed integers to avoid bugs that arise from mixing signed and unsigned integers. ## Relevance in Stellar Client Libraries diff --git a/docs/learn/fundamentals/stellar-data-structures/events.mdx b/docs/learn/fundamentals/stellar-data-structures/events.mdx index 0580da7df3..61d1b92c11 100644 --- a/docs/learn/fundamentals/stellar-data-structures/events.mdx +++ b/docs/learn/fundamentals/stellar-data-structures/events.mdx @@ -1,7 +1,7 @@ --- sidebar_position: 11 title: Events -description: Monitor off-chain movement of value and smart contract changes. +description: Monitor on-chain movement of value and smart contract events from off-chain applications. --- Events are the mechanism that applications off-chain can use to monitor movement of value of any Stellar operation, as well as custom events in contracts on-chain. diff --git a/docs/learn/fundamentals/stellar-data-structures/ledgers.mdx b/docs/learn/fundamentals/stellar-data-structures/ledgers.mdx deleted file mode 100644 index 9673cefbf0..0000000000 --- a/docs/learn/fundamentals/stellar-data-structures/ledgers.mdx +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: "Ledgers Store Accounts, Balances, Orders, Smart Contract Data & More" -sidebar_position: 10 -sidebar_label: Ledgers -description: "A ledger captures the state of the Stellar network at a point in time, storing accounts, balances, orders, smart contract data, and other persistent information." ---- - -# Ledgers - -A ledger represents the state of the Stellar network at a point in time. It is shared across all Core nodes in the network and contains the list of accounts and balances, orders on the distributed exchange, smart contract data, and any other persisting data. - -:::note - -Blockchains typically refer to the **ledger** as the entire record of all transactions on the blockchain and **blocks** as individual units of data that contain a collection of transactions. In Stellar, "ledger" can refer to both. - -::: - -In every Stellar Consensus Protocol round, the network reaches consensus on which transaction set to apply to the last closed ledger, and when the new set is applied, a new “last closed ledger” is defined. Each ledger is cryptographically linked to the unique previous ledger, creating a historical chain that goes back to the genesis ledger. - -Data is stored on the ledger as ledger entries. Possible ledger entries include: - -- [Accounts](./accounts.mdx) -- [Claimable balances](../../../build/guides/transactions/claimable-balances.mdx) -- [Liquidity pools](../../fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx) -- [Contract data](../../fundamentals/contract-development/storage/persisting-data.mdx#ledger-entries) - -## Ledger headers - -Every ledger has a header that references the data in that ledger and the previous ledger. These references are cryptographic hashes of the content which behave like pointers in typical data structures but with added security guarantees. Think of a historical ledger chain as a linked list of ledger headers. Time flows forward from left to right, hashes point backwards in time, from right to left. Each hash in the chain links a ledger to its previous ledger, which authenticates the entire history of ledgers in its past: - -```mermaid -flowchart RL - subgraph genesis["Genesis"] - direction LR - prev1["Prev: none"] - state1["Genesis state"] - end - - subgraph block2["Ledger 2"] - prev2["Prev: hash(Genesis)"] - state2["Ledger 2
transactions
and state"] - end - - subgraph block3["Ledger 3"] - prev3["Prev: hash(Ledger 2)"] - state3["Ledger 3
transactions
and state"] - end - - subgraph dotdot["..."] - end - - subgraph blockn["Ledger N"] - prevn["Prev: hash(Ledger N-1)"] - staten["Ledger N
transactions
and state"] - end - - - genesis ~~~ block2 ~~~ block3 ~~~ dotdot ~~~ blockn - prev2 --> genesis - prev3 --> block2 - dotdot --> block3 - prevn --> dotdot - -``` - -The genesis ledger has a sequence number of 1. The ledger directly following a ledger with sequence number `N` has a sequence number of `N+1`. Ledger `N+1` contains a hash of ledger `N` in its previous ledger field. - -## Ledger header fields - -### Version - -The protocol version of this ledger. - -### Previous ledger hash - -Hash of the previous ledger. - -### SCP value - -During consensus, all the validating nodes in the network run SCP and agree on a particular value, which is a transaction set they will apply to a ledger. This value is stored here and in the following three fields (transaction set hash, close time, and upgrades). - -### Transaction set hash - -Hash of the transaction set applied to the previous ledger. - -### Close time - -The close time is a UNIX timestamp indicating when the ledger closes. Its accuracy depends on the system clock of the validator proposing the block. Consequently, SCP may confirm a close time that lags a few seconds behind or up to 60 seconds ahead. It's strictly monotonic – guaranteed to be greater than the close time of an earlier ledger. - -### Upgrades - -How the network adjusts overall values (like the base fee) and agrees to network-wide changes (like switching to a new protocol version). This field is usually empty. When there is a network-wide upgrade, the SDF will inform and help coordinate participants using the #validators channel on the Dev Discord and the Stellar Validators Google Group. - -### Transaction set result hash - -Hash of the results of applying the transaction set. This data is not necessary for validating the results of the transactions. However, it makes it easier for entities to validate the result of a given transaction without having to apply the transaction set to the previous ledger. - -### Bucket list hash - -Hash of all the objects in this ledger. The data structure that contains all the objects is called the bucket list. - -### Ledger sequence - -The sequence number of this ledger. - -### Total coins - -Total number of lumens in existence. - -### Fee pool - -Number of lumens that have been paid in fees. Note this is denominated in lumens, even though a transaction’s fee field is in stroops. - -### Inflation sequence - -Number of times inflation has been run. Note: the inflation operation was deprecated when validators voted to upgrade the network to Protocol 12 on 10/28/2019. Therefore, inflation no longer runs, so this sequence number no longer changes. - -### ID pool - -The last used global ID. These IDs are used for generating objects. - -### Maximum number of transactions - -The maximum number of operations validators have agreed to process in a given ledger. If more transactions are submitted than this number, the network will enter into surge pricing mode. For more about surge pricing and fee strategies, see our [Fees section](../../fundamentals/fees-resource-limits-metering.mdx). - -### Base fee - -The fee the network charges per operation in a transaction. Calculated in stroops. See the [Fees section](../../fundamentals/fees-resource-limits-metering.mdx) for more information. - -### Base reserve - -The reserve the network uses when calculating an account’s minimum balance. - -### Skip list - -Hashes of ledgers in the past. Intended to accelerate access to past ledgers without walking back ledger by ledger. Currently unused. diff --git a/docs/learn/fundamentals/stellar-data-structures/ledgers/README.mdx b/docs/learn/fundamentals/stellar-data-structures/ledgers/README.mdx new file mode 100644 index 0000000000..e49d6b5947 --- /dev/null +++ b/docs/learn/fundamentals/stellar-data-structures/ledgers/README.mdx @@ -0,0 +1,23 @@ +--- +title: "Ledgers Store Accounts, Balances, Orders, Smart Contract Data & More" +sidebar_position: 10 +sidebar_label: Ledgers +hide_table_of_contents: false +description: "A ledger captures the state of the Stellar network at a point in time, storing accounts, balances, orders, smart contract data, and other persistent information." +--- + +import DocCardList from "@theme/DocCardList"; + +# Ledgers + +A ledger represents the state of the Stellar network at a point in time. It is shared across all Core nodes in the network and contains the list of accounts and balances, orders on the distributed exchange, smart contract data, and any other persisting data. + +:::note + +Blockchains typically refer to the **ledger** as the entire record of all transactions on the blockchain and **blocks** as individual units of data that contain a collection of transactions. In Stellar, "ledger" can refer to both. + +::: + +During each [Stellar Consensus Protocol](../../stellar-consensus-protocol.mdx) round, validators agree on a transaction set to apply to the last closed ledger. When that set is applied, a new last closed ledger is defined. Each ledger is cryptographically linked to the unique previous ledger, creating a historical chain that goes back to the genesis ledger. + + diff --git a/docs/learn/fundamentals/stellar-data-structures/ledgers/_category_.json b/docs/learn/fundamentals/stellar-data-structures/ledgers/_category_.json new file mode 100644 index 0000000000..a37565f28d --- /dev/null +++ b/docs/learn/fundamentals/stellar-data-structures/ledgers/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Ledgers", + "position": 10, + "link": { + "type": "doc", + "id": "learn/fundamentals/stellar-data-structures/ledgers/README" + }, + "description": "Learn how Stellar ledgers snapshot the entire network state, from accounts and balances to DEX orders and smart-contract data." +} diff --git a/docs/learn/fundamentals/stellar-data-structures/ledgers/entries.mdx b/docs/learn/fundamentals/stellar-data-structures/ledgers/entries.mdx new file mode 100644 index 0000000000..6fa2b8ce09 --- /dev/null +++ b/docs/learn/fundamentals/stellar-data-structures/ledgers/entries.mdx @@ -0,0 +1,88 @@ +--- +title: "Ledger Entries" +sidebar_label: Entries +sidebar_position: 20 +description: "Ledger entries are the durable data structures that make up the Stellar network state." +--- + +# Ledger Entries + +Data is stored on the ledger as ledger entries, akin to key-value stores. Each key is a `LedgerKey` entry defined in [protocol XDR](https://github.com/stellar/stellar-xdr/blob/v23.0/Stellar-ledger-entries.x#L588), with values as `LedgerEntry` instances. Keys define which object we reference, while entries contain the data stored at that key in [protocol XDR](https://github.com/stellar/stellar-xdr/blob/v23.0/Stellar-ledger-entries.x#L548). + +Because `LedgerEntries` are a full record of ledger data, they include the identifying key. These fungible key identifiers and fields lower excess variables, using the same subset of XDR structure. There are 10 different forms a ledger key can take: + +### Account + +The `AccountEntry` holistically defines a Stellar account, including its balance, sequence number, thresholds, signers, and flags. See [Accounts](../accounts.mdx) section. + +- **Key:** Identified by `AccountID` +- **Entry:** Full account record including balance, sequence number, thresholds, flags, home domain, and list of signers. Tracks [subentries](../accounts.mdx#subentries) that drive the minimum balance requirement. + +### Trustline + +The `TrustLineEntry` defines a balance line to a non-native asset issued on the network. Created and updated via [`changeTrustOp`](../../transactions/list-of-operations.mdx#change-trust). + +- **Key:** `(AccountID, asset)` +- **Entry:** Trustline state for a non-native asset. Stores the asset balance, limit, authorization flags, and trading liability counters. + +### Offer + +The `OfferEntry` represents an offer made on the SDEX order book. See [Liquidity on Stellar](../../liquidity-on-stellar-sdex-liquidity-pools.mdx#orderbook) DEX section. + +- **Key:** `(sellerID, offerID)` +- **Entry:** A single offer on the DEX. Contains the selling and buying assets, amount, price, and [passive](../../liquidity-on-stellar-sdex-liquidity-pools.mdx#passive-offer) flags. + +### Account Data + +A `DataEntry` stores key-value data entries attached to an account. Used through the [`manageDataOp`](../../transactions/list-of-operations.mdx#manage-data). + +- **Key:** `(accountID, dataName)` +- **Entry:** Arbitrary key-value pair (`DataValue`) attached to an account. + +### Claimable Balance + +The `ClaimableBalanceEntry` tracks a balance that may or may not actively be claimable. See [Claimable Balances](../../../../build/guides/transactions/claimable-balances.mdx) guide. + +- **Key:** `balanceID` +- **Entry:** Tracks the asset, amount, claimants, [predicates](../../../../build/guides/transactions/claimable-balances.mdx#other-parameters), and optional flags. + +### Liquidity Pool + +The `LiquidityPoolEntry` defines the configuration of a constant-product liquidity pool between two assets. See [Liquidity on Stellar](../../liquidity-on-stellar-sdex-liquidity-pools.mdx#liquidity-pools) section. + +- **Key:** `liquidityPoolID` +- **Entry:** Holds pool parameters (two assets, fee rate), reserves, total pool shares, and a pool's trustline count (to prevent entry scanning). + +### Contract Data + +`ContractDataEntry` stores a piece of data under a contract-defined key. See [Persisting Contract Data](../../contract-development/storage/persisting-data.mdx#ledger-entries) section. + +- **Key:** `(contract, key, durability)` +- **Entry:** Stores the `SCVal` value associated with that key under the contract. Used for Soroban data storage alongside keys. + +### Contract Code + +The `ContractCodeEntry` contains the Wasm bytecode of a Soroban contract. See [Smart Contracts Overview](../../../../build/smart-contracts/overview.mdx) page. + +- **Key:** `hash` of the Wasm module. +- **Entry:** Contains the Wasm bytecode and associated cost inputs to deploy or invoke contracts. [Fee metadata](../../fees-resource-limits-metering.mdx#resource-fee) takes into account component factors like number of instructions, functions, and variables. + +### Config Setting + +The `ConfigSettingEntry` holds governed network configuration values for over a dozen rules. See [Soroban Settings](../../../../validators/admin-guide/soroban-settings.mdx) section. + +- **Key:** `configSettingID` (index of setting [in list](https://github.com/stellar/stellar-xdr/blob/4b7a2ef7931ab2ca2499be68d849f38190b443ca/Stellar-contract-config-setting.x#L319-L338)) +- **Entry:** Active value of a network-level parameter (e.g., protocol limits, compute fees, state archival). The whole validator quorum set has to agree on these values. + +### TTL + +The `TTLEntry` defines the time-to-live of an associated contract data or code entry. + +- **Key:** `keyHash` (hash of associated LedgerKey). +- **Entry:** Defines the expiration ledger sequence (`liveUntilLedgerSeq`) for temporary objects such as contract data or code. + +## Working with ledger entries + +Most products only need to reason about a subset of ledger entries—for example, accounts, trustlines, and liquidity pools cover the majority of asset and trading use cases. Once you understand how to build and parse a few representative keys and entries, you can extrapolate the same patterns to the remaining entry types. + +Ledger entries are always mutated through transactions: operations either create, update, or delete these records as part of applying a transaction set. Use [`getLedgerEntries`](../../../../data/apis/rpc/api-reference/methods/getLedgerEntries.mdx) when you need to fetch the raw on-ledger representation for debugging or cross-checking derived data. diff --git a/docs/learn/fundamentals/stellar-data-structures/ledgers/headers.mdx b/docs/learn/fundamentals/stellar-data-structures/ledgers/headers.mdx new file mode 100644 index 0000000000..2adbced62b --- /dev/null +++ b/docs/learn/fundamentals/stellar-data-structures/ledgers/headers.mdx @@ -0,0 +1,119 @@ +--- +title: "Ledger Headers" +sidebar_label: Headers +sidebar_position: 10 +description: "Ledger headers capture the metadata that links each Stellar ledger to its history and summarizes recent transactions." +--- + +# Ledger Headers + +Every ledger has a header that references the data in that ledger and the previous ledger. These references are cryptographic hashes of the content which behave like pointers in typical data structures but with added security guarantees. Think of a historical ledger chain as a linked list of ledger headers. Time flows forward from left to right, hashes point backwards in time, from right to left. Each hash in the chain links a ledger to its previous ledger, which authenticates the entire history of ledgers in its past: + +```mermaid +flowchart RL + subgraph genesis["Genesis"] + direction LR + prev1["Prev: none"] + state1["Genesis state"] + end + + subgraph block2["Ledger 2"] + prev2["Prev: hash(Genesis)"] + state2["Ledger 2\\ntransactions\\nand state"] + end + + subgraph block3["Ledger 3"] + prev3["Prev: hash(Ledger 2)"] + state3["Ledger 3\\ntransactions\\nand state"] + end + + subgraph dotdot["..."] + end + + subgraph blockN["Ledger N"] + prevN["Prev: hash(Ledger N-1)"] + stateN["Ledger N\\ntransactions\\nand state"] + end + + genesis ~~~ block2 ~~~ block3 ~~~ dotdot ~~~ blockN + prev2 --> genesis + prev3 --> block2 + dotdot --> block3 + prevN --> dotdot +``` + +The genesis ledger has a sequence number of 1. The ledger directly following a ledger with sequence number `N` has a sequence number of `N + 1`. Ledger `N + 1` contains a hash of ledger `N` in its previous ledger field. + +## Consensus Fields + +### Version + +The protocol version of this ledger. + +### Previous Ledger Hash + +Hash of the previous ledger. + +### SCP Value + +During consensus, all the validating nodes in the network run SCP and agree on a particular value, which is a transaction set they will apply to a ledger. This value is stored here and in the following three fields (transaction set hash, close time, and upgrades). + +### Transaction Set Hash + +Hash of the transaction set applied to the previous ledger. + +### Transaction Set Result Hash + +Hash of the results of applying the transaction set. This data is not necessary for validating the results of the transactions. However, it makes it easier for entities to validate the result of a given transaction without having to apply the transaction set to the previous ledger. + +### Close Time + +The close time is a UNIX timestamp indicating when the ledger closes. Its accuracy depends on the system clock of the validator proposing the block. Consequently, SCP may confirm a close time that lags a few seconds behind or up to 60 seconds ahead. It is strictly monotonic - guaranteed to be greater than the close time of an earlier ledger. + +### Upgrades + +How the network adjusts overall values (like the base fee) and agrees to network-wide changes (like switching to a new protocol version). This field is usually empty. When there is a network-wide upgrade, the SDF will inform and help coordinate participants using the #validators channel on the Dev Discord and the Stellar Validators Google Group. + +### Bucket List Hash + +Hash of all the objects in this ledger. The data structure that contains all the objects is called the bucket list. + +### Ledger Sequence + +The sequence number of this ledger. + +### Total Coins + +Total number of lumens in existence. + +### Fee Pool + +Number of lumens that have been paid in fees. Note this is denominated in lumens, even though a transaction's fee field is in stroops. + +### Inflation Sequence + +Number of times inflation has been run. The inflation operation was stopped when validators voted to upgrade the network to Protocol 12 on 28 Oct 2019. This number does not change without inflation. + +### ID Pool + +The last used global ID. These IDs are used for generating objects. The number is a single monotonically increasing counter which corresponds to unique identifiers like OfferIDs or ClaimableBalanceIDs. + +### Maximum Number of Transactions + +The maximum number of operations validators have agreed to process in a given ledger. If more transactions are submitted than this number, the network will enter surge pricing mode. For more about surge pricing and fee strategies, see the [Fees section](../../fees-resource-limits-metering.mdx). + +### Base Fee + +The fee the network charges per operation in a transaction. Calculated in stroops. See the [Fees section](../../fees-resource-limits-metering.mdx) for more information. + +### Base Reserve + +The reserve the network uses when calculating an account's minimum balance. + +### Skip List + +Hashes of ledgers in the past. Intended to accelerate access to past ledgers without walking back ledger by ledger. Currently unused. + +## Extension Flags + +A field that validators can flip through `LEDGER_UPGRADE_FLAGS`, directly changing header flags bitmasks. Unlike the config settings above, these are not durable ledger entries with keys and tunable parameters. This feature can be extended like all of the [Ledger Entries](./entries.mdx) for arbitrary future expansion of network functions that could require an "emergency switch." diff --git a/docs/learn/fundamentals/stellar-ecosystem-proposals.mdx b/docs/learn/fundamentals/stellar-ecosystem-proposals.mdx index 5084be5d6d..f6bfc36880 100644 --- a/docs/learn/fundamentals/stellar-ecosystem-proposals.mdx +++ b/docs/learn/fundamentals/stellar-ecosystem-proposals.mdx @@ -11,7 +11,7 @@ Each SEP is a distinct blueprint meant to help users build a product or service :::note -This page covers Stellar Ecosystem Proposals (SEPs), which define standards and protocols for projects building on the Stellar network. SEPs differ from Core Advancement Proposals (CAPs), which propose changes to the Stellar network’s core protocol. You can learn more about CAPs on [GitHub](https://github.com/stellar/stellar-protocol/tree/master/core). +This page covers Stellar Ecosystem Proposals (SEPs), which define standards and protocols for projects building on the Stellar network. SEPs differ from [Core Advancement Proposals](../glossary.mdx#caps-core-advancement-proposals) (CAPs), which propose changes to the Stellar network’s core protocol. You can learn more about CAPs on [GitHub](https://github.com/stellar/stellar-protocol/tree/master/core). ::: diff --git a/docs/learn/fundamentals/stellar-stack.mdx b/docs/learn/fundamentals/stellar-stack.mdx index 0472e7bfb2..b6aee96fab 100644 --- a/docs/learn/fundamentals/stellar-stack.mdx +++ b/docs/learn/fundamentals/stellar-stack.mdx @@ -37,7 +37,7 @@ SDF does not provide a publicly available RPC endpoint for Mainnet. Developers s :::warning -Horizon is considered deprecated in favor of Stellar RPC. While it will continue to receive updates to maintain compatiblity with upcoming protocol releases, it won't receive significant new feature development. +Horizon is considered deprecated in favor of Stellar RPC. While it will continue to receive updates to maintain compatiblity with upcoming protocol releases, it won't receive significant new feature development from [the SDF](../glossary.mdx#stellar-development-foundation-sdf). ::: diff --git a/docs/learn/fundamentals/transactions/README.mdx b/docs/learn/fundamentals/transactions/README.mdx index c962cd76db..5a680704de 100644 --- a/docs/learn/fundamentals/transactions/README.mdx +++ b/docs/learn/fundamentals/transactions/README.mdx @@ -5,8 +5,13 @@ sidebar_label: Operations & Transactions description: "Learn about transactions on Stellar: how they work, the transaction lifecycle, a list of operations, and how they transfer assets and execute smart contracts." --- +import DocCardList from "@theme/DocCardList"; +import YouTube from "@site/src/components/YouTube"; + # Operations & Transactions -import DocCardList from "@theme/DocCardList"; + + +Stellar transactions bundle operations into atomic submissions, so you can move assets, authorize accounts, or run smart contracts without juggling multiple network calls. This section shows how a transaction travels from signing to the ledger and spotlights the operations you can mix and match to build your use cases. diff --git a/docs/learn/fundamentals/transactions/_category_.json b/docs/learn/fundamentals/transactions/_category_.json new file mode 100644 index 0000000000..4bb739336d --- /dev/null +++ b/docs/learn/fundamentals/transactions/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Operations & Transactions", + "position": 60, + "link": { + "type": "doc", + "id": "learn/fundamentals/transactions/README" + }, + "description": "Trace how Stellar transactions bundle operations, flow through the network, and unlock asset transfers or contract calls." +} diff --git a/docs/learn/fundamentals/transactions/list-of-operations.mdx b/docs/learn/fundamentals/transactions/list-of-operations.mdx index dec2d89543..24d043c7b6 100644 --- a/docs/learn/fundamentals/transactions/list-of-operations.mdx +++ b/docs/learn/fundamentals/transactions/list-of-operations.mdx @@ -19,9 +19,9 @@ All these operations have an optional source account parameter. If the source ac ::: -## Create account +## Create Account -Creates and funds a new account with the specified starting balance +Creates and funds a new account with a specified starting balance. **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.createAccount) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/CreateAccountOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#CreateAccount) **Threshold**: Medium @@ -44,7 +44,7 @@ Creates and funds a new account with the specified starting balance ## Payment -Sends an amount in a specific asset to a destination account +Sends an amount in a specific asset to a destination account. **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.payment) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/PaymentOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#Payment) **Threshold**: Medium @@ -70,9 +70,9 @@ Sends an amount in a specific asset to a destination account | `PAYMENT_NOT_AUTHORIZED` | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | | `PAYMENT_LINE_FULL` | -8 | The destination account (receiver) does not have sufficient limits to receive `amount` and still satisfy its buying liabilities. | -## Path payment strict send +## Path Payment Strict Send -A payment where the asset sent can be different than the asset received; allows the user to specify the amount of the asset to send +Facilitates a payment where the asset sent can be different from the asset received, allowing you to specify the amount of an asset to send. Learn more about path payments: [Path Payments Guide](../../../build/guides/transactions/path-payments.mdx) @@ -88,7 +88,7 @@ Learn more about path payments: [Path Payments Guide](../../../build/guides/tran | Destination | account ID | Account ID of the recipient. | | Destination asset | asset | The asset the destination account receives. | | Destination min | integer | The minimum amount of `destination asset` the destination account can receive. | -| Path | list of assets | The assets (other than `send asset` and `destination asset`) involved in the offers the path takes. For example, if you can only find a path from USD to EUR through XLM and BTC, the path would be USD -> XLM -> BTC -> EUR and the `path` field would contain XLM and BTC. | +| Path | list of assets | The assets (other than `send asset` and `destination asset`) involved in path a route takes. For example, the conversion `AstroDollar` -> _CryptoToken_ -> _AstroEuro_ -> _GoldReserves_ -> `AstroPeso` uses three assets to send from dollars to pesos, included in the `path` [found](../../../build/guides/transactions/path-payments.mdx#pathfinding). | **Possible errors**: @@ -102,13 +102,13 @@ Learn more about path payments: [Path Payments Guide](../../../build/guides/tran | `PATH_PAYMENT_STRICT_SEND_NO_TRUST` | -6 | The destination account does not trust the issuer of the asset being sent. For more, see the [Assets section](../stellar-data-structures/assets.mdx). | | `PATH_PAYMENT_STRICT_SEND_NOT_AUTHORIZED` | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | | `PATH_PAYMENT_STRICT_SEND_LINE_FULL` | -8 | The destination account does not have sufficient limits to receive `destination amount` and still satisfy its buying liabilities. | -| `PATH_PAYMENT_STRICT_SEND_TOO_FEW_OFFERS` | -10 | There is no path of offers connecting the `send asset` and `destination asset`. Stellar only considers paths of length 5 or shorter. | +| `PATH_PAYMENT_STRICT_SEND_TOO_FEW_OFFERS` | -10 | There is no path of offers connecting the `send asset` and `destination asset`. Intermediate assets must be 5 or less. | | `PATH_PAYMENT_STRICT_SEND_OFFER_CROSS_SELF` | -11 | The payment would cross one of its own offers. | | `PATH_PAYMENT_STRICT_SEND_UNDER_DESTMIN` | -12 | The paths that could send `destination amount` of `destination asset` would fall short of `destination min`. | -## Path payment strict receive +## Path Payment Strict Receive -A payment where the asset received can be different from the asset sent; allows the user to specify the amount of the asset received +Facilitates a payment where the asset received can be different from the asset sent, allowing you to specify the amount of an asset to receive. Learn more about path payments: [Path Payments Guide](../../../build/guides/transactions/path-payments.mdx) @@ -124,7 +124,7 @@ Learn more about path payments: [Path Payments Guide](../../../build/guides/tran | Destination | account ID | Account ID of the recipient. | | Destination asset | asset | The asset the destination account receives. | | Destination amount | integer | The amount of `destination asset` the destination account receives. | -| Path | list of assets | The assets (other than `send asset` and `destination asset`) involved in the offers the path takes. For example, if you can only find a path from USD to EUR through XLM and BTC, the path would be USD -> XLM -> BTC -> EUR and the `path` field would contain XLM and BTC. | +| Path | list of assets | The assets (other than `send asset` and `destination asset`) involved in path a route takes. For example, the conversion `AstroDollar` -> _CryptoToken_ -> _AstroEuro_ -> _GoldReserves_ -> `AstroPeso` uses three assets to send from dollars to pesos, included in the `path` [found](../../../build/guides/transactions/path-payments.mdx#pathfinding). | **Possible errors**: @@ -138,17 +138,17 @@ Learn more about path payments: [Path Payments Guide](../../../build/guides/tran | `PATH_PAYMENT_STRICT_RECEIVE_NO_TRUST` | -6 | The destination account does not trust the issuer of the asset being sent. For more, see the [Assets section](../stellar-data-structures/assets.mdx). | | `PATH_PAYMENT_STRICT_RECEIVE_NOT_AUTHORIZED` | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | | `PATH_PAYMENT_STRICT_RECEIVE_LINE_FULL` | -8 | The destination account does not have sufficient limits to receive `destination amount` and still satisfy its buying liabilities. | -| `PATH_PAYMENT_STRICT_RECEIVE_TOO_FEW_OFFERS` | -10 | There is no path of offers connecting the `send asset` and `destination asset`. Stellar only considers paths of length 5 or shorter. | +| `PATH_PAYMENT_STRICT_RECEIVE_TOO_FEW_OFFERS` | -10 | There is no path of offers connecting the `send asset` and `destination asset`. Intermediate assets must be 5 or less. | | `PATH_PAYMENT_STRICT_RECEIVE_OFFER_CROSS_SELF` | -11 | The payment would cross one of its own offers. | | `PATH_PAYMENT_STRICT_RECEIVE_OVER_SENDMAX` | -12 | The paths that could send `destination amount` of `destination asset` would exceed `send max`. | -## Manage buy offer +## Manage Buy Offer -Creates, updates, or deletes an offer to buy a specific amount of an asset for another +Creates, updates, or deletes an offer to buy a specific amount of an asset for another. Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity Pools](../liquidity-on-stellar-sdex-liquidity-pools.mdx) -**SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageBuyOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ManageBuyOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#ManageBuyOffer) +**SDKs**: [Python](https://stellar-sdk.readthedocs.io/en/stable/api.html#managebuyoffer) | [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageBuyOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ManageBuyOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#ManageBuyOffer) **Threshold**: Medium **Result**: `ManageBuyOfferResult` **Parameters**: @@ -176,13 +176,13 @@ Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity | `MANAGE_BUY_OFFER_NOT_FOUND` | -11 | An offer with that `offerID` cannot be found. | | `MANAGE_BUY_OFFER_LOW_RESERVE` | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | -## Manage sell offer +## Manage Sell Offer -Creates, updates, or deletes an offer to sell a specific amount of an asset for another +Creates, updates, or deletes an offer to sell a specific amount of an asset for another. Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity Pools](../liquidity-on-stellar-sdex-liquidity-pools.mdx) -**SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageSellOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ManageSellOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#ManageSellOffer) +**SDKs**: [Python](https://stellar-sdk.readthedocs.io/en/stable/api.html#stellar_sdk.operation.ManageSellOffer) | [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageSellOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/ManageSellOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#ManageSellOffer) **Threshold**: Medium **Result**: `ManageSellOfferResult` **Parameters**: @@ -210,13 +210,13 @@ Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity | `MANAGE_SELL_OFFER_NOT_FOUND` | -11 | An offer with that `offerID` cannot be found. | | `MANAGE_SELL_OFFER_LOW_RESERVE` | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | -## Create passive sell offer +## Create Passive Sell Offer -Creates an offer to sell one asset for another without taking a reverse offer of equal price +Creates an offer to sell an asset for another without taking a reverse offer of equal price. Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity Pools](../liquidity-on-stellar-sdex-liquidity-pools.mdx) -**SDKs**: [JavaScript](https://stellar.github.io/js-stellar-sdk/Operation.html#.createPassiveSellOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/CreatePassiveSellOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#CreatePassiveSellOffer) +**SDKs**: [Python](https://stellar-sdk.readthedocs.io/en/stable/api.html#createpassiveselloffer) | [JavaScript](https://stellar.github.io/js-stellar-sdk/Operation.html#.createPassiveSellOffer) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/CreatePassiveSellOfferOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#CreatePassiveSellOffer) **Threshold**: Medium **Result**: `ManageSellOfferResult` **Parameters**: @@ -243,16 +243,16 @@ Learn more about passive sell offers: [Liquidity on Stellar: SDEX and Liquidity | `MANAGE_SELL_OFFER_NOT_FOUND` | -11 | An offer with that `offerID` cannot be found. | | `MANAGE_SELL_OFFER_LOW_RESERVE` | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | -## Set options +## Set Options -Set options for an account such as flags, inflation destination, signers, home domain, and master key weight +Sets account-level options such as flags, inflation destination, signers, home domain, and master key weight. Learn more about flags: [Flags Section](../../../tokens/control-asset-access.mdx#controlling-access-to-an-asset-with-flags) Learn more about the home domain: [Stellar Ecosystem Proposals SEP-0001](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) Learn more about signers operations and key weight: [Signature and Multisignature Section](../../fundamentals/transactions/signatures-multisig.mdx) -**SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.setOptions) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/SetOptionsOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#SetOptions) -**Threshold**: High (when updating signers or other thresholds) or Medium (when updating everything else) +**SDKs**: [Python](https://stellar-sdk.readthedocs.io/en/stable/api.html#setoptions) | [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.setOptions) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/SetOptionsOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#SetOptions) +**Threshold**: High if updating signers or other thresholds; Medium for everything else **Result**: `SetOptionsResult` **Parameters**: @@ -282,9 +282,9 @@ Learn more about signers operations and key weight: [Signature and Multisignatur | `SET_OPTIONS_BAD_SIGNER` | -8 | Any additional signers added to the account cannot be the master key. | | `SET_OPTIONS_INVALID_HOME_DOMAIN` | -9 | Home domain is malformed. | -## Change trust +## Change Trust -Creates, updates, or deletes a trustline +Creates, updates, or deletes a trustline for an asset. Learn more about trustlines: [Trustlines section](../stellar-data-structures/accounts.mdx#trustlines) @@ -311,15 +311,11 @@ Learn more about trustlines: [Trustlines section](../stellar-data-structures/acc | `CHANGE_TRUST_CANNOT_DELETE` | -7 | The asset trustline is still referenced by a liquidity pool. | | `CHANGE_TRUST_NOT_AUTH_MAINTAIN_LIABILITIES` | -8 | The asset trustline is deauthorized. | -## Allow trust +## Allow Trust -Updates the authorized flag of an existing trustline. This operation can only be performed by the asset issuer +Updates the authorization flag of an existing trustline. -:::warning - -This operation is deprecated as of Protocol 17- prefer [_SetTrustlineFlags_](#set-trustline-flags) operation instead. - -::: +Protocol 17 depricated this operation in favor of [`SetTrustlineFlags`](#set-trustline-flags). **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.allowTrust) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/AllowTrustOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#AllowTrust) **Threshold**: Low @@ -343,9 +339,9 @@ This operation is deprecated as of Protocol 17- prefer [_SetTrustlineFlags_](#se | `ALLOW_TRUST_SELF_NOT_ALLOWED` | -5 | The source account attempted to allow a trustline for itself, which is not allowed because an account cannot create a trustline with itself. | | `ALLOW_TRUST_LOW_RESERVE` | -6 | Claimable balances can't be created on revocation of asset (or pool share) trustlines associated with a liquidity pool due to low reserves. | -## Account merge +## Account Merge -Transfers the XLM balance of an account to another account and removes the source account from the ledger +Transfers the XLM balance of one account to another account and removes the source account from the ledger. **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.accountMerge) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/AccountMergeOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#AccountMerge) **Threshold**: High @@ -368,9 +364,9 @@ Transfers the XLM balance of an account to another account and removes the sourc | `ACCOUNT_MERGE_DEST_FULL` | -6 | The `destination` account cannot receive the balance of the source account and still satisfy its lumen buying liabilities. | | `ACCOUNT_MERGE_IS_SPONSOR` | -7 | The source account is a sponsor. | -## Manage data +## Manage Data -Sets, modifies, or deletes a data entry (name/value pair) that is attached to an account +Adds, modifies, or deletes a data entry ([name-value pair](../../../data/apis/horizon/api-reference/resources/operations/object/manage-data.mdx)) attached to an account. Learn more about entries and subentries: [Accounts section](../stellar-data-structures/accounts.mdx#subentries) @@ -393,9 +389,9 @@ Learn more about entries and subentries: [Accounts section](../stellar-data-stru | `MANAGE_DATA_LOW_RESERVE` | -3 | This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new DataEntry added to an account, the minimum reserve of XLM that account must hold increases. | | `MANAGE_DATA_INVALID_NAME` | -4 | Name not a valid string. | -## Bump sequence +## Bump Sequence -Bumps forward the sequence number of the source account to the given sequence number, invalidating any transaction with a smaller sequence number +Bumps forward the sequence number of the source account to the given sequence number, invalidating any transaction with a smaller sequence number. **SDKs**: [JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.bumpSequence) | [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/BumpSequenceOperation.java) | [Go](https://godoc.org/github.com/stellar/go-stellar-sdk/txnbuild#BumpSequence) **Threshold**: Low @@ -414,7 +410,7 @@ Bumps forward the sequence number of the source account to the given sequence nu ## Create claimable balance -Moves an amount of asset from the operation source account into a new ClaimableBalanceEntry +Creates a `ClaimableBalanceEntry`, transferring an amount of an asset from the operation source account into a new ledger entry. Learn more about claimable balances: [Claimable Balances Guide](../../../build/guides/transactions/claimable-balances.mdx) @@ -439,9 +435,9 @@ Learn more about claimable balances: [Claimable Balances Guide](../../../build/g | `CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED` | -4 | The source account is not authorized to transfer this asset. | | `CREATE_CLAIMABLE_BALANCE_UNDERFUNDED` | -5 | The source account does not have enough funds to transfer amount of this asset to the ClaimableBalanceEntry. | -## Claim claimable balance +## Claim Claimable Balance -Claims a ClaimableBalanceEntry that corresponds to the BalanceID and adds the amount of an asset on the entry to the source account +Claims a `ClaimableBalanceEntry` that corresponds to the `BalanceID`, transferring its assets to the claiming account. Learn more about claimable balances and view more parameters: [Claimable Balances Guide](../../../build/guides/transactions/claimable-balances.mdx) @@ -464,11 +460,11 @@ Learn more about claimable balances and view more parameters: [Claimable Balance | `CLAIM_CLAIMABLE_BALANCE_NO_TRUST` | -4 | The source account does not trust the issuer of the asset it is trying to claim in the ClaimableBalanceEntry. | | `CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED` | -5 | The source account is not authorized to claim the asset in the ClaimableBalanceEntry. | -## Begin sponsoring future reserves +## Begin Sponsoring Future Reserves -Allows an account to pay the base reserves for another account; sponsoring account establishes the is-sponsoring-future-reserves relationship +Allows an account to pay the base reserves for another account, whereby the sponsoring account establishes the `is-sponsoring-future-reserves` relationship. -There must also be an end sponsoring future reserves operation in the same transaction +There must also be an [end sponsoring future reserves](#end-sponsoring-future-reserves) operation in the same transaction. Learn more about sponsored reserves: [Sponsored Reserves Guide](../../../build/guides/transactions/sponsored-reserves.mdx) @@ -489,9 +485,9 @@ Learn more about sponsored reserves: [Sponsored Reserves Guide](../../../build/g | `BEGIN_SPONSORING_FUTURE_RESERVES_ALREADY_SPONSORED` | -2 | Source account is already sponsoring sponsoredID. | | `BEGIN_SPONSORING_FUTURE_RESERVES_RECURSIVE` | -3 | Either source account is currently being sponsored, or sponsoredID is sponsoring another account. | -## End sponsoring future reserves +## End Sponsoring Future Reserves -Terminates the current is-sponsoring-future-reserves relationship in which the source account is sponsored +Terminates the current `is-sponsoring-future-reserves` relationship between the source account and the sponsored account. Learn more about sponsored reserves: [Sponsored Reserves Guide](../../../build/guides/transactions/sponsored-reserves.mdx) @@ -510,9 +506,11 @@ Learn more about sponsored reserves: [Sponsored Reserves Guide](../../../build/g | --- | --- | --- | | `END_SPONSORING_FUTURE_RESERVES_NOT_SPONSORED` | -1 | Source account is not sponsored. | -## Revoke sponsorship +## Revoke Sponsorship -Sponsoring account can remove or transfer sponsorships of existing ledgerEntries and signers; the logic of this operation depends on the state of the source account +Removes or transfers the sponsoring account's sponsorship of existing [`ledgerEntries`](../stellar-data-structures/ledgers/entries.mdx) or signers. + +The logic of this operation depends on the state of the source account. Learn more about sponsored reserves: [Sponsored Reserves Guide](../../../build/guides/transactions/sponsored-reserves.mdx) @@ -525,7 +523,7 @@ This operation is a union with **two** possible types: | --- | --- | --- | --- | | `REVOKE_SPONSORSHIP_LEDGER_ENTRY` | LedgerKey | ledgerKey | Ledger key that holds information to identify a specific ledgerEntry that may have its sponsorship modified. See [LedgerKey](../../glossary.mdx#ledgerkey) for more information. | -Or +or | Union Type | Parameters | Type | Description | | --- | --- | --- | --- | @@ -543,7 +541,7 @@ Or ## Clawback -Burns an amount in a specific asset from an account. Only the issuing account for the asset can perform this operation. +Burns an amount of an asset from the account holding it, as allowed by the asset's issuing account. Learn more about clawbacks: [Clawback Guide](../../../build/guides/transactions/clawbacks.mdx) @@ -569,7 +567,7 @@ Learn more about clawbacks: [Clawback Guide](../../../build/guides/transactions/ ## Clawback claimable balance -Claws back an unclaimed ClaimableBalanceEntry, burning the pending amount of the asset. Only the issuing account for the asset can perform this operation. +Burns the unclaimed amount of an asset from a `ClaimableBalanceEntry`. Learn more about clawbacks: [Clawback Guide](../../../build/guides/transactions/clawbacks.mdx) @@ -592,14 +590,14 @@ Learn more about claimable balances: [Claimable Balances Guide](../../../build/g | `CLAWBACK_CLAIMABLE_BALANCE_NOT_ISSUER` | -2 | The source account is not the issuer of the asset in the claimable balance. | | `CLAWBACK_CLAIMABLE_BALANCE_NOT_CLAWBACK_ENABLED` | -3 | `The CLAIMABLE_BALANCE_CLAWBACK_ENABLED_FLAG` is not set for this trustline. | -## Set trustline flags - -Allows issuing account to configure authorization and trustline flags to an asset +## Set Trustline Flags -The Asset parameter is of the `TrustLineAsset` type. If you are modifying a trustline to a regular asset (i.e. one in a Code:Issuer format), this is equivalent to the Asset type. If you are modifying a trustline to a pool share, however, this is composed of the liquidity pool's unique ID. +Allows [issuing accounts](../../../tokens/control-asset-access.mdx#issuing-and-distribution-accounts) to configure future authorization or other trustline flags for an asset. Learn more about flags: [Flags Glossary Entry](../../glossary.mdx#flags) +The Asset parameter is of the `TrustLineAsset` type. If you are modifying a trustline to a regular asset (i.e. one in a `Code:Issuer` format), this is equivalent to the Asset type. If you are modifying a trustline to a pool share, however, this is composed of the liquidity pool's [unique ID](../liquidity-on-stellar-sdex-liquidity-pools.mdx#pool-id). + **SDKs**: [Java](https://github.com/lightsail-network/java-stellar-sdk/blob/master/src/main/java/org/stellar/sdk/operations/SetTrustlineFlagsOperation.java) **Threshold**: Low **Result**: `SetTrustLineFlagsResult` @@ -622,13 +620,13 @@ Learn more about flags: [Flags Glossary Entry](../../glossary.mdx#flags) | `SET_TRUST_LINE_FLAGS_INVALID_STATE` | -4 | If the final state of the trustline has both AUTHORIZED_FLAG (1) and AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG (2) set, which are mutually exclusive. | | `SET_TRUST_LINE_FLAGS_LOW_RESERVE` | -5 | Claimable balances can't be created on revocation of asset (or pool share) trustlines associated with a liquidity pool due to low reserves. | -## Liquidity pool deposit +## Liquidity Pool Deposit -Deposits assets into a liquidity pool, increasing the reserves of a liquidity pool in exchange for pool shares +Deposits assets into a liquidity pool, increasing its reserves in exchange for pool shares. -Parameters to this operation depend on the ordering of assets in the liquidity pool: “A” refers to the first asset in the liquidity pool, and “B” refers to the second asset in the liquidity pool. +Parameters to this operation depend on the ordering of assets in the liquidity pool: $\mathrm{A}$ refers to the first asset in the liquidity pool, and $\mathrm{B}$ refers to the second asset in the liquidity pool. -If the pool is empty, then this operation deposits maxAmountA of A and maxAmountB of B into the pool. If the pool is not empty, then this operation deposits at most maxAmountA of A and maxAmountB of B into the pool. The actual amounts deposited are determined using the current reserves of the pool. You can use these parameters to control a percentage of slippage. +If the pool is empty, then this operation deposits `maxAmountA` of $\mathrm{A}$ and `maxAmountB` of $\mathrm{B}$ into the pool. If the pool is not empty, then this operation deposits at most `maxAmountA` of $\mathrm{A}$ and `maxAmountB` of $\mathrm{B}$ into the pool. The actual amounts deposited are determined using the current reserves of the pool. You can use these parameters to control a percentage of slippage. Learn more about liquidity pools: [Liquidity Pools section](../liquidity-on-stellar-sdex-liquidity-pools.mdx) @@ -657,9 +655,9 @@ Learn more about liquidity pools: [Liquidity Pools section](../liquidity-on-stel | `LIQUIDITY_POOL_DEPOSIT_BAD_PRICE` | -6 | The deposit price is outside of the given bounds. | | `LIQUIDITY_POOL_DEPOSIT_POOL_FULL` | -7 | The liquidity pool reserves are full. | -## Liquidity pool withdraw +## Liquidity Pool Withdraw -Withdraw assets from a liquidity pool, reducing the number of pool shares in exchange for reserves of a liquidity pool +Withdraw assets from a liquidity pool, reducing the number of pool shares in exchange for its constituent reserves. The minAmountA and minAmountB parameters can be used to control a percentage of slippage from the "spot price" on the pool. @@ -689,9 +687,9 @@ Learn more about liquidity pools: [Liquidity Pools section](../liquidity-on-stel ## Invoke Host Function -Invoke and deploy Soroban smart contracts with `InvokeHostFunctionOp`. +Invokes Soroban smart-contract functions, deploys contracts, or uploads WebAssembly to the network. -The `InvokeHostFunctionOp` can be used to perform the following Soroban operations: +Soroban operations performed by `InvokeHostFunctionOp`: - Invoke contract functions: `HOST_FUNCTION_TYPE_INVOKE_CONTRACT` - Upload Wasm of the contracts: `HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM` diff --git a/docs/learn/fundamentals/transactions/operations-and-transactions.mdx b/docs/learn/fundamentals/transactions/operations-and-transactions.mdx index 1f634b9093..0a029cbea5 100644 --- a/docs/learn/fundamentals/transactions/operations-and-transactions.mdx +++ b/docs/learn/fundamentals/transactions/operations-and-transactions.mdx @@ -33,7 +33,7 @@ The Stellar network encodes transactions using a standardized protocol called Ex Accounts can only perform one transaction at a time. -Transactions comprise a bundle of between 1-100 operations (except smart contract transactions, which can only have one operation per transaction) and are signed and submitted to the ledger by accounts. Transactions always need to be authorized by the source account’s public key to be valid, which involves signing the transaction object with the public key’s associated secret key. A transaction plus its signature(s) is called a transaction envelope. +Transactions comprise a bundle of between 1–100 operations (except smart contract transactions, which can only have one operation per transaction) and are signed and submitted to the ledger by accounts. Transactions always need to be authorized by the source account’s public key to be valid, which involves signing the transaction object with the public key’s associated secret key. A transaction plus its signature(s) is called a transaction envelope. A transaction may need more than one signature- this happens if it has operations that affect more than one account or if it has a high threshold weight. Check out the [Signature and Multisignature Section](../../fundamentals/transactions/signatures-multisig.mdx) for more information. diff --git a/docs/learn/fundamentals/transactions/signatures-multisig.mdx b/docs/learn/fundamentals/transactions/signatures-multisig.mdx index 0b1ba943e7..81c50a9909 100644 --- a/docs/learn/fundamentals/transactions/signatures-multisig.mdx +++ b/docs/learn/fundamentals/transactions/signatures-multisig.mdx @@ -1,8 +1,17 @@ --- title: Signatures and Multisig +description: How digital signatures authorize transactions, how threshold weights work, and how multisig setups protect accounts. sidebar_position: 20 --- +:::danger TODO + +let's walk though creating a custodied solution akin to FT [`href`] and then break down the implications re CAP23 pending merges + +::: + +starting from minimal singing basis at https://developers.stellar.org/docs/build/guides/basics/create-account + :::note This section details signing non-smart contract transactions. For auth related to smart contract transactions, see [authorization](../../fundamentals/contract-development/authorization.mdx).) @@ -38,6 +47,234 @@ In some cases, a transaction may need more than one signature: Each additional signer beyond the master key increases the account’s minimum balance by one base reserve. Up to 20 signatures can be attached to one transaction. Once a signature threshold is met, if there are leftover signatures, the transaction will fail. For example, if your transaction requires three signatures, providing more than three signatures, even if they are all valid, will result in a failed transaction error: `TX_BAD_AUTH_EXTRA`. This design is because unnecessary signature verification has a large effect on performance before accepting transactions in consensus. +`example five signers each own auth, collectively cannot change signers` + +REQUIRES: server, primaryAccountSecret, &primaryAccount primatives + +```python +channelAccPubKeys = [ + "GA7WHVWBBUCVKVZ35GM5FVGZXZYJ63EFDB5VTFV2TRF4KLUXWN6CSIJL", + "GD4EITS74V4RN5XOKO42DX2SRR3NI5JVC3LFUWIAVQH2PXBUEKLZU6V3", + "GCEO3FNAOF7VKBRD22JHMT3Q5IVVUPDEICBKZNTKL7KUBOX6EDYYYTG4", + "GAUTAZU2E6VUFOPRDOYAMYQBD7JU3XRLODSKLQL4EXIYUDB22FFPYDJC", + "GBACJWSCHTRILBAPVOYXCVIFGUF7FSHTEKMITBI5ESCEQVEIJ6QFV4P5" +] + +primaryPubKey = "GC5AT5GU7WI5NHHRM4XH2ALBOQS5I2MTLNYH5RTEJZC5LWCKVN52SVBV" +primaryAccount = server.load_account(primaryPubKey) + +def generateChannelAccounts(primaryPubKey, channelAccPubKeys, primaryAccountSecret): + transactionBuilder = TransactionBuilder( + source_account = primaryAccount, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = 100 + ) + + for channelPubKeys in channelAccPubKeys: + transactionBuilder.append_create_account_op( + destination = channelPubKey, + starting_balance = "4.4" + ) + + transaction = transactionBuilder.build() + transaction.sign(primarySecretKey) + return server.submit_transaction(transaction) + +def authorizeChannelAccounts(primaryPubKey, channelAccPubKeys, primaryAccountSecret): + transactionBuilder = TransactionBuilder( + source_account = primaryAccount, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = 100 + ) + + transactionBuilder.append_set_options_op( + high_threshold = 44, + master_weight = 44, + medium_threshold = 3 + ) + + for channelPubKeys in channelAccPubKeys: + transactionBuilder.append_set_options_op( + signer={ + "ed25519PublicKey": channelPubKey, + "weight": 3 + } + ) +``` + +```js +const channelAccPubKeys = [ + "GA7WHVWBBUCVKVZ35GM5FVGZXZYJ63EFDB5VTFV2TRF4KLUXWN6CSIJL", + "GD4EITS74V4RN5XOKO42DX2SRR3NI5JVC3LFUWIAVQH2PXBUEKLZU6V3", + "GCEO3FNAOF7VKBRD22JHMT3Q5IVVUPDEICBKZNTKL7KUBOX6EDYYYTG4", + "GAUTAZU2E6VUFOPRDOYAMYQBD7JU3XRLODSKLQL4EXIYUDB22FFPYDJC", + "GBACJWSCHTRILBAPVOYXCVIFGUF7FSHTEKMITBI5ESCEQVEIJ6QFV4P5", +]; + +const primaryPubKey = + "GC5AT5GU7WI5NHHRM4XH2ALBOQS5I2MTLNYH5RTEJZC5LWCKVN52SVBV"; +const primaryAccount = server.loadAccount(primaryPubKey); + +async function generateChannelAccounts(primaryAccountSecret) { + const transactionBuilder = new StellarSdk.TransactionBuilder(primaryAccount, { + fee: StellarSdk.BASE_FEE, + networkPassphrase: StellarSdk.Networks.TESTNET, + }); + + channelAccPubKeys.forEach((channelPubKey) => { + transactionBuilder.addOperation( + StellarSdk.Operation.createAccount({ + destination: channelPubKey, + startingBalance: "4.4", + }), + ); + }); + + const transaction = transactionBuilder.build(); + transaction.sign(primaryKeypair); + const result = await server.submitTransaction(transaction); +} + +async function authorizeChannelAccounts(primaryAccountSecret) { + const transactionBuilder = new StellarSdk.TransactionBuilder(primaryAccount, { + fee: StellarSdk.BASE_FEE, + networkPassphrase: StellarSdk.Networks.TESTNET, + }); + + transactionBuilder.addOperation( + StellarSdk.Operation.setOptions({ + highThreshold: 44, + masterWeight: 44, + mediumThreshold: 3, + }), + ); + + channelAccPubKeys.forEach((channelPubKey) => { + transactionBuilder.addOperation( + StellarSdk.Operation.setOptions({ + signer: { + ed25519PublicKey: channelPubKey, + weight: 3, + }, + }), + ); + }); +} +``` + +```java +private static final List channelAccPubKeys = Arrays.asList( + "GA7WHVWBBUCVKVZ35GM5FVGZXZYJ63EFDB5VTFV2TRF4KLUXWN6CSIJL", + "GD4EITS74V4RN5XOKO42DX2SRR3NI5JVC3LFUWIAVQH2PXBUEKLZU6V3", + "GCEO3FNAOF7VKBRD22JHMT3Q5IVVUPDEICBKZNTKL7KUBOX6EDYYYTG4", + "GAUTAZU2E6VUFOPRDOYAMYQBD7JU3XRLODSKLQL4EXIYUDB22FFPYDJC", + "GBACJWSCHTRILBAPVOYXCVIFGUF7FSHTEKMITBI5ESCEQVEIJ6QFV4P5" +); +private static final String primaryPubKey = "GC5AT5GU7WI5NHHRM4XH2ALBOQS5I2MTLNYH5RTEJZC5LWCKVN52SVBV"; +private static final AccountResponse primaryAccount = server.accounts().account(primaryPubKey); + +public static void generateChannelAccounts(String primaryAccountSecret) throws Exception { + Transaction.Builder transactionBuilder = new Transaction.Builder(primaryAccount, Network.TESTNET) + .setBaseFee(Transaction.MIN_BASE_FEE) + + for (String channelPubKeys : channelAccPubKeys) { + transactionBuilder.addOperation( + new CreateAccountOperation.Builder(channelPubKeys, startingBalance).build() + ); + } + + Transaction transaction = transactionBuilder.build(); + transaction.sign(primaryKeypair); + + SubmitTransactionResponse response = server.submitTransaction(transaction); + System.out.println("Transaction successful: " + response); +} + +public static void authorizeChannelAccounts(String primaryAccountSecret) throws Exception { + + Transaction.Builder transactionBuilder = new Transaction.Builder(primaryAccount, Network.TESTNET) + .setBaseFee(Transaction.MIN_BASE_FEE) + + transactionBuilder.addOperation( + new SetOptionsOperation.Builder() + .setHighThreshold(44) + .setMasterKeyWeight(44) + .setMediumThreshold(3) + .build() + ); + + for (String channelPubKeys : channelAccPubKeys) { + transactionBuilder.addOperation( + new SetOptionsOperation.Builder() + .setSigner(new Signer.SignerKeyEd25519(channelPubKeys), 3) + .build() + ); + } +} +``` + +```go +var ( + channelAccPubKeys = []string{ + "GA7WHVWBBUCVKVZ35GM5FVGZXZYJ63EFDB5VTFV2TRF4KLUXWN6CSIJL", + "GD4EITS74V4RN5XOKO42DX2SRR3NI5JVC3LFUWIAVQH2PXBUEKLZU6V3", + "GCEO3FNAOF7VKBRD22JHMT3Q5IVVUPDEICBKZNTKL7KUBOX6EDYYYTG4", + "GAUTAZU2E6VUFOPRDOYAMYQBD7JU3XRLODSKLQL4EXIYUDB22FFPYDJC", + "GBACJWSCHTRILBAPVOYXCVIFGUF7FSHTEKMITBI5ESCEQVEIJ6QFV4P5", + } + primaryPubKey = "GC5AT5GU7WI5NHHRM4XH2ALBOQS5I2MTLNYH5RTEJZC5LWCKVN52SVBV" +) + +func generateChannelAccounts(primaryAccountSecret string) { + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &primaryAccount, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Operations: []txnbuild.Operation{}, + }, + ) + check(err) + + for _, channelPubKey := range channelAccPubKeys { + createOp := txnbuild.CreateAccount{ + Destination: channelPubKey, + Amount: "4.4", + } + tx.Operations = append(tx.Operations, &createOp) + } +} + +func authorizeChannelAccounts(primaryAccountSecret string) { + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &primaryAccount, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Operations: []txnbuild.Operation{}, + }, + ) + check(err) + + setThreshOp := txnbuild.SetOptions{ + HighThreshold: txnbuild.NewThreshold(44), + MasterWeight: txnbuild.NewThreshold(44), + MediumThreshold: txnbuild.NewThreshold(3), + } + tx.Operations = append(tx.Operations, &setThreshOp) + + for _, channelPubKey := range channelAccPubKeys { + addSignerOp := txnbuild.SetOptions{ + Signer: &txnbuild.Signer{ + Address: channelPubKey, + Weight: txnbuild.NewThreshold(3) + }, + } + tx.Operations = append(tx.Operations, &addSignerOp) + } +} +``` + ### Alternate signature types To enable some advanced smart contract features there are a couple of additional signature types. These signature types also have weights and can be added and removed similarly to normal signature types. But rather than check a cryptographic signature for authorization they have a different method of proving validity to the network. diff --git a/docs/learn/fundamentals/transactions/transaction-lifecycle.mdx b/docs/learn/fundamentals/transactions/transaction-lifecycle.mdx index 881defc96b..8a18af3d0f 100644 --- a/docs/learn/fundamentals/transactions/transaction-lifecycle.mdx +++ b/docs/learn/fundamentals/transactions/transaction-lifecycle.mdx @@ -1,5 +1,6 @@ --- title: Transaction Lifecycle +description: Follow how a transaction moves from creation and signing through consensus, fee collection, and ledger application. sidebar_position: 30 --- diff --git a/docs/learn/glossary.mdx b/docs/learn/glossary.mdx index 107a6dabc5..d0e97ead98 100644 --- a/docs/learn/glossary.mdx +++ b/docs/learn/glossary.mdx @@ -21,7 +21,7 @@ The public key used to create an account. This key persists across different key The on and off-ramps on the Stellar network that facilitate one-to-one conversion of off-chain representations to and from tokenized assets, for example, digital tokens representing bank deposits. -Read more in the [Anchor Encyclopedia entry](./fundamentals/anchors.mdx) +Read more in the [Anchor Encyclopedia Entry](./fundamentals/anchors.mdx) ### Application (app) @@ -33,6 +33,10 @@ Fiat, physical, or other tokens of value that are tracked, held, or transferred See the [Assets section](./fundamentals/stellar-data-structures/assets.mdx) to learn more. +### Atomic Swap + +An atomic swap is a decentralized method of exchanging one asset for another without the need for a trusted third-party intermediary. The process ensures that the interchange will either be completed fully by both parties or not at all—hence the term “atomic,” meaning indivisible. If one party fails to fulfill the required conditions, the trade is automatically canceled, and both parties retain their assets. + ### Balance The amount of a given asset an account holds. Each asset has its own balance and these balances are stored in trustlines for every asset except XLM, which is held directly by the account. @@ -57,7 +61,11 @@ Learn more in our [Lumens section](./fundamentals/lumens.mdx#base-reserves). ### Burn -Remove an asset from circulation, which can happen in two ways: 1) a holder sends the asset back to the issuing account 2) an issuer claws back a clawback-enabled asset from a holder's account. +Remove an asset from circulation, which can happen in three ways: + +1. a holder sends the asset back to the issuing account, +2. an issuer claws back a clawback-enabled asset from a holder's account, or +3. an issuer aquires their own asset through the SDEX. ### CAPs (Core Advancement Proposals) @@ -69,14 +77,20 @@ Find a list of all draft, accepted, implemented, and rejected CAPs in [GitHub](h A recursive data structure used to construct complex conditionals with different values of ClaimPredicateType. +_See_ [Claimable Balances Guide](../build/guides/transactions/claimable-balances.mdx). + ### ClaimableBalanceID -A SHA-256 hash of the OperationID for claimable balances. +A SHA-256 hash of a combination of transaction data for claimable balances. + +_See_ [Claimable Balances Guide](../build/guides/transactions/claimable-balances.mdx). ### 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. +_See_ [Claimable Balances Guide](../build/guides/transactions/claimable-balances.mdx). + ### Clawback An amount of asset from a trustline or claimable balance removed (clawed back) from a recipient’s balance sheet. @@ -91,12 +105,6 @@ An account that is implemented as a smart contract, allowing the contract to def Tokens created and managed through smart contracts. These assets are programmable and governed by on-chain logic instead of built-in protocol features. -:::note - -Contract tokens used to be referred to as "custom tokens", which has been deprecated. - -::: - ### Create account operation Makes a payment to a 0-balance public key (Stellar address), thereby creating the account. You must use this operation to initialize an account rather than a standard payment operation. @@ -111,9 +119,9 @@ This term has been deprecated in favor of [contract tokens](#contract-token). ### Decentralized exchange -A distributed exchange that allows the trading and conversion of assets on the network. +A distributed exchange that allows the trading and conversion of assets on the network through atomic swaps. -Learn more in our [Liquidity on Stellar](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#sdex) section. +Learn more in our [Liquidity on Stellar](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#orderbook) section. ### External Data Representation (XDR) @@ -171,7 +179,7 @@ An encrypted store or file that serves as a repository of private keys, certific A representation of the state of the Stellar universe at a given point in time, shared across all network nodes. -Learn more in the [Ledgers section](./fundamentals/stellar-data-structures/ledgers.mdx). +Learn more in the [Ledgers section](./fundamentals/stellar-data-structures/ledgers/index). ### LedgerKey @@ -215,9 +223,9 @@ Learn more in our [Operations and Transactions](./fundamentals/transactions/oper Contains the transaction source account, sequence number, and the operation index of the CreateClaimableBalance operation in a transaction. -### Order +### Offer -An offer to buy or sell an asset. +An order to trade between two assets at a specific price. The order data structure can be retrieved from the network while active. Exchange execution decreases the offer amount. Learn more in our [Liquidity on Stellar: SDEX and Liquidity Pools section](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#orders). @@ -225,7 +233,7 @@ Learn more in our [Liquidity on Stellar: SDEX and Liquidity Pools section](./fun A record of outstanding orders on the Stellar network. -Learn more in our [Liquidity on Stellar: SDEX and Liquidity Pools section](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#order-books). +Learn more in our [Liquidity on Stellar: SDEX and Liquidity Pools section](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx#orderbook). ### Passive order @@ -241,6 +249,8 @@ Learn more about network passphrases in the [Networks section](../networks/READM The process of determining the best path of a payment, evaluating the current orderbooks, and finding the series of conversions to achieve the best rate. +It is currently only done on marketable Core execution and in [Horizon aggregation](../data/apis/horizon/api-reference/aggregations/README.mdx). + ### Payment channel Allows two parties who frequently transact with one another to move the bulk of their activity off-chain, while still recording opening balances and final settlement on-chain. @@ -265,6 +275,12 @@ The Stellar Public Network, aka mainnet, the main network used by applications i Read more in our [Networks section](../networks/README.mdx). +### SDEX + +A combination of native protocol features enabling seamless asset conversions. + +Learn more in our [Liquidity on Stellar](./fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx) section. + ### Sequence number Used to identify and verify the order of transactions with the source account. @@ -297,12 +313,16 @@ The smart contract platform on the Stellar network. The name "Soroban" comes fro ### Source account -The account that originates a transaction. This account also provides the fee and sequence number for the transaction. +The account from which assets are consumed. ### Starlight Stellar’s layer 2 protocol that allows for bi-directional payment channels. +See the [initial release](https://github.com/stellar-deprecated/starlight), [design considerations](https://youtu.be/LI_M6rWPCgQ), and [implimentation details](https://stellar.org/blog/developers/starlight-a-layer-2-payment-channel-protocol-for-stellar). + +Starlight relies on [CAP-21](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0021.md) and [CAP-40](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0040.md), both of which were introduced in [Protocol 19](https://stellar.org/blog/developers/protocol-19-upgrade-guide). + ### Stellar A decentralized, federated peer-to-peer network that allows people to send payments in any asset anywhere in the world instantaneously, and with minimal fees. @@ -359,7 +379,7 @@ Read more in the [Operations and Transactions section](./fundamentals/transactio ### Transaction envelope -A wrapper for a transaction that carries signatures. +A transaction plus its signature(s) is called a transaction envelope. ### Transaction fee diff --git a/docs/platforms/anchor-platform/api-reference/platform/rpc/anchor-platform.openrpc.json b/docs/platforms/anchor-platform/api-reference/platform/rpc/anchor-platform.openrpc.json index 8fa9f9e324..db64091b51 100644 --- a/docs/platforms/anchor-platform/api-reference/platform/rpc/anchor-platform.openrpc.json +++ b/docs/platforms/anchor-platform/api-reference/platform/rpc/anchor-platform.openrpc.json @@ -27,7 +27,7 @@ { "name": "do_stellar_payment", "summary": "Submits a Stellar payment", - "description": "Submits a payment to a stellar network by a custody service.", + "description": "Submits a payment to the Stellar network by a custody service.", "paramStructure": "by-name", "tags": [ { @@ -386,7 +386,7 @@ { "name": "do_stellar_refund", "summary": "Submits a Stellar refund", - "description": "Submits a refund payment to a stellar network by a custody service", + "description": "Submits a refund payment to the Stellar network by a custody service", "paramStructure": "by-name", "tags": [ { diff --git a/docs/tokens/README.mdx b/docs/tokens/README.mdx index e5ee26ead1..7ff08a3e69 100644 --- a/docs/tokens/README.mdx +++ b/docs/tokens/README.mdx @@ -1,16 +1,10 @@ --- sidebar_position: 10 -title: "Issuing Assets vs. Creating Custom Tokens: Key Differences & Best Practices" +title: "Issuing Assets vs. Creating Contract Tokens: Key Differences & Best Practices" sidebar_label: Stellar Assets and Contract Tokens description: "Learn about the differences between issuing assets on the Stellar network, creating smart contract tokens, and information and best practices for each." --- -:::info - -The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). - -::: - # Stellar Assets and Contract Tokens Tokens exist in two forms on Stellar: diff --git a/docs/tokens/control-asset-access.mdx b/docs/tokens/control-asset-access.mdx index 71e72d1179..87bfe0b896 100644 --- a/docs/tokens/control-asset-access.mdx +++ b/docs/tokens/control-asset-access.mdx @@ -3,31 +3,31 @@ title: Asset Design Considerations sidebar_position: 40 --- -## Issuing and distribution accounts +## Issuer and distributor accounts -It is best practice on the Stellar network to create two accounts when issuing an asset: 1) the issuing account and 2) the distribution account. +It is best practice on the Stellar network to create two accounts when issuing an asset: 1) the issuer account and 2) the distributor account. -The **issuing account** creates (or mints) the asset on the network by executing a payment operation. The issuing account will always be linked to the asset’s identity. Any account wanting to hold the asset must first establish a trustline with the issuing account. Read about trustlines in our [Trustlines section](../learn/fundamentals/stellar-data-structures/accounts.mdx#trustlines). +The **issuer account** creates (or mints) the asset on the network by executing a payment operation. The issuer account will always be linked to the asset’s identity. Any account wanting to hold the asset must first establish a trustline with the issuer account. Read about trustlines in our [Trustlines section](../learn/fundamentals/stellar-data-structures/accounts.mdx#trustlines). -The **distribution account** is the first recipient of the issued asset and handles all other transactions. +The **distributor account** is the first recipient of the issued asset and handles all other transactions. -Note that you can also issue an asset by creating an offer or liquidity pool deposit with the issuing account. +Note that you can also issue an asset by creating an offer or liquidity pool deposit with the issuer account. -It is best practice to issue an asset by sending it from the issuing account to a distribution account for two main reasons: security and auditing. +It is best practice to issue an asset by sending it from the issuer account to a distributor account for two main reasons: security and auditing. ### Security -The distribution account will be a hot account, meaning that some web service out there has direct access to sign its transactions. +The distributor account is a warm/hot account, meaning that some web service out there has direct access to sign its transactions. -For example, if the account you're distributing from is also the issuing account and it is compromised by a malicious actor, the actor can now issue as much of the asset as they want. If the malicious actor redeems the newly issued tokens with an anchor service, the anchor may not have the liquidity to support the customer withdrawals. Stakes are lower if you use a distribution account- if the distribution account is compromised, you can freeze the account’s asset balance and start with a new distribution account without changing the issuing account. +For example, if the account you're distributing from is also the issuer account and it is compromised by a malicious actor, the actor can now issue as much of the asset as they want. If the malicious actor redeems the newly issued tokens with an anchor service, the anchor may not have the liquidity to support the customer withdrawals. Stakes are lower if you use a distributor account; if the distributor account is compromised, you can freeze the account’s asset balance and start with a new distributor account without changing the issuer account. ### Auditing -Using a distribution account is better for auditing because an issuing account can’t actually hold a balance of its own asset. Sending an asset back to its issuing account burns (deletes) the asset. If you have a standing inventory of the issued asset in a separate account, it’s easier to track and can help with bookkeeping. +Using a distributor account is better for auditing because an issuer account can’t actually hold a balance of its own asset. Sending an asset back to its issuer account burns (deletes) the asset. If you have a standing inventory of the issued asset in a separate account, it’s easier to track and can help with bookkeeping. ## Naming an asset -One thing you must decide when issuing an asset is what to call it. An asset code is the asset’s identifying code. There are three possible formats: Alphanumeric 4, Alphanumeric 12, and liquidity pool shares. +One thing you must decide when issuer an asset is what to call it. An asset code is the asset’s identifying code. There are three possible formats: Alphanumeric 4, Alphanumeric 12, and liquidity pool shares. Learn about liquidity pool shares in the [Liquidity Pool Encyclopedia Entry](../learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools.mdx). @@ -39,13 +39,13 @@ Provided it falls into one of these buckets, you can choose any asset code you l ## Controlling access to an asset with flags -When you issue an asset on Stellar, anyone can hold it by default. In general, that’s a good thing: easy access means better reach and better liquidity. However, if you need to control access to an asset to comply with regulations (or for any other reason), you can easily do so by enabling flags on your issuing account. +When you issue an asset on Stellar, anyone can hold it by default. In general, that’s a good thing: easy access means better reach and better liquidity. However, if you need to control access to an asset to comply with regulations (or for any other reason), you can easily do so by enabling flags on your issuer account. Flags are created on the account level using a `set_options` operation. They can be set at any time in the life cycle of an asset, not just when you issue it. ### Flag types -The (0xn) next to each flag type denotes the bit settings for each flag. +The ($0x\mathfrak{n}$) next to each flag type denotes the bit settings for each flag. #### Authorization Required (0x1) @@ -80,11 +80,13 @@ Note that this flag requires that revocable is also set. #### Authorization Immutable (0x4) -With this setting, none of the other authorization flags (`AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`) can be set, and the issuing account can’t be merged. You set this flag to signal to potential token holders that your issuing account and its assets will persist on the ledger in an open and accessible state. +With this setting, none of the other authorization flags can be set, and the account can’t be merged.[^none] You set this flag to signal to potential token holders that your issuer account and its assets will persist on the ledger in an open and accessible state. + +[^none]: The issuer will not be able to set `AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`, or `AUTH_CLAWBACK_ENABLED_FLAG`. They also will not be able to disable any of these flags anymore if set when configuring `AUTH_IMMUTABLE_FLAG`. ### Set Trustline Flag operation -The issuing account can configure various authorization and trustline flags for individual trustlines to an asset. The asset parameter is of the TrustLineAsset type. If you are modifying a trustline to a regular asset (i.e. one in a Code:Issuer format), this is equivalent to the asset type. If you are modifying a trustline to a pool share, this is the liquidity pool’s unique ID. +The issuer account can configure various authorization and trustline flags for individual trustlines to an asset. The asset parameter is of the TrustLineAsset type. If you are modifying a trustline to a regular asset (i.e. one in a Code:Issuer format), this is equivalent to the asset type. If you are modifying a trustline to a pool share, this is the liquidity pool’s unique ID. ### Example flow @@ -112,19 +114,59 @@ The following example sets authorization to be both required and revocable: +```python +from stellar_sdk import Keypair, Network, Server, TransactionBuilder, AuthorizationFlag +from stellar_sdk.exceptions import BaseHorizonError + +# Configure Stellar SDK to talk to the horizon instance hosted by SDF +# To use the live network, set the hostname to horizon_url for mainnet +server = Server(horizon_url="https://horizon-testnet.stellar.org") + +# Using the test network. If you want to use the live network use `Network.PUBLIC_NETWORK_PASSPHRASE` +network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE + +# Keys for accounts to issue and receive the new asset +issuer_keypair = Keypair.from_secret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" +) +issuer_public = issuer_keypair.public_key + +# Transactions require a valid sequence number that is specific to this account. +# We can fetch the current sequence number for the source account from Horizon. +issuer_account = server.load_account(issuer_public) + +transaction = ( + TransactionBuilder( + source_account=issuer_account, + network_passphrase=network_passphrase, + base_fee=100, + ) + .append_set_options_op( + set_flags=AuthorizationFlag.AUTH_REVOCABLE_FLAG | AuthorizationFlag.AUTHORIZATION_REQUIRED + ) + .build() +) +transaction.sign(issuer_keypair) +try: + transaction_resp = server.submit_transaction(transaction) + print(f"Success: {transaction_resp}") +except BaseHorizonError as e: + print(f"Error: {e}") +``` + ```js var StellarSdk = require("stellar-sdk"); var server = new StellarSdk.Horizon.Server( "https://horizon-testnet.stellar.org", ); -// Keys for issuing account -var issuingKeys = StellarSdk.Keypair.fromSecret( +// Keys for issuer account +var issuerKeys = StellarSdk.Keypair.fromSecret( "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", ); server - .loadAccount(issuingKeys.publicKey()) + .loadAccount(issuerKeys.publicKey()) .then(function (issuer) { var transaction = new StellarSdk.TransactionBuilder(issuer, { fee: 100, @@ -138,7 +180,7 @@ server // setTimeout is required for a transaction .setTimeout(100) .build(); - transaction.sign(issuingKeys); + transaction.sign(issuerKeys); return server.submitTransaction(transaction); }) .then(console.log) @@ -154,10 +196,10 @@ import org.stellar.sdk.responses.AccountResponse; Server server = new Server("https://horizon-testnet.stellar.org"); -// Keys for issuing account -KeyPair issuingKeys = KeyPair +// Keys for issuer account +KeyPair issuerKeys = KeyPair .fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); -AccountResponse sourceAccount = server.accounts().account(issuingKeys.getAccountId()); +AccountResponse sourceAccount = server.accounts().account(issuerKeys.getAccountId()); Transaction setAuthorization = new Transaction.Builder(sourceAccount, Network.TESTNET) .addOperation(new SetOptionsOperation.Builder() @@ -166,58 +208,75 @@ Transaction setAuthorization = new Transaction.Builder(sourceAccount, Network.TE AccountFlag.AUTH_REVOCABLE_FLAG.getValue()) .build()) .build(); -setAuthorization.sign(issuingKeys); +setAuthorization.sign(issuerKeys); server.submitTransaction(setAuthorization); ``` -```python -from stellar_sdk import Keypair, Network, Server, TransactionBuilder, AuthorizationFlag -from stellar_sdk.exceptions import BaseHorizonError - -# Configure Stellar SDK to talk to the horizon instance hosted by Stellar.org -# To use the live network, set the hostname to horizon_url for mainnet -server = Server(horizon_url="https://horizon-testnet.stellar.org") -# Use the test network, if you want to use the live network, please set it to `Network.PUBLIC_NETWORK_PASSPHRASE` -network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE +```go +package main -# Keys for accounts to issue and receive the new asset -issuing_keypair = Keypair.from_secret( - "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" +import ( + "fmt" + "github.com/stellar/go-stellar-sdk/clients/horizonclient" + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/network" + "github.com/stellar/go-stellar-sdk/txnbuild" ) -issuing_public = issuing_keypair.public_key - -# Transactions require a valid sequence number that is specific to this account. -# We can fetch the current sequence number for the source account from Horizon. -issuing_account = server.load_account(issuing_public) -transaction = ( - TransactionBuilder( - source_account=issuing_account, - network_passphrase=network_passphrase, - base_fee=100, - ) - .append_set_options_op( - set_flags=AuthorizationFlag.AUTH_REVOCABLE_FLAG | AuthorizationFlag.AUTHORIZATION_REQUIRED - ) - .build() -) -transaction.sign(issuing_keypair) -try: - transaction_resp = server.submit_transaction(transaction) - print(f"Transaction Resp:\n{transaction_resp}") -except BaseHorizonError as e: - print(f"Error: {e}") +func main() { + client := horizonclient.DefaultTestNetClient + issuerKeys, err := keypair.ParseFull( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" + ) + check(err) + + issuerAccountRequest := horizonclient.AccountRequest{AccountID: issuerKeys.Address()} + issuerAccount, err := client.AccountDetail(issuerAccountRequest) + check(err) + + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &issuerAccount, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.SetOptions{ + SetFlags: []txnbuild.AccountFlag{ + txnbuild.AuthRevocable, + txnbuild.AuthRequired, + }, + }, + }, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewInfiniteTimeout(), + }, + ) + if err != nil { + log.Fatalf("Failed to build transaction: %v", err) + } + tx, err = tx.Sign(network.TestNetworkPassphrase, issuerKeys) + check(err) + + resp, err := client.SubmitTransaction(tx) + check(err) + fmt.Printf("Success: %s", resp.Hash) +} ``` ## Limiting the supply of an asset -:::danger Warning +::::danger Warning + +This section details how to lock your account with the purpose of limiting the supply of your issued asset. However, locking your account means you’ll never be able to do anything with it ever again—whether that’s adjusting signers, changing the home domain, claiming any held XLM, or any other operation. Your account will be completely frozen. + +:::: This section details how to lock your account with the purpose of limiting the supply of your issued asset. However, locking your account means you’ll never be able to do anything with it ever again- whether that’s adjusting signers, changing the home domain, claiming any held XLM, or any other operation. Your account will be completely frozen. -::: +It is possible to lock down the issuer account of an asset so that the asset’s supply cannot increase. To do this, first set the issuer account’s master weight to 0 using the Set Options operation.[^unless-regulated] This prevents the issuer account from being able to sign transactions and therefore, making the issuer unable to issue any more assets. Be sure to do this only after you’ve issued all desired assets to the distributor account. If the asset has a Stellar Asset Contract, also make sure the admin for the contract was not updated from the default (which is the issuer) using the `set_admin` contract call. If the admin was not the issuer, then the admin would be able to mint the asset even with the issuer account locked. + +[^unless-regulated]: If you are issuing regulated assets, make sure to assign `low` signature threshold keys before removing access to `high` treshold master keypair. If you do intend to limit an asset's supply, then the `low` threshold signers should not be able to combine up to a `medium` threshold, which could issue new tokens. Relevantly, assuming `high` > `medium` weight for issuer account threholds, you will not be able to change the `low` threshold signers after locking. With that warning in mind, it is possible to lock down the issuing account of an asset so that the asset’s supply cannot increase. To do this, first set the issuing account’s master weight to 0 using the Set Options operation. This prevents the issuing account from being able to sign transactions and therefore, making the issuer unable to issue any more assets. Be sure to do this only after you’ve issued all desired assets to the distribution account. If the asset has a Stellar Asset Contract, also make sure the admin for the contract was not updated from the default (which is the issuer) using the `set_admin` contract call. If the admin was not the issuer, then the admin would be able to mint the asset even with the issuing account locked. diff --git a/docs/tokens/how-to-issue-an-asset.mdx b/docs/tokens/how-to-issue-an-asset.mdx index f184b07984..21d65c6259 100644 --- a/docs/tokens/how-to-issue-an-asset.mdx +++ b/docs/tokens/how-to-issue-an-asset.mdx @@ -84,7 +84,7 @@ Many users secure their issuing account with cold storage techniques, such as a ### Distribution account keypair -Your asset can be issued and transferred between accounts through a payment, contract, or claimable balance. Although it is not required to create a distribution account, it is best practice, so we will do so in this example. Read more in our [Issuing and Distribution Accounts section](./control-asset-access.mdx#issuing-and-distribution-accounts). +Your asset can be issued and transferred between accounts through a payment, contract, or claimable balance. Although it is not required to create a distribution account, it is best practice, so we will do so in this example. Read more in our [Issuing and Distribution Accounts section](./control-asset-access.mdx#issuer-and-distributor-accounts). #### Three Operations @@ -136,7 +136,7 @@ distributorKeypair := keypair.MustParseFull("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U :::danger -Be careful when working with raw secret keys. If you don't have issuer trustline [clawback](../build/guides/transactions/clawbacks.mdx) enabled, any misstep here could permanently render assets lost. Many users put their first few projects on [testnet](../networks/README.mdx#testnet) or try out [Quests](../learn/interactive/quest.mdx) which provide a low-stakes introductory sandbox. +Be careful when working with raw secret keys. If you don't have issuer trustline [clawback](../build/guides/transactions/clawbacks.mdx) enabled, any misstep here could permanently render assets lost. Many users put their first few projects on [testnet](../networks/README.mdx#network-comparison) or try out [Quests](../learn/interactive/quest.mdx) which provide a low-stakes introductory sandbox. ::: @@ -367,11 +367,11 @@ You can also create a market directly from the issuing account and issue tokens :::danger -This section details how to lock your account with the purpose of limiting the supply of your issued asset. However, locking your account means you’ll never be able to do anything with it ever again—whether that’s adjusting signers, changing the home domain, claiming any held XLM, or any other operation. Your account will be completely frozen. +This section details how to lock your account with the purpose of limiting the supply of your issued asset. However, locking your account means you’ll never be able to do anything with it ever again—whether that’s adjusting signers, changing the home domain, claiming any held XLM, or any other operation. Your account will be completely frozen. ::: -You can permanently configure the exact number of an asset that will ever exist. Learn more about asset supply in our section on [Limiting the Supply of an Asset](./control-asset-access.mdx#limiting-the-supply-of-an-asset) +You can permanently configure the exact number of an asset that will ever exist. Learn more about asset supply in our section on [Limiting the Supply of an Asset](./control-asset-access.mdx#limiting-the-supply-of-an-asset). diff --git a/docs/tokens/stellar-asset-contract.mdx b/docs/tokens/stellar-asset-contract.mdx index 93273bbc69..5e82c5b566 100644 --- a/docs/tokens/stellar-asset-contract.mdx +++ b/docs/tokens/stellar-asset-contract.mdx @@ -5,18 +5,18 @@ description: "Learn to make payments and interact with assets issued on the Stel sidebar_label: Stellar Asset Contract --- -:::info - -The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). - -::: - # Stellar Asset Contract (SAC) The Stellar Asset Contract (SAC) is an implementation of [CAP-46-6 Smart Contract Standardized Asset] and [SEP-41 Token Interface] for Stellar [assets]. See examples of how to use the SAC in the [Tokens How-To Guides](../build/guides/tokens/README.mdx). +:::info + +"Contract token" is the preferred syntax over "custom token," as discussed in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). + +::: + ## Overview :::note diff --git a/docs/tokens/token-interface.mdx b/docs/tokens/token-interface.mdx index 35cf625691..381726397b 100644 --- a/docs/tokens/token-interface.mdx +++ b/docs/tokens/token-interface.mdx @@ -5,12 +5,6 @@ description: "Learn about and create contract tokens on the Stellar network. Lea sidebar_label: Token Interface --- -:::info - -The term "custom token" has been deprecated in favor of "contract token". View the conversation in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). - -::: - # Token Interface Token contracts, including the Stellar Asset Contract and example token implementations expose the following common interface. @@ -19,6 +13,12 @@ Tokens deployed on Soroban can implement any interface they choose, however, the Note, that in the specific cases the interface doesn't have to be fully implemented. For example, the contract token may not implement the administrative interface compatible with the Stellar Asset Contract - it won't stop it from being usable in the contracts that only perform the regular user operations (transfers, allowances, balances etc.). +:::info + +"Contract token" is the preferred syntax over "custom token," as discussed in the [Stellar Developer Discord](https://discord.com/channels/897514728459468821/966788672164855829/1359276952971640953). + +::: + ### Compatibility Requirements For any given contract function, there are three requirements that should be consistent with the interface described here: diff --git a/docs/tools/README.mdx b/docs/tools/README.mdx index 026b61693a..8c43730cc8 100644 --- a/docs/tools/README.mdx +++ b/docs/tools/README.mdx @@ -21,7 +21,7 @@ The command line interface to Soroban smart contracts. It allows you to build, d ### [Lab](./lab/README.mdx) -Stellar Lab is our new go-to tool for development, experimenting, and testing, as well as exploring APIs developers use to interact with the Stellar network. +Stellar Lab is our go-to tool for development, experimenting, and testing, as well as exploring APIs developers use to interact with the Stellar network. Learn more in the [GitHub repository](https://github.com/stellar/laboratory). ### [Quickstart](./quickstart/README.mdx) diff --git a/docs/tools/lab/README.mdx b/docs/tools/lab/README.mdx index b834d05d03..daf8a134a0 100644 --- a/docs/tools/lab/README.mdx +++ b/docs/tools/lab/README.mdx @@ -5,16 +5,20 @@ sidebar_label: Lab sidebar_position: 30 --- +import Link from "@docusaurus/Link"; + # Lab ### [Stellar Lab](https://lab.stellar.org) -Stellar Lab is our new go-to tool for development, experimenting, and testing, as well as exploring APIs developers use to interact with the Stellar network. Whether you're a developer seeking to test transactions, explore RPC methods or Horizon endpoints, or dive deeper into the ecosystem, Stellar Lab provides a modern and user-friendly interface that makes the process smooth and intuitive. +Stellar Lab is our go-to tool for development, experimenting, and testing, as well as exploring APIs developers use to interact with the Stellar network. Whether you're a developer seeking to test transactions, explore RPC methods or Horizon endpoints, or dive deeper into the ecosystem, Stellar Lab provides a modern and user-friendly interface that makes the process smooth and intuitive. ![Lab: Homepage](/assets/lab/lab.png) ### Features of Stellar Lab +These are the features that are available now. There will be more upcoming features to support smart contracts. We look forward to showing you the future of Stellar Lab. + - Easily Create Accounts: Create Accounts on Mainnet, Testnet, and Futurenet using a web UI. You can use Friendbot to fund those accounts directly on Lab for Testnet and Futurenet. - Access RPC Methods and Horizon Endpoints: Leverage powerful Stellar RPC methods and Stellar Horizon endpoints in a web UI to interact with the Stellar network and obtain crucial data. Try RPC methods to get states from the ledger like accounts, trustlines, contract wasm, and more. - Simulate and Submit: Lab supports the ability to use custom RPC providers so that you could simulate transactions, save transactions, and submit transactions directly using Lab. You can also submit transactions using Horizon. @@ -24,16 +28,21 @@ Stellar Lab is our new go-to tool for development, experimenting, and testing, a ![Lab XDR to JSON](/assets/lab/xdr-json-lab.png) -These are the features that are available now. There will be more upcoming features to support smart contracts. We look forward to showing you the future of Stellar Lab. - -### Video Tutorial for Stellar Lab - -- Video Tutorial for Lab from the [2024-11-14 developers meeting](/meetings/2024/11/14) - -### What About the Old Lab? - -In case you need it, the previous version of Stellar Lab is still accessible [here](https://old-lab.stellar.org). However, it will no longer be actively maintained. We encourage you to explore the new Lab and if you think there is anything that’s missing, please reach out to us on the [Stellar Github](https://github.com/stellar/laboratory/issues). + + Video Tutorial + ### Help Us Improve! -We’re committed to making Stellar Lab even better. If you have any feature requests, please submit them on [Github](https://github.com/stellar/laboratory/issues). Your feedback is important to us with product iterations and in shaping the future of Stellar Lab. +We’re committed to making Stellar Lab even better. If you have any feature requests, please submit them in a [GitHub issue](https://github.com/stellar/laboratory/issues). Your feedback is important to us with product iterations and in shaping the future of Stellar Lab. diff --git a/docs/tools/lab/api-explorer/horizon-endpoint.mdx b/docs/tools/lab/api-explorer/horizon-endpoint.mdx index 5cf0d7c74b..08ce881c8d 100644 --- a/docs/tools/lab/api-explorer/horizon-endpoint.mdx +++ b/docs/tools/lab/api-explorer/horizon-endpoint.mdx @@ -267,7 +267,7 @@ Horizon's `/assets` endpoint provides information about an asset. Since we are n ![Lab: Horizon - Assets Page](/assets/lab/horizon-assets.png) -Let's inquire about one of the most popular assets on the Stellar network: [USDC by Circle](https://www.circle.com/usdc). To do this, enter `USDC` as the Asset Code and `GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` as the Asset Issuer. +Let's inquire about one of the most popular assets on the Stellar network: Circle's USDC. To do this, enter `USDC` as the Asset Code and `GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` as the Asset Issuer. ![Lab: Horizon - Assets Page](/assets/lab/horizon-assets-usdc.png) diff --git a/docs/tools/openzeppelin-contracts.mdx b/docs/tools/openzeppelin-contracts.mdx index ac2cb80ed2..641c50339f 100644 --- a/docs/tools/openzeppelin-contracts.mdx +++ b/docs/tools/openzeppelin-contracts.mdx @@ -5,6 +5,8 @@ sidebar_label: OpenZeppelin Contracts and Toolings sidebar_position: 44 --- +import YouTube from "@site/src/components/YouTube"; + To bring battle-tested smart contracts to the Stellar developer community, OpenZeppelin is actively contributing towards a library of smart contracts and extensions, as well as developer tooling (including products such as [Contract Wizard](https://wizard.openzeppelin.com/stellar), [Relayer](https://github.com/OpenZeppelin/openzeppelin-relayer), [Monitor](https://github.com/OpenZeppelin/openzeppelin-monitor), and [UI Builder](https://builder.openzeppelin.com/)). For latest docs on OpenZeppelin products, please visit: [https://docs.openzeppelin.com/stellar-contracts](https://docs.openzeppelin.com/stellar-contracts/.). @@ -17,16 +19,7 @@ To bring battle-tested smart contracts to the Stellar developer community, OpenZ For a walkthrough on using these contracts check out the video linked below! - + ## OpenZeppelin Stellar Contracts and Utilities diff --git a/docs/validators/README.mdx b/docs/validators/README.mdx index b2c709339b..5169dc3a6b 100644 --- a/docs/validators/README.mdx +++ b/docs/validators/README.mdx @@ -5,9 +5,13 @@ description: "Learn how validators support network security and consensus. Under sidebar_position: 10 --- +import YouTube from "@site/src/components/YouTube"; + # Validators Introduction -Stellar is a peer-to-peer network made up of nodes, which are computers that keep a common distributed [ledger](../learn/fundamentals/stellar-data-structures/ledgers.mdx), and that communicate to validate and add [transactions](../learn/fundamentals/transactions/operations-and-transactions.mdx) to it. Nodes use a program called Stellar Core — an implementation of the [Stellar Consensus Protocol](../learn/fundamentals/stellar-consensus-protocol.mdx) — to stay in sync as they work to agree on the validity of transaction sets and to apply them to the ledger. Generally, nodes reach consensus, apply a transaction set, and update the ledger every 3-5 seconds. + + +Stellar is a peer-to-peer network made up of nodes, which are computers that keep a common distributed [ledger](../learn/fundamentals/stellar-data-structures/ledgers/README.mdx), and that communicate to validate and add [transactions](../learn/fundamentals/transactions/operations-and-transactions.mdx) to it. Nodes use a program called Stellar Core — an implementation of the [Stellar Consensus Protocol](../learn/fundamentals/stellar-consensus-protocol.mdx) — to stay in sync as they work to agree on the validity of transaction sets and to apply them to the ledger. Generally, nodes reach consensus, apply a transaction set, and update the ledger every 3-5 seconds. This section of the docs explains how to run a validator node, which participates in consensus to validate transactions and determine network settings. A validator node _should not_ be used for network data access and transaction submission. There are two varieties of _non-validating_ nodes that can be used for those purposes, each of which has its own process for set up, interaction, maintenance, and monitoring. They are: diff --git a/i18n/es/docusaurus-plugin-content-docs/current/build/guides/basics/create-account.mdx b/i18n/es/docusaurus-plugin-content-docs/current/build/guides/basics/create-account.mdx index bf39b09499..383c9da0ad 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/build/guides/basics/create-account.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/build/guides/basics/create-account.mdx @@ -51,7 +51,7 @@ package main import ( "log" - "github.com/stellar/go/keypair" + "github.com/stellar/go-stellar-sdk/keypair" ) func main() { @@ -255,7 +255,7 @@ package main import ( "log" - "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go-stellar-sdk/clients/horizonclient" ) func main() { diff --git a/i18n/es/docusaurus-plugin-content-docs/current/build/guides/basics/send-and-receive-payments.mdx b/i18n/es/docusaurus-plugin-content-docs/current/build/guides/basics/send-and-receive-payments.mdx index 61f36d383a..ebd65a4703 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/build/guides/basics/send-and-receive-payments.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/build/guides/basics/send-and-receive-payments.mdx @@ -275,10 +275,10 @@ try { package main import ( - "github.com/stellar/go/keypair" - "github.com/stellar/go/network" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/clients/horizonclient" + "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/clients/horizonclient" "fmt" ) @@ -827,8 +827,8 @@ import ( "fmt" "time" - "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go-stellar-sdk/clients/horizonclient" + "github.com/stellar/go-stellar-sdk/protocols/horizon/operations" ) func main() { diff --git a/i18n/es/docusaurus-plugin-content-docs/current/data/analytics/hubble/data-catalog/data-dictionary/history-operations.mdx b/i18n/es/docusaurus-plugin-content-docs/current/data/analytics/hubble/data-catalog/data-dictionary/history-operations.mdx index 4c5bb96790..31bdb8d035 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/data/analytics/hubble/data-catalog/data-dictionary/history-operations.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/data/analytics/hubble/data-catalog/data-dictionary/history-operations.mdx @@ -148,7 +148,7 @@ description: "" | batch_insert_ts | La marca de tiempo en UTC cuando se insertó un lote de registros en la base de datos. Este campo puede ayudar a identificar si un lote se ejecutó en tiempo real o como parte de un backfill | timestamp | | Sí | | | closed_at | Marca de tiempo en UTC cuando este ledger se cerró y se comprometió a la red. Se espera que los ledgers se cierren aproximadamente cada 5 segundos | timestamp | | Sí | Estamos buscando reorganizar esta tabla por closed_at | | operation_result_code | El código de resultado devuelto cuando se aplica una operación. Este código es útil para entender operaciones fallidas | cadena |
  • OperationResultCodeOpInner
  • OperationResultCodeOpBadAuth
  • OperationResultCodeOpNoAccount
  • OperationResultCodeOpNotSupported
  • OperationResultCodeOpTooManySubentries
  • OperationResultCodeOpExceededWorkLimit
  • OperationResultCodeOpTooManySponsoring
| Sí | El campo se llenará en una fecha futura | -| operation_trace_code | El código de traza devuelto cuando se aplica una operación a la red Stellar. Este código es útil para comprender fallos matizados por tipo de operación. Este código proporciona el detalle más bajo sobre por qué falla una transacción | cadena |
  • InvokeHostFunctionResultCodeInvokeHostFunctionSuccess
  • Malformed
  • Trapped
  • ResourceLimitExceeded
  • EntryArchived
  • InsufficientRefundableFee
  • ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess
  • Malformed
  • ResourceLimitExceeded
  • InsufficientRefundableFee
  • RestoreFootprintResultCodeRestoreFootprintSuccess
  • Malformed
  • ResourceLimitExceeded
  • InsufficientRefundableFee
| Sí | Consulta la [documentación](https://pkg.go.dev/github.com/stellar/go/xdr#OperationResultTr) de XDR para más detalles | +| operation_trace_code | El código de traza devuelto cuando se aplica una operación a la red Stellar. Este código es útil para comprender fallos matizados por tipo de operación. Este código proporciona el detalle más bajo sobre por qué falla una transacción | cadena |
  • InvokeHostFunctionResultCodeInvokeHostFunctionSuccess
  • Malformed
  • Trapped
  • ResourceLimitExceeded
  • EntryArchived
  • InsufficientRefundableFee
  • ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess
  • Malformed
  • ResourceLimitExceeded
  • InsufficientRefundableFee
  • RestoreFootprintResultCodeRestoreFootprintSuccess
  • Malformed
  • ResourceLimitExceeded
  • InsufficientRefundableFee
| Sí | Consulta la [documentación](https://pkg.go.dev/github.com/stellar/go-stellar-sdk/xdr#OperationResultTr) de XDR para más detalles | | detalles_json | Registro que contiene detalles basados en el tipo de operación ejecutada. Cada operación devolverá sus propios detalles relevantes, y el resto de los detalles como nulos | json | | | | diff --git a/i18n/es/docusaurus-plugin-content-docs/current/data/analytics/hubble/data-catalog/data-dictionary/history-transactions.mdx b/i18n/es/docusaurus-plugin-content-docs/current/data/analytics/hubble/data-catalog/data-dictionary/history-transactions.mdx index f3032bbf0c..919c7422fc 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/data/analytics/hubble/data-catalog/data-dictionary/history-transactions.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/data/analytics/hubble/data-catalog/data-dictionary/history-transactions.mdx @@ -54,7 +54,7 @@ description: "" | bytes_leídos_recursos_soroban | Número de bytes leídos por la transacción Soroban | número entero | | No | | | bytes_escritos_recursos_soroban | Número de bytes escritos por la transacción Soroban | número entero | | No | | | cerrado_en | Marca de tiempo en UTC cuando este ledger se cerró y se comprometió a la red. Se espera que los ledgers se cierren aproximadamente cada 5 segundos | marca_de_tiempo | | Sí | Tenemos como objetivo reorganizar la tabla por cerrado_en | -| código_resultado_transacción | El código de resultado detallado que describe por qué falló una transacción. Este código solo es útil para transacciones fallidas. La lista completa de valores de dominio se puede encontrar [aquí](https://pkg.go.dev/github.com/stellar/go/xdr#TransactionResultCode) | cadena |
  • CódigoDeResultadoDeTransacciónTxIncrementoDeTarifaInternaÉxito
  • CódigoDeResultadoDeTransacciónTxÉxito
  • CódigoDeResultadoDeTransacciónTxFallido
  • CódigoDeResultadoDeTransacciónTxDemasiadoTemprano
  • CódigoDeResultadoDeTransacciónTxDemasiadoTarde
  • CódigoDeResultadoDeTransacciónTxOperaciónFaltante
  • CódigoDeResultadoDeTransacciónTxMalaSecuencia
  • CódigoDeResultadoDeTransacciónTxMalaAutenticación
  • CódigoDeResultadoDeTransacciónTxSaldoInsuficiente
  • CódigoDeResultadoDeTransacciónTxSinCuenta
  • CódigoDeResultadoDeTransacciónTxTarifaInsuficiente
  • CódigoDeResultadoDeTransacciónTxMalaAutenticaciónExtra
  • CódigoDeResultadoDeTransacciónTxErrorInterno
  • CódigoDeResultadoDeTransacciónTxNoSoportado
  • CódigoDeResultadoDeTransacciónTxIncrementoDeTarifaInternaFallido
  • CódigoDeResultadoDeTransacciónTxMalaSponsorización
  • CódigoDeResultadoDeTransacciónTxMalaEdadDeSecuenciaMínimaOIntervalo
  • CódigoDeResultadoDeTransacciónTxMalformado
  • CódigoDeResultadoDeTransacciónTxSorobanInválido
| Sí | | +| código_resultado_transacción | El código de resultado detallado que describe por qué falló una transacción. Este código solo es útil para transacciones fallidas. La lista completa de valores de dominio se puede encontrar [aquí](https://pkg.go.dev/github.com/stellar/go-stellar-sdk/xdr#TransactionResultCode) | cadena |
  • CódigoDeResultadoDeTransacciónTxIncrementoDeTarifaInternaÉxito
  • CódigoDeResultadoDeTransacciónTxÉxito
  • CódigoDeResultadoDeTransacciónTxFallido
  • CódigoDeResultadoDeTransacciónTxDemasiadoTemprano
  • CódigoDeResultadoDeTransacciónTxDemasiadoTarde
  • CódigoDeResultadoDeTransacciónTxOperaciónFaltante
  • CódigoDeResultadoDeTransacciónTxMalaSecuencia
  • CódigoDeResultadoDeTransacciónTxMalaAutenticación
  • CódigoDeResultadoDeTransacciónTxSaldoInsuficiente
  • CódigoDeResultadoDeTransacciónTxSinCuenta
  • CódigoDeResultadoDeTransacciónTxTarifaInsuficiente
  • CódigoDeResultadoDeTransacciónTxMalaAutenticaciónExtra
  • CódigoDeResultadoDeTransacciónTxErrorInterno
  • CódigoDeResultadoDeTransacciónTxNoSoportado
  • CódigoDeResultadoDeTransacciónTxIncrementoDeTarifaInternaFallido
  • CódigoDeResultadoDeTransacciónTxMalaSponsorización
  • CódigoDeResultadoDeTransacciónTxMalaEdadDeSecuenciaMínimaOIntervalo
  • CódigoDeResultadoDeTransacciónTxMalformado
  • CódigoDeResultadoDeTransacciónTxSorobanInválido
| Sí | | | oferta_tasa_inclusión | La oferta máxima que el presentador está dispuesto a pagar por la inclusión de la transacción. Esta tarifa se utiliza para priorizar las transacciones que se incluyen en el ledger. | número entero | | No | | | tarifa_inclusión_cobrada | La tarifa cobrada por la transacción para ser incluida en el ledger. Esta es una tarifa fija para todo el ledger y comienza con un mínimo de 100 stroops. La tarifa aumenta según la demanda | número entero | | No | | | reembolso_tarifa_recurso | La cantidad del reembolso de la tarifa del recurso al presentador de la transacción. Las tarifas reembolsables se calculan a partir del alquiler, eventos y valor de retorno. Las tarifas reembolsables se cobran de la cuenta origen antes de que se ejecute la transacción y luego se reembolsan según el uso real. | número entero | | No | | diff --git a/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/network-configuration/federation.mdx b/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/network-configuration/federation.mdx index c4770d054f..1783c895d6 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/network-configuration/federation.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/network-configuration/federation.mdx @@ -29,7 +29,7 @@ Por ejemplo: `FEDERATION_SERVER="https://api.tudominio.com/federation"` Una vez que hayas publicado la ubicación de tu servidor de federación, implementa un endpoint HTTP de federación que acepte una solicitud HTTP GET y emita respuestas de la forma detallada a continuación: -Para facilitar la configuración de un servidor de federación, puedes usar la [implementación de referencia](https://github.com/stellar/go/tree/master/services/federation) diseñada para ser integrada en tu infraestructura existente. +Para facilitar la configuración de un servidor de federación, puedes usar la [implementación de referencia](https://github.com/stellar/go-stellar-sdk/tree/master/services/federation) diseñada para ser integrada en tu infraestructura existente. ## Solicitudes de federación diff --git a/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/transactions-specialized/claimable-balances.mdx b/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/transactions-specialized/claimable-balances.mdx index e1bf787616..18f0a7d94e 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/transactions-specialized/claimable-balances.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/transactions-specialized/claimable-balances.mdx @@ -130,10 +130,10 @@ 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" + sdk "github.com/stellar/go-stellar-sdk/clients/horizonclient" + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/network" + "github.com/stellar/go-stellar-sdk/txnbuild" ) diff --git a/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/transactions-specialized/sponsored-reserves.mdx b/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/transactions-specialized/sponsored-reserves.mdx index fe17f0b93f..34f8a82456 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/transactions-specialized/sponsored-reserves.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/learn/encyclopedia/transactions-specialized/sponsored-reserves.mdx @@ -128,11 +128,11 @@ import ( "fmt" "net/http" - sdk "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/keypair" - "github.com/stellar/go/network" - protocol "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/txnbuild" + sdk "github.com/stellar/go-stellar-sdk/clients/horizonclient" + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/network" + protocol "github.com/stellar/go-stellar-sdk/protocols/horizon" + "github.com/stellar/go-stellar-sdk/txnbuild" ) func main() { diff --git a/i18n/es/docusaurus-plugin-content-docs/current/tokens/how-to-issue-an-asset.mdx b/i18n/es/docusaurus-plugin-content-docs/current/tokens/how-to-issue-an-asset.mdx index 38503661f5..2279f1a842 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/tokens/how-to-issue-an-asset.mdx +++ b/i18n/es/docusaurus-plugin-content-docs/current/tokens/how-to-issue-an-asset.mdx @@ -136,7 +136,7 @@ distributorKeypair := keypair.MustParseFull("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U :::danger -Ten cuidado al trabajar con claves secretas en bruto. Si no tienes habilitado el [clawback](../build/guides/transactions/clawbacks.mdx) de trustline del emisor, cualquier error aquí podría ocasionar la pérdida permanente de activos. Muchos usuarios colocan sus primeros proyectos en la [testnet](../networks/README.mdx#testnet) o prueban las [Misiones](../learn/interactive/quest.mdx) que ofrecen un entorno introductorio con bajo riesgo. +Ten cuidado al trabajar con claves secretas en bruto. Si no tienes habilitado el [clawback](../build/guides/transactions/clawbacks.mdx) de trustline del emisor, cualquier error aquí podría ocasionar la pérdida permanente de activos. Muchos usuarios colocan sus primeros proyectos en la [testnet](../networks/README.mdx#network-comparison) o prueban las [Misiones](../learn/interactive/quest.mdx) que ofrecen un entorno introductorio con bajo riesgo. ::: diff --git a/meeting-notes/2025-01-23.mdx b/meeting-notes/2025-01-23.mdx index ae36d5a326..5ac70c2d02 100644 --- a/meeting-notes/2025-01-23.mdx +++ b/meeting-notes/2025-01-23.mdx @@ -18,7 +18,7 @@ In this week's meeting Hoops Finance's founders Bastian and Tim talk about the p Recording from a protocol meeting where two Core Advancement Proposals - -CAP-0062 (Soroban Live State Prioritization) and CAP-0066 (Soroban In-memory Read Resource) where discussed. +CAP-0062 (Soroban Live State Prioritization) and CAP-0066 (Soroban In-memory Read Resource) were discussed. Here's are some resources to read up on: diff --git a/meeting-notes/2025-01-30.mdx b/meeting-notes/2025-01-30.mdx index c7200899ba..0b40f7ea6a 100644 --- a/meeting-notes/2025-01-30.mdx +++ b/meeting-notes/2025-01-30.mdx @@ -18,7 +18,7 @@ Visit their website here: https://rampmedaddy.com Part 2 -In this protocol meeting two Core Advancement Proposals are discussed - Dima will be presenting CAP-0064 (Memo Authorization for Soroban), and Graydon will be presenting CAP-0065 (Reusable Module Cache). +In this protocol meeting two Core Advancement Proposals are discussed - Dima presented CAP-0064 (Memo Authorization for Soroban), and Graydon presented CAP-0065 (Reusable Module Cache). Here are some resources: diff --git a/meeting-notes/2025-05-22.mdx b/meeting-notes/2025-05-22.mdx index 1cb168aa01..c2f23fcb58 100644 --- a/meeting-notes/2025-05-22.mdx +++ b/meeting-notes/2025-05-22.mdx @@ -4,11 +4,8 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + At this Stellar Developer Meeting we are having a chat with Bram Hoogenkamp from OpenZeppelin about the OpenZeppelin Monitor - providing monitoring and alerting for smart contracts. diff --git a/meeting-notes/2025-07-10.mdx b/meeting-notes/2025-07-10.mdx index e50f55704f..c8ecdc0f65 100644 --- a/meeting-notes/2025-07-10.mdx +++ b/meeting-notes/2025-07-10.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this call we are getting an update on the exciting Scaffold Stellar project by Fifo from Aha Labs. diff --git a/meeting-notes/2025-07-17.mdx b/meeting-notes/2025-07-17.mdx index 1c66005564..9fd78e0ee1 100644 --- a/meeting-notes/2025-07-17.mdx +++ b/meeting-notes/2025-07-17.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this meeting the Nomyx team will give us an introduction to their advanced smart contract architecture (an implementation of the Diamond Pattern) and the Nomyx Diamond proxy viewer interface, that facilitates inspection of deployed diamonds and makes them a bit easier to work with. diff --git a/meeting-notes/2025-07-24.mdx b/meeting-notes/2025-07-24.mdx index f7ee6cd4a4..69cbe9c927 100644 --- a/meeting-notes/2025-07-24.mdx +++ b/meeting-notes/2025-07-24.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this meeting we are talking to Esteban and Francisco from PaltaLabs. Stellar Hacks is currently running a hackathon in collaboration with PaltaLabs, where the goal is to build using their projects Soroswap and DeFindex. diff --git a/meeting-notes/2025-08-07.mdx b/meeting-notes/2025-08-07.mdx index ac3cf30ef5..40a3bcfb44 100644 --- a/meeting-notes/2025-08-07.mdx +++ b/meeting-notes/2025-08-07.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this meeting we talk to Flashback founder Brieuc Berruet, who participated in the DraperU x Stellar incubator program last year, to get an update on his project (spoiler alert - it's live!). diff --git a/meeting-notes/2025-09-25.mdx b/meeting-notes/2025-09-25.mdx index aaafdce411..03e1d8ac6d 100644 --- a/meeting-notes/2025-09-25.mdx +++ b/meeting-notes/2025-09-25.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this week’s Stellar Developer Meeting we discuss new Core Advancement Proposals. diff --git a/meeting-notes/2025-10-02.mdx b/meeting-notes/2025-10-02.mdx index 32577fac55..761d88a5b4 100644 --- a/meeting-notes/2025-10-02.mdx +++ b/meeting-notes/2025-10-02.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this meeting we will discuss CAP-74, BN254 and Poseidon hash functions. diff --git a/meeting-notes/2025-10-09.mdx b/meeting-notes/2025-10-09.mdx index bfede2756a..51df60ab29 100644 --- a/meeting-notes/2025-10-09.mdx +++ b/meeting-notes/2025-10-09.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + ## OpenZeppelin UI Builder demo @@ -19,12 +16,7 @@ This time Steve will demo UI Builder, an easy way to spin up a front-end for any [OpenZeppelin UI Builder](https://builder.openzeppelin.com) - + ## Protocol Discussion diff --git a/meeting-notes/2025-10-16.mdx b/meeting-notes/2025-10-16.mdx index 7e9032cc0a..7bea1e6fd6 100644 --- a/meeting-notes/2025-10-16.mdx +++ b/meeting-notes/2025-10-16.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + ## Protocol Discussion diff --git a/meeting-notes/2025-10-23.mdx b/meeting-notes/2025-10-23.mdx index 9eeb1c6959..3f14427c0c 100644 --- a/meeting-notes/2025-10-23.mdx +++ b/meeting-notes/2025-10-23.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this meeting we are talking to Dobprotocol to learn more about how they use Stellar to build a unique platform, turning machines into investable assets. diff --git a/meeting-notes/2025-10-30.mdx b/meeting-notes/2025-10-30.mdx index 1893aab9e5..561836fe06 100644 --- a/meeting-notes/2025-10-30.mdx +++ b/meeting-notes/2025-10-30.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this meeting we are continuing our mini series about the Stellar-based open source tooling OpenZeppelin is developing. In the last session we talked briefly about Relayer and this time we are diving deeper into this tool, and OpenZeppelin Managed Service. diff --git a/meeting-notes/2025-11-06.mdx b/meeting-notes/2025-11-06.mdx index 54058a45ae..aa6cc2e3e1 100644 --- a/meeting-notes/2025-11-06.mdx +++ b/meeting-notes/2025-11-06.mdx @@ -4,12 +4,9 @@ authors: carstenjacobsen tags: [developer] --- - +import YouTube from "@site/src/components/YouTube"; + + In this meeting we do a walkthrough of OpenZeppelin’s Q3 library releases for Smart Account, Vault and RWA. diff --git a/nginx/includes/redirects.conf b/nginx/includes/redirects.conf index f01e71506d..3414782353 100644 --- a/nginx/includes/redirects.conf +++ b/nginx/includes/redirects.conf @@ -152,7 +152,7 @@ rewrite ^/docs/learn/encyclopedia/fee-bump-transactions$ "/docs/learn/encycloped rewrite ^/docs/learn/encyclopedia/channel-accounts$ "/docs/learn/encyclopedia/transactions-specialized/channel-accounts" permanent; rewrite ^/docs/learn/encyclopedia/contract-development/fees-and-metering "/docs/learn/fundamentals/fees-resource-limits-metering" permanent; rewrite ^/docs/learn/encyclopedia/inflation$ "/docs/learn/encyclopedia/network-configuration/inflation" permanent; -rewrite ^/docs/learn/encyclopedia/ledger-headers$ "/docs/learn/encyclopedia/network-configuration/ledger-headers" permanent; +rewrite ^/docs/learn/encyclopedia/ledger-headers$ "/docs/learn/fundamentals/stellar-data-structures/ledgers/headers" permanent; rewrite ^/docs/learn/encyclopedia/liquidity-on-stellar-sdex-liquidity-pools$ "/docs/learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools" permanent; rewrite ^/docs/learn/encyclopedia/sdex$ "/docs/learn/fundamentals/liquidity-on-stellar-sdex-liquidity-pools" permanent; rewrite ^/docs/learn/encyclopedia/lumen-supply-metrics$ "/docs/learn/fundamentals/lumens#lumen-supply-metrics" permanent; diff --git a/openapi/horizon/components/endpoints/paths.yml b/openapi/horizon/components/endpoints/paths.yml index b0e83d08f9..a1fac29b13 100644 --- a/openapi/horizon/components/endpoints/paths.yml +++ b/openapi/horizon/components/endpoints/paths.yml @@ -5,9 +5,11 @@ paths: - Paths summary: List Strict Receive Payment Paths description: | - The strict receive payment path endpoint lists the paths a payment can take based on the amount of an asset you want the recipient to receive. The destination asset amount stays constant, and the type and amount of an asset sent varies based on offers in the order books. + The strict-receive payment-path endpoint lists the paths a payment can take based on the amount of an asset you want the recipient to receive. The destination asset amount stays constant, and the type and amount of an asset sent varies based on offers in the order books. For this search, Horizon loads a list of assets available to the sender (based on `source_account` or `source_assets`) and displays the possible paths from the different source assets to the destination asset. Only paths that satisfy the `destination_amount` are returned. + + Learn more about this operation: [Path Payment Strict Receive](/docs/learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-receive) operationId: ListStrictReceivePaymentPaths parameters: - $ref: '../parameters.yml#/components/parameters/SourceAccountParam' @@ -92,6 +94,8 @@ paths: The strict send payment path endpoint lists the paths a payment can take based on the amount of an asset you want to send. The source asset amount stays constant, and the type and amount of an asset received varies based on offers in the order books. For this search, Horizon loads a list of assets that the recipient can receive (based on `destination_account` or `destination_assets`) and displays the possible paths from the different source assets to the destination asset. Only paths that satisfy the `source_amount` are returned. + + Learn more about this operation: [Path Payment Strict Send](/docs/learn/fundamentals/transactions/list-of-operations.mdx#path-payment-strict-send) operationId: ListStrictSendPaymentPaths parameters: - $ref: '../parameters.yml#/components/parameters/SourceAssetTypeParam' diff --git a/openrpc/src/anchor-platform/methods/do_stellar_payment.json b/openrpc/src/anchor-platform/methods/do_stellar_payment.json index 4fef7e64cb..cf12887ed9 100644 --- a/openrpc/src/anchor-platform/methods/do_stellar_payment.json +++ b/openrpc/src/anchor-platform/methods/do_stellar_payment.json @@ -1,7 +1,7 @@ { "name": "do_stellar_payment", "summary": "Submits a Stellar payment", - "description": "Submits a payment to a stellar network by a custody service.", + "description": "Submits a payment to the Stellar network by a custody service.", "paramStructure": "by-name", "tags": [ { "name": "SEP-6" }, diff --git a/routes.txt b/routes.txt index 1e45f02bbf..5988afa41a 100644 --- a/routes.txt +++ b/routes.txt @@ -508,6 +508,8 @@ /docs/learn/fundamentals/stellar-data-structures/contracts /docs/learn/fundamentals/stellar-data-structures/events /docs/learn/fundamentals/stellar-data-structures/ledgers +/docs/learn/fundamentals/stellar-data-structures/ledgers/headers +/docs/learn/fundamentals/stellar-data-structures/ledgers/entries /docs/learn/fundamentals/stellar-ecosystem-proposals /docs/learn/fundamentals/stellar-stack /docs/learn/fundamentals/transactions diff --git a/src/components/CodeExample.js b/src/components/CodeExample.js index 47ea63b7b7..3aec534143 100644 --- a/src/components/CodeExample.js +++ b/src/components/CodeExample.js @@ -10,9 +10,7 @@ export const CodeExample = ({ children }) => ( {React.Children.map(children, (child, index) => { const codeProps = child.props.children.props; const { className = '' } = codeProps; - const [, language] = className.split('-'); - return ( ( -
- -
-); - -YouTube.propTypes = { - ID: PropTypes.string.isRequired, -}; - -export default YouTube; \ No newline at end of file diff --git a/src/components/YouTube.tsx b/src/components/YouTube.tsx new file mode 100644 index 0000000000..390c41994c --- /dev/null +++ b/src/components/YouTube.tsx @@ -0,0 +1,37 @@ +import React, { type CSSProperties } from "react"; + +type YouTubeVid = { + ID: string; +}; + +const containerStyle: CSSProperties = { + position: "relative", + width: "100%", + paddingBottom: "56.25%", // Maintain 16:9 ratio + height: 0, + marginBottom: "23px", +}; + +const iframeStyle: CSSProperties = { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + borderRadius: "25pt", +}; + +const YouTube = ({ ID }: YouTubeVid): JSX.Element => ( +
+