diff --git a/pages/guides/fetch-network/cosmpy/_meta.json b/pages/guides/fetch-network/cosmpy/_meta.json index d1ed0a6aa..47f65e15b 100644 --- a/pages/guides/fetch-network/cosmpy/_meta.json +++ b/pages/guides/fetch-network/cosmpy/_meta.json @@ -4,5 +4,7 @@ "querying-address-balances": "Querying Balances \uD83D\uDD0D", "creating-wallet": "Wallets and Private Keys \uD83D\uDCB8\uD83D\uDD10", "transaction-broadcast": "Sending funds \uD83D\uDCB8", - "staking": "Staking \uD83D\uDCB0" + "staking": "Staking \uD83D\uDCB0", + "smart-contracts": "Smart contracts", + "use-cases": "Use cases" } diff --git a/pages/guides/fetch-network/cosmpy/smart-contracts.mdx b/pages/guides/fetch-network/cosmpy/smart-contracts.mdx new file mode 100644 index 000000000..c5d060f98 --- /dev/null +++ b/pages/guides/fetch-network/cosmpy/smart-contracts.mdx @@ -0,0 +1,67 @@ +# Smart contracts + +## Deploy a smart contract + +CosmPy offers the possibility to easily deploy **smart contracts** using `LedgerContract` object. For this, you will need the **path** to where the contract is stored (in this case `simple.wasm`), a [`LedgerClient` ↗️](/guides/fetch-network/cosmpy/establishing-node-connection) and a [Wallet ↗️](/guides/fetch-network/cosmpy/creating-wallet): + +```py copy +from cosmpy.aerial.contract import LedgerContract + +PATH = "contracts/simple/simple.wasm" + +contract = LedgerContract(PATH, ledger_client) +contract.deploy({}, wallet) +``` + +## Interact with smart contracts + +We can now start interacting with any smart contract in different ways by executing one of the following operations. + +### Retrieve contract address + +After deployment, you can obtain the address of the deployed contract on the network using the `address` method in the following way: + +```py copy +print(f"Contract deployed at: {contract.address}") +``` + +### Query contract's state variables + +We can also query the values of the contract's state variables using the `query` method and provide a dictionary specifying the query information: + +```py copy +result = contract.query({"get": {"owner": wallet}}) +print("Initial state:", result) +``` + +### Set contract's state variables + +We can update the contract's state variables by using the `execute` method and by providing a dictionary specifying the update wanted. Use the wait_to_complete() method to wait for the execution to finish. + +The following code sets the state variable `value` to `foobar`: + +```py copy +contract.execute({"set": {"value": "foobar"}}, wallet).wait_to_complete() +``` + +Once you set a state variable to its updated value, you can query it again to confirm such changes took place effectively. Considering the example above, we can check if changes were set correctly in the following way: + +```py copy +result = contract.query({"get": {"owner": wallet)}}) +print("State after set:", result) +``` + +### Clear state variables + +We can clear the contract's state variables using the `execute` method in the following way: + +```py copy +contract.execute({"clear": {}}, wallet).wait_to_complete() +``` + +And check if operations went smoothly by running: + +```py copy +result = contract.query({"get": {"owner": wallet}}) +print("State after clear:", result) +``` \ No newline at end of file diff --git a/pages/guides/fetch-network/cosmpy/transaction-broadcast.mdx b/pages/guides/fetch-network/cosmpy/transaction-broadcast.mdx index de7b66195..2d1ca7507 100644 --- a/pages/guides/fetch-network/cosmpy/transaction-broadcast.mdx +++ b/pages/guides/fetch-network/cosmpy/transaction-broadcast.mdx @@ -2,7 +2,7 @@ CosmPy simplifies broadcasting transactions. After [creating a wallet ↗️](/guides/fetch-network/cosmpy/creating-wallet.mdx) you can send transactions by following the provided example. In it we first connect to the desired network then provide the desired destination address. The transaction is then broadcast using the `Ledger_client.send_tokens` function: -```py copy +```py copy from cosmpy.aerial.client import LedgerClient, NetworkConfig # Establishing connection to the network diff --git a/pages/guides/fetch-network/cosmpy/use-cases/_meta.json b/pages/guides/fetch-network/cosmpy/use-cases/_meta.json new file mode 100644 index 000000000..1740f1295 --- /dev/null +++ b/pages/guides/fetch-network/cosmpy/use-cases/_meta.json @@ -0,0 +1,8 @@ +{ + "stake-auto-compounder": "Stake auto-compounder️", + "stake-optimizer": "Stake optimizer️", + "oracles": "Oracles \uD83D\uDD2E", + "wallet-top-up": "Wallet top-up \uD83D\uDCB5", + "liquidity-pool": "Liquidity pool", + "swap_automation": "Swap automation \uD83D\uDD04" +} diff --git a/pages/guides/fetch-network/cosmpy/use-cases/liquidity-pool.mdx b/pages/guides/fetch-network/cosmpy/use-cases/liquidity-pool.mdx new file mode 100644 index 000000000..95abec14f --- /dev/null +++ b/pages/guides/fetch-network/cosmpy/use-cases/liquidity-pool.mdx @@ -0,0 +1,393 @@ +# Liquidity pool + +## Introduction + +This Liquidity pool interaction guide provides a practical demonstration of interacting with a liquidity pool on the Fetch.ai network. This script showcases various operations, including swapping assets, providing liquidity, and withdrawing liquidity, utilizing smart contracts and local wallets. + +## Walk-through + +1. Let's start by creating a Python script for this and name it: `touch aerial_liquidity_pool.py` +2. Let's then import the needed modules: + + ```py copy + import argparse + import base64 + from cosmpy.aerial.client import LedgerClient, NetworkConfig + from cosmpy.aerial.contract import LedgerContract + from cosmpy.aerial.faucet import FaucetApi + from cosmpy.aerial.wallet import LocalWallet + ``` + +3. We need to define a `_parse_commandline()` function: + + ```py copy + def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "swap_amount", + type=int, + nargs="?", + default=10000, + help="atestfet swap amount to get some cw20 tokens on wallet's address", + ) + parser.add_argument( + "cw20_liquidity_amount", + type=int, + nargs="?", + default=100, + help="amount of cw20 tokens that will be provided to LP", + ) + parser.add_argument( + "native_liquidity_amount", + type=int, + nargs="?", + default=2470, + help="amount of atestfet tokens that will be provided to LP", + ) + return parser.parse_args() + ``` + + The function expects and processes three command-line arguments: + + - `swap_amount`: this argument specifies the amount of atestfet tokens to swap in order to receive some cw20 tokens on the wallet's address. It is an optional argument, and if not provided, it defaults to `10000`. + - `cw20_liquidity_amount`: this argument sets the amount of cw20 tokens that will be provided to the liquidity pool. It is also optional and defaults to `100` if not provided. + - `native_liquidity_amount`: this argument represents the amount of atestfet tokens that will be provided to the liquidity pool. Like the others, it is optional and defaults to `2470` if not specified. + + The `help` parameter for each argument provides a description of what it is used for. The function then uses `parser.parse_args()` to process the command-line arguments provided by the user and return them as an object containing the values for `swap_amount`, `cw20_liquidity_amount`, and `native_liquidity_amount`. + +4. We are now ready to define our `main()` function, which orchestrates the interaction with a liquidity pool using the provided command-line arguments. We define it in multiple parts, as follows: + + ```py copy + def main(): + """Run main.""" + args = _parse_commandline() + + # Define any wallet + wallet = LocalWallet.generate() + + # Network configuration + ledger = LedgerClient(NetworkConfig.latest_stable_testnet()) + + # Add tokens to wallet + faucet_api = FaucetApi(NetworkConfig.latest_stable_testnet()) + faucet_api.get_wealth(wallet.address()) + + # Define cw20, pair and liquidity token contracts + token_contract_address = ( + "fetch1qr8ysysnfxmqzu7cu7cq7dsq5g2r0kvkg5e2wl2fnlkqss60hcjsxtljxl" + ) + pair_contract_address = ( + "fetch1vgnx2d46uvyxrg9pc5mktkcvkp4uflyp3j86v68pq4jxdc8j4y0s6ulf2a" + ) + liq_token_contract_address = ( + "fetch1alzhf9yhghud3qhucdjs895f3aek2egfq44qm0mfvahkv4jukx4qd0ltxx" + ) + + token_contract = LedgerContract( + path=None, client=ledger, address=token_contract_address + ) + pair_contract = LedgerContract( + path=None, client=ledger, address=pair_contract_address + ) + liq_token_contract = LedgerContract( + path=None, client=ledger, address=liq_token_contract_address + ) + + print("Pool (initial state): ") + print(pair_contract.query({"pool": {}}), "\n") + ``` + + It starts by calling `_parse_commandline()` to retrieve the command-line arguments. These arguments control various aspects of the liquidity pool interaction, like swap amounts and liquidity provision. We then create new wallet called `wallet`. This wallet will be used for conducting transactions. We proceed and set the network configuration to the latest stable testnet. Through the `faucet_api` we add tokens to the wallet. This simulates the process of acquiring tokens from an external source. We go on and define the contract addresses. In the part, addresses of three different contracts (CW20 token, pair, and liquidity token contracts) are defined. These contracts are essential for interacting with the liquidity pool. Finally we print the initial pool state. This provides an initial snapshot of the liquidity pool before any actions are taken. + + ```py copy + # Swap atestfet for CW20 tokens + swap_amount = str(args.swap_amount) + native_denom = "atestfet" + + tx = pair_contract.execute( + { + "swap": { + "offer_asset": { + "info": {"native_token": {"denom": native_denom}}, + "amount": swap_amount, + } + } + }, + sender=wallet, + funds=swap_amount + native_denom, + ) + + print(f"Swapping {swap_amount + native_denom} for CW20 Tokens...") + tx.wait_to_complete() + + print("Pool (after swap): ") + print(pair_contract.query({"pool": {}}), "\n") + + # To provide cw20 token to LP, increase your allowance first + cw20_liquidity_amount = str(args.cw20_liquidity_amount) + native_liquidity_amount = str(args.native_liquidity_amount) + + tx = token_contract.execute( + { + "increase_allowance": { + "spender": pair_contract_address, + "amount": cw20_liquidity_amount, + "expires": {"never": {}}, + } + }, + wallet, + ) + + print("Increasing Allowance...") + tx.wait_to_complete() + + ``` + + In this part of the main() function, the script swaps a specified amount of atestfet tokens for CW20 tokens using the `pair_contract`. This is done by constructing a transaction with the `"swap"` operation. `swap_amount` is the amount of atestfet tokens to swap, retrieved from the command-line arguments. `native_denom` is set to `"atestfet"` which is the native token denomination. The transaction is executed with the `execute()` method, specifying the `"swap"` operation. The `sender` parameter is set to the user's `wallet`, and the `funds` parameter is set to the amount being swapped in addition to the native denomination. The script then waits for the transaction to complete, and after this, a message is printed to indicate the swap operation has occurred. Within the function, we then provide CW20 tokens to the liquidity pool. The script first increases the allowance for the pair contract to spend CW20 tokens from the user's wallet. The `cw20_liquidity_amount` is the amount of CW20 tokens to provide to the LP, retrieved from the command-line arguments. The `native_liquidity_amount` is the amount of atestfet tokens to provide to the LP, also retrieved from the command-line arguments. A transaction is created with the `"increase_allowance"` operation using the `execute()` method. The transaction specifies the `spender` (`pair_contract_address`), the `amount `to allow spending (`cw20_liquidity_amount`), and an `expires` parameter set to `never`. The script waits for the transaction to complete, and after this, a message is printed to indicate that the allowance has been increased. + + ```py copy + # Provide Liquidity + # Liquidity should be added so that the slippage tolerance parameter isn't exceeded + + tx = pair_contract.execute( + { + "provide_liquidity": { + "assets": [ + { + "info": {"token": {"contract_addr": token_contract_address}}, + "amount": cw20_liquidity_amount, + }, + { + "info": {"native_token": {"denom": native_denom}}, + "amount": native_liquidity_amount, + }, + ], + "slippage_tolerance": "0.1", + } + }, + sender=wallet, + funds=native_liquidity_amount + native_denom, + ) + + print( + f"Providing {native_liquidity_amount + native_denom} and {cw20_liquidity_amount} CW20 tokens to Liquidity Pool..." + ) + tx.wait_to_complete() + + print("Pool (after providing liquidity): ") + print(pair_contract.query({"pool": {}}), "\n") + + # Withdraw Liquidity + LP_token_balance = liq_token_contract.query( + {"balance": {"address": str(wallet.address())}} + )["balance"] + + withdraw_msg = '{"withdraw_liquidity": {}}' + withdraw_msg_bytes = withdraw_msg.encode("ascii") + withdraw_msg_base64 = base64.b64encode(withdraw_msg_bytes) + msg = str(withdraw_msg_base64)[2:-1] + + tx = liq_token_contract.execute( + { + "send": { + "contract": pair_contract_address, + "amount": LP_token_balance, + "msg": msg, + } + }, + sender=wallet, + ) + + print(f"Withdrawing {LP_token_balance} from pool's total share...") + tx.wait_to_complete() + + print("Pool (after withdrawing liquidity): ") + print(pair_contract.query({"pool": {}}), "\n") + + if __name__ == "__main__": + main() + ``` + + Within the `main()` script we would need to provide liquidity to the pool, ensuring that the slippage tolerance parameter isn't exceeded. Liquidity is added by creating a transaction with the `"provide_liquidity"` operation. The assets being provided include CW20 tokens and atestfet tokens. These are specified in a list within the `"assets"` field of the operation. The script also sets a slippage tolerance of `0.1`, meaning that the price impact of the liquidity provision must be within 10% of the expected value. The transaction is executed with `execute()` method, specifying the `"provide_liquidity"` operation. The `sender` parameter is set to the user's wallet, and the `funds` parameter includes the amount of atestfet tokens being provided. A message is printed indicating the amount of CW20 and atestfet tokens being provided to the liquidity pool. + + Afterwards, the script initiates a withdrawal of liquidity from the pool. This involves creating a transaction with the `"withdraw_liquidity"` operation. The LP token balance is queried using `query()` method to determine the amount of LP tokens held by the user. A withdrawal message is constructed in JSON format and then encoded and base64-encoded to be included in the transaction. The transaction is executed with the `execute()`, specifying the `"send"` operation. The `contract` parameter is set to the pair contract address, the `amount` parameter is set to the LP token balance, and the `msg` parameter includes the withdrawal message. A message is printed indicating the amount of LP tokens being withdrawn from the pool. Also, the LP balance is printed after withdrawal take place. + + In summary, the main function orchestrates a series of actions, simulating interactions with a liquidity pool. These actions include swapping tokens, providing liquidity, and withdrawing liquidity, and the state of the pool is printed at different stages to provide feedback to the user. + +5. Save the script. + +The overall script should be as follows: + +```py copy filename="aerial_liquidity_pool.py" +import argparse +import base64 +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from cosmpy.aerial.contract import LedgerContract +from cosmpy.aerial.faucet import FaucetApi +from cosmpy.aerial.wallet import LocalWallet + +def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "swap_amount", + type=int, + nargs="?", + default=10000, + help="atestfet swap amount to get some cw20 tokens on wallet's address", + ) + parser.add_argument( + "cw20_liquidity_amount", + type=int, + nargs="?", + default=100, + help="amount of cw20 tokens that will be provided to LP", + ) + parser.add_argument( + "native_liquidity_amount", + type=int, + nargs="?", + default=2470, + help="amount of atestfet tokens that will be provided to LP", + ) + return parser.parse_args() + +def main(): + """Run main.""" + args = _parse_commandline() + + # Define any wallet + wallet = LocalWallet.generate() + + # Network configuration + ledger = LedgerClient(NetworkConfig.latest_stable_testnet()) + + # Add tokens to wallet + faucet_api = FaucetApi(NetworkConfig.latest_stable_testnet()) + faucet_api.get_wealth(wallet.address()) + + # Define cw20, pair and liquidity token contracts + token_contract_address = ( + "fetch1qr8ysysnfxmqzu7cu7cq7dsq5g2r0kvkg5e2wl2fnlkqss60hcjsxtljxl" + ) + pair_contract_address = ( + "fetch1vgnx2d46uvyxrg9pc5mktkcvkp4uflyp3j86v68pq4jxdc8j4y0s6ulf2a" + ) + liq_token_contract_address = ( + "fetch1alzhf9yhghud3qhucdjs895f3aek2egfq44qm0mfvahkv4jukx4qd0ltxx" + ) + + token_contract = LedgerContract( + path=None, client=ledger, address=token_contract_address + ) + pair_contract = LedgerContract( + path=None, client=ledger, address=pair_contract_address + ) + liq_token_contract = LedgerContract( + path=None, client=ledger, address=liq_token_contract_address + ) + + print("Pool (initial state): ") + print(pair_contract.query({"pool": {}}), "\n") + + # Swap atestfet for CW20 tokens + swap_amount = str(args.swap_amount) + native_denom = "atestfet" + + tx = pair_contract.execute( + { + "swap": { + "offer_asset": { + "info": {"native_token": {"denom": native_denom}}, + "amount": swap_amount, + } + } + }, + sender=wallet, + funds=swap_amount + native_denom, + ) + + print(f"Swapping {swap_amount + native_denom} for CW20 Tokens...") + tx.wait_to_complete() + + print("Pool (after swap): ") + print(pair_contract.query({"pool": {}}), "\n") + + # To provide cw20 token to LP, increase your allowance first + cw20_liquidity_amount = str(args.cw20_liquidity_amount) + native_liquidity_amount = str(args.native_liquidity_amount) + + tx = token_contract.execute( + { + "increase_allowance": { + "spender": pair_contract_address, + "amount": cw20_liquidity_amount, + "expires": {"never": {}}, + } + }, + wallet, + ) + + print("Increasing Allowance...") + tx.wait_to_complete() + + # Provide Liquidity + # Liquidity should be added so that the slippage tolerance parameter isn't exceeded + + tx = pair_contract.execute( + { + "provide_liquidity": { + "assets": [ + { + "info": {"token": {"contract_addr": token_contract_address}}, + "amount": cw20_liquidity_amount, + }, + { + "info": {"native_token": {"denom": native_denom}}, + "amount": native_liquidity_amount, + }, + ], + "slippage_tolerance": "0.1", + } + }, + sender=wallet, + funds=native_liquidity_amount + native_denom, + ) + + print( + f"Providing {native_liquidity_amount + native_denom} and {cw20_liquidity_amount}CW20 tokens to Liquidity Pool..." + ) + tx.wait_to_complete() + + print("Pool (after providing liquidity): ") + print(pair_contract.query({"pool": {}}), "\n") + + # Withdraw Liquidity + LP_token_balance = liq_token_contract.query( + {"balance": {"address": str(wallet.address())}} + )["balance"] + + withdraw_msg = '{"withdraw_liquidity": {}}' + withdraw_msg_bytes = withdraw_msg.encode("ascii") + withdraw_msg_base64 = base64.b64encode(withdraw_msg_bytes) + msg = str(withdraw_msg_base64)[2:-1] + + tx = liq_token_contract.execute( + { + "send": { + "contract": pair_contract_address, + "amount": LP_token_balance, + "msg": msg, + } + }, + sender=wallet, + ) + + print(f"Withdrawing {LP_token_balance} from pool's total share...") + tx.wait_to_complete() + + print("Pool (after withdrawing liquidity): ") + print(pair_contract.query({"pool": {}}), "\n") + +if __name__ == "__main__": + main() +``` diff --git a/pages/guides/fetch-network/cosmpy/use-cases/oracles.mdx b/pages/guides/fetch-network/cosmpy/use-cases/oracles.mdx new file mode 100644 index 000000000..e1f7fef80 --- /dev/null +++ b/pages/guides/fetch-network/cosmpy/use-cases/oracles.mdx @@ -0,0 +1,391 @@ +# Oracles 🔮 + +## Introduction + +**Oracles** are entities that can update state variables in smart contracts and whose goal is usually to accurately estimate or predict some real world quantity or quantities. These quantities can then be used in the logic of other smart contracts. This guide shows how to write a CosmPy script that deploys and updates an oracle contract with a coin price, and another script that deploys a contract that queries this coin price. + +## Walk-through + +Here we provide an overview guide for setting up your own aerial oracle in few steps. + +We initially need to download the binaries for both contracts, which can be done as follows: + + ``` + wget https://raw.githubusercontent.com/fetchai/agents-aea/develop/packages/fetchai/contracts/oracle/build/oracle.wasm + wget https://raw.githubusercontent.com/fetchai/agents-aea/develop/packages/fetchai/contracts/oracle_client/build/oracle_client.wasm + ``` + +### Aerial oracle + +1. First of all, create a Python script and name it: `touch aerial_oracle.py` +2. We would then also require the following imports: + + ```py copy + from time import sleep + import requests + from cosmpy.aerial.client import LedgerClient, NetworkConfig + from cosmpy.aerial.contract import LedgerContract + from cosmpy.aerial.faucet import FaucetApi + from cosmpy.aerial.wallet import LocalWallet + from cosmpy.crypto.address import Address + ``` + +3. We then need to choose a data source for the coin price, the update interval, the decimal precision, and the decimal timeout for the oracle value: + + ```py copy + COIN_PRICE_URL = ( + "https://api.coingecko.com/api/v3/simple/price?ids=fetch-ai&vs_currencies=usd" + ) + UPDATE_INTERVAL_SECONDS = 10 + ORACLE_VALUE_DECIMALS = 5 + DEFAULT_TIMEOUT = 60.0 + ``` + +5. We then proceed and define a `_parse_commandline()` function by first importing the `argparse` module, which is a standard Python module for parsing command-line arguments: + + ```py copy + def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "contract_path", help="The path to the oracle contract to upload" + ) + parser.add_argument( + "contract_address", + nargs="?", + type=Address, + help="The address of the oracle contract if already deployed", + ) + return parser.parse_args() + ``` + + This first creates an argument `parser` object. The `ArgumentParser` class provides a way to specify the arguments your script should accept and automatically generates help messages and error messages. We then use `add_argument()` to add a positional argument named `contract_path`. This argument is required and should be a path to the oracle contract that you want to upload. The help argument provides a description of what this argument does. We further add another positional argument named `contract_address`. This argument is optional (`nargs="?"` allows it to be omitted), and it should be of type `Address`. The `type` argument specifies the type of the argument. In this case, `Address` is a custom type or class used to represent addresses. The `help` argument provides a description of what this argument does. At the end, we parse the command-line arguments provided when the script is executed. It returns an object that contains the values of the parsed arguments. + +6. We then need to proceed and define our `main()` function: + + ```py copy + def main(): + """Run main.""" + args = _parse_commandline() + + wallet = LocalWallet.generate() + + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + wallet_balance = ledger.query_bank_balance(wallet.address()) + + while wallet_balance < (10**18): + print("Providing wealth to wallet...") + faucet_api.get_wealth(wallet.address()) + wallet_balance = ledger.query_bank_balance(wallet.address()) + + contract = LedgerContract(args.contract_path, ledger, address=args.contract_address) + + if not args.contract_address: + instantiation_message = {"fee": "100"} + contract.deploy(instantiation_message, wallet, funds="1atestfet") + + print(f"Oracle contract deployed at: {contract.address}") + + grant_role_message = {"grant_oracle_role": {"address": wallet}} + contract.execute(grant_role_message, wallet).wait_to_complete() + + print(f"Oracle role granted to address: {wallet}") + + while True: + resp = requests.get(COIN_PRICE_URL, timeout=DEFAULT_TIMEOUT).json() + price = resp["fetch-ai"]["usd"] + value = int(price * 10**ORACLE_VALUE_DECIMALS) + + update_message = { + "update_oracle_value": { + "value": str(value), + "decimals": str(ORACLE_VALUE_DECIMALS), + } + } + contract.execute(update_message, wallet).wait_to_complete() + + print(f"Oracle value updated to: {price} USD") + print(f"Next update in {UPDATE_INTERVAL_SECONDS} seconds...") + sleep(UPDATE_INTERVAL_SECONDS) + + + if __name__ == "__main__": + main() + ``` + + This defines our `main()` function. When we run the script, the code inside `main()` will be executed. `args = _parse_commandline()` calls the `_parse_commandline()` function that we defined earlier. It parses the command-line arguments and returns an object (`args`) containing the values of the parsed arguments. We then generate a new local wallet, and then create a client for interacting with a blockchain ledger, using `LedgerClient()` class. We configured it to use the Fetch.ai stable testnet. We then create a client for interacting with a faucet API and query the balance of the wallet's address using the `query_bank_balance()` method. We also define an initial `while` loop which continues as long as the `wallet_balance` is less than `10**18`. Inside this first loop: it prints a message indicating that wealth is being provided to the wallet, then it calls the faucet API to get wealth for the wallet, and it updates the `wallet_balance` by querying the bank balance again. + + After this, we create a `contract` object using `LedgerContract()`: this takes the path to the oracle contract file, the ledger client, and optionally, the contract address. `if not args.contract_address:` condition checks if `args.contract_address` is not provided. If it has not been provided, it means the contract has not been deployed yet. We then set up an instantiation message with a fee of 100. We can then deploy the contract using the provided instantiation message, the wallet, and a specified fund source (`"1atestfet"` in this case). + + The `print()` function prints the address of the deployed oracle contract. After this, we define a `grant_role_message` object which sets up a message to grant the oracle role to the address associated with the wallet, and execute the message to grant the oracle role and wait for the transaction to complete. The following `print()` function prints a message indicating that the oracle role has been granted to the address associated with the wallet. + + We can finally define a second `while` loop which runs indefinitely: it sends a GET request to a URL (`COIN_PRICE_URL`) to retrieve coin prices, then extracts the price in USD. It then calculates a value based on the price and the specified decimal precision (`ORACLE_VALUE_DECIMALS`), and sets up an update message with the new oracle value. Lastly, it executes the update message, waits for the transaction to complete, prints the updated oracle value and indicates when the next update will occur. + +This script let us interact with a blockchain ledger, deploy a contract, and perform oracle-related tasks such as updating values based on external data. + +7. Save the script. + +The overall script should be as follows: + +```py copy filename="aerial_oracle.py" +import argparse +from time import sleep + +import requests + +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from cosmpy.aerial.contract import LedgerContract +from cosmpy.aerial.faucet import FaucetApi +from cosmpy.aerial.wallet import LocalWallet +from cosmpy.crypto.address import Address + + +COIN_PRICE_URL = ( + "https://api.coingecko.com/api/v3/simple/price?ids=fetch-ai&vs_currencies=usd" +) +UPDATE_INTERVAL_SECONDS = 10 +ORACLE_VALUE_DECIMALS = 5 +DEFAULT_TIMEOUT = 60.0 + + +def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "contract_path", help="The path to the oracle contract to upload" + ) + parser.add_argument( + "contract_address", + nargs="?", + type=Address, + help="The address of the oracle contract if already deployed", + ) + return parser.parse_args() + +def main(): + """Run main.""" + args = _parse_commandline() + + wallet = LocalWallet.generate() + + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + wallet_balance = ledger.query_bank_balance(wallet.address()) + + while wallet_balance < (10**18): + print("Providing wealth to wallet...") + faucet_api.get_wealth(wallet.address()) + wallet_balance = ledger.query_bank_balance(wallet.address()) + + contract = LedgerContract(args.contract_path, ledger, address=args.contract_address) + + if not args.contract_address: + instantiation_message = {"fee": "100"} + contract.deploy(instantiation_message, wallet, funds="1atestfet") + + print(f"Oracle contract deployed at: {contract.address}") + + grant_role_message = {"grant_oracle_role": {"address": wallet}} + contract.execute(grant_role_message, wallet).wait_to_complete() + + print(f"Oracle role granted to address: {wallet}") + + while True: + resp = requests.get(COIN_PRICE_URL, timeout=DEFAULT_TIMEOUT).json() + price = resp["fetch-ai"]["usd"] + value = int(price * 10**ORACLE_VALUE_DECIMALS) + + update_message = { + "update_oracle_value": { + "value": str(value), + "decimals": str(ORACLE_VALUE_DECIMALS), + } + } + contract.execute(update_message, wallet).wait_to_complete() + + print(f"Oracle value updated to: {price} USD") + print(f"Next update in {UPDATE_INTERVAL_SECONDS} seconds...") + sleep(UPDATE_INTERVAL_SECONDS) + +if __name__ == "__main__": + main() +``` + +### Oracle client + +Now, we will write a script that deploys a contract that can request the oracle value in exchange for the required fee. + +1. Let's first create a Python script and name it: `touch aerial_oracle_client.py` +2. We start by importing the needed classes and define a `REQUEST_INTERVAL_SECONDS` variable: + + ```py copy + import argparse + from time import sleep + + from cosmpy.aerial.client import LedgerClient, NetworkConfig + from cosmpy.aerial.contract import LedgerContract + from cosmpy.aerial.faucet import FaucetApi + from cosmpy.aerial.wallet import LocalWallet + from cosmpy.crypto.address import Address + + REQUEST_INTERVAL_SECONDS = 10 + ``` +3. Like before, we proceed and define a `_parse_commandline()` function: + + ```py copy + def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "contract_path", help="The path to the oracle client contract to upload" + ) + parser.add_argument( + "oracle_contract_address", + type=Address, + help="The address of the oracle contract", + ) + parser.add_argument( + "contract_address", + nargs="?", + type=Address, + help="The address of the oracle client contract if already deployed", + ) + return parser.parse_args() + ``` + + This `_parse_commandline()` function is designed to parse command-line arguments. We first create a `parser` object. This object is used to specify what command-line arguments the program should expect. We then use the `add_argument()` method to define the arguments that the program expects. In this function, there are three arguments being defined: + + - `contract_path`: this is a required argument. It expects a string representing the path to the oracle client contract to upload. + - `oracle_contract_address`: this is also a required argument. It expects an `Address` object representing the address of the oracle contract. + - `contract_address`: this is an optional argument. It expects an `Address` object and is used to specify the address of the oracle client contract if it has already been deployed. The `nargs="?"` indicates that this argument is optional. + + The function returns an object containing the parsed values. + +4. We can now define our `main()` function. + + ```py copy + def main(): + """Run main.""" + args = _parse_commandline() + + wallet = LocalWallet.generate() + + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + wallet_balance = ledger.query_bank_balance(wallet.address()) + + while wallet_balance < (10**18): + print("Providing wealth to wallet...") + faucet_api.get_wealth(wallet.address()) + wallet_balance = ledger.query_bank_balance(wallet.address()) + + contract = LedgerContract(args.contract_path, ledger, address=args.contract_address) + + if not args.contract_address: + instantiation_message = { + "oracle_contract_address": str(args.oracle_contract_address) + } + contract.deploy(instantiation_message, wallet) + + print(f"Oracle client contract deployed at: {contract.address}") + + while True: + request_message = {"query_oracle_value": {}} + contract.execute( + request_message, wallet, funds="100atestfet" + ).wait_to_complete() + + result = contract.query({"oracle_value": {}}) + print(f"Oracle value successfully retrieved: {result}") + + sleep(REQUEST_INTERVAL_SECONDS) + + if __name__ == "__main__": + main() + ``` + + The first line calls the `_parse_commandline()` function that we defined earlier. It will parse the command-line arguments and return an object (`args`) containing the parsed values. We proceed and generate a new local wallet, `wallet`, and then create a new `ledger` object for interacting with the blockchain or ledger system, using `LedgerClient()`. Afterwards, we create a `FaucetApi` object, `faucet_api`, which is used for interacting with the faucet service. We use the `query_bank_balance()` method to query the balance associated with the wallet's address. We then define a `while` loop which will continue as long as the `wallet_balance` is less than `10**18`. This is to ensure the wallet has a sufficient balance. Afterwards, we use the `get_wealth()` method to add wealth to the wallet, and then create a new `LedgerContract()` object which takes the `contract_path`, the `ledger` object, and an optional `contract_address`. `if not args.contract_address:` checks if `args.contract_address` is not provided. If it has not been provided, it means that the contract has not been deployed yet. We then create an `instantiation_message`, which contains the data needed for deploying the contract. `contract.deploy()` deploys the contract with the provided `instantiation_message` and the `wallet`. The code then prints out the address of the deployed contract. Finally, we define a second loop starting with `while True:` which repeatedly executes the following steps: + + - It creates a request message, which is used to query the oracle value. + - It executes the contract function call with the request message, using the wallet for authorization. The funds argument is set to `"100atestfet"`. + - It queries the contract for the oracle value. + - It prints out the retrieved oracle value. + - It finally waits for a specified number of seconds (defined by `REQUEST_INTERVAL_SECONDS`) before the next iteration. This is likely to prevent overloading the system with requests. + +5. Save the script. + +The overall script should be as follows: + +```py copy filename="aerial_oracle_client.py" +import argparse +from time import sleep + +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from cosmpy.aerial.contract import LedgerContract +from cosmpy.aerial.faucet import FaucetApi +from cosmpy.aerial.wallet import LocalWallet +from cosmpy.crypto.address import Address + +REQUEST_INTERVAL_SECONDS = 10 + +def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "contract_path", help="The path to the oracle client contract to upload" + ) + parser.add_argument( + "oracle_contract_address", + type=Address, + help="The address of the oracle contract", + ) + parser.add_argument( + "contract_address", + nargs="?", + type=Address, + help="The address of the oracle client contract if already deployed", + ) + return parser.parse_args() + +def main(): + """Run main.""" + args = _parse_commandline() + + wallet = LocalWallet.generate() + + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + wallet_balance = ledger.query_bank_balance(wallet.address()) + + while wallet_balance < (10**18): + print("Providing wealth to wallet...") + faucet_api.get_wealth(wallet.address()) + wallet_balance = ledger.query_bank_balance(wallet.address()) + + contract = LedgerContract(args.contract_path, ledger, address=args.contract_address) + + if not args.contract_address: + instantiation_message = { + "oracle_contract_address": str(args.oracle_contract_address) + } + contract.deploy(instantiation_message, wallet) + + print(f"Oracle client contract deployed at: {contract.address}") + + while True: + request_message = {"query_oracle_value": {}} + contract.execute( + request_message, wallet, funds="100atestfet" + ).wait_to_complete() + + result = contract.query({"oracle_value": {}}) + print(f"Oracle value successfully retrieved: {result}") + + sleep(REQUEST_INTERVAL_SECONDS) + +if __name__ == "__main__": + main() +``` + +Bear in mind that specific data related to the oracle's address and contract need to be provided by hand based on your personalized information! diff --git a/pages/guides/fetch-network/cosmpy/use-cases/stake-auto-compounder.mdx b/pages/guides/fetch-network/cosmpy/use-cases/stake-auto-compounder.mdx new file mode 100644 index 000000000..26c7e5d6e --- /dev/null +++ b/pages/guides/fetch-network/cosmpy/use-cases/stake-auto-compounder.mdx @@ -0,0 +1,336 @@ +# Stake auto-compounder + +## Introduction + +The Stake Auto-Compounder is a CosmPy based use case developed using Python and designed to automate the process of staking tokens in a blockchain network, claiming rewards, and compounding those rewards by re-delegating them to a validator. When an account delegates tokens to a network's validator, it will start generating rewards proportionally to the amount of [`Stake` ↗️]️(/guides/fetch-network/cosmpy/staking) delegated. But since rewards aren not automatically added to your stake and therefore do not contribute to future rewards, we can perform a compounding strategy to generate exponential rewards. + +## Delegate your tokens + +The first thing we need to do is delegate some tokens to a `validator`. You can do so by using a [`Wallet` ↗️]️(/guides/fetch-network/cosmpy/creating-wallet)️ and specifying the validator address and amount. you can delegate tokens to a specific validator by using the `delegate_tokens` method of the `ledger_client` object and specifying the validator's address, the amount of tokens and the wallet from which the delegation is made: + + ```py copy + validators = ledger_client.query_validators() + + # choose any validator + validator = validators[0] + + key = PrivateKey("FX5BZQcr+FNl2usnSIQYpXsGWvBxKLRDkieUNIvMOV7=") + wallet = LocalWallet(key) + + # delegate some tokens to this validator + tx = ledger_client.delegate_tokens(validator.address, 9000000000000000000, wallet) + tx.wait_to_complete() + ``` + +## Auto compounder + +We can write a script helping us claiming rewards and delegating the rewarded tokens back to the `validator` of choice. This way we keep growing our `Stake` given the generated compounded rewards on such staked amount. We first need to define the `time limit` and the compounding `period`. + +Importantly, bear in mind that each time an account performs a claim or a delegate a transaction, it has to pay certain fees. Therefore, the compounding period has to be long enough to generate sufficient rewards to exceed the fees that will be paid in each transaction and generate a profit. + +After having defined such parameters, we can then start a timer that claims rewards and delegates them in each time period: + + ```py copy + time_check = 0 + start_time = time.monotonic() + time.sleep(period) + + # query, claim and delegate rewards after time period + while time_check < time_limit: + + begin = time.monotonic() + + summary = ledger_client.query_staking_summary(wallet.address()) + print(f"Staked: {summary.total_staked}") + + balance_before = ledger_client.query_bank_balance(wallet.address()) + + tx = ledger_client.claim_rewards(validator.address, wallet) + tx.wait_to_complete() + + balance_after = ledger_client.query_bank_balance(wallet.address()) + + # reward after any fees + true_reward = balance_after - balance_before + + if true_reward > 0: + + print(f"Staking {true_reward} (reward after fees)") + + tx = ledger_client.delegate_tokens(validator.address, true_reward, wallet) + tx.wait_to_complete() + + else: + print("Fees from claim rewards transaction exceeded reward") + + end = time.monotonic() + + time.sleep(period-(end-begin)) + time_check = time.monotonic() - start_time + ``` + + In the code snippet above we defined a while loop running until the timer exceeds the `time limit`. Each loop will last the time specified in `period`. We query the balance before and after claiming rewards to get the value of the reward after any fees. If the true reward value is positive, we delegate those tokens to the validator, if it is negative, it means that the fees from claiming and delegating transactions exceeded the rewards, and therefore we will not delegate. + +## Walk-through + +Below we provide a step-by-step guide to create an auto compounder using the `cosmpy.aerial` package. + +1. First of all, create a Python script and name it: `touch aerial_compounder.py` +2. We need to import the necessary modules, including `argparse`, `time`, and various modules from the `cosmpy.aerial` package: + + ```py copy + import argparse + import time + + from cosmpy.aerial.client import LedgerClient + from cosmpy.aerial.config import NetworkConfig + from cosmpy.aerial.faucet import FaucetApi + from cosmpy.aerial.wallet import LocalWallet + ``` + +3. We now need to define a `_parse_commandline()` function responsible for parsing command-line arguments when the script is being executed: + + ```py copy + def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "initial_stake", + type=int, + nargs="?", + default=9000000000000000000, + help="Initial amount of atestfet to delegate to validator", + ) + parser.add_argument( + "time_limit", + type=int, + nargs="?", + default=600, + help="total time", + ) + parser.add_argument( + "period", + type=int, + nargs="?", + default=100, + help="compounding period", + ) + + return parser.parse_args() + ``` + + We first create a `parser` instance of the `ArgumentParser` class using the `argparse` module. Argument parsers are used to specify and parse command-line arguments. The `add_argument()` method is used to specify the arguments that the script will accept. It takes several parameters, including: + + - `name`: the name of the argument. + - `type`: the type to which the argument should be converted (in this case, `int`). + - `nargs`: the number of arguments expected (in this case, `"?"` means zero or one argument). + - `default`: the default value if the argument is not provided. + - `help`: a brief description of the argument, which will be displayed if the user asks for help with the script. + + Three arguments are defined in this function: + + - `initial_stake`: the initial amount of tokens to delegate to a validator. It expects an integer and has a default value of `9000000000000000000`. + - `time_limit`: the total time limit for the compounder. It expects an integer (representing seconds) and has a default value of `600` seconds (10 minutes). + - `period`: the compounding period, which is the interval between each compounding operation. It expects an integer (also in seconds) and has a default value of `100` seconds. + + The last line of the snippet above, `parser.parse_args()`, parses the command-line arguments provided when the script is executed. The function returns the parsed arguments object. + +4. We are now ready to define our `main()` function: + + ```py copy + def main(): + """Run main.""" + args = _parse_commandline() + + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + # get all the active validators on the network + validators = ledger.query_validators() + + # choose any validator + validator = validators[0] + + alice = LocalWallet.generate() + + wallet_balance = ledger.query_bank_balance(alice.address()) + initial_stake = args.initial_stake + + while wallet_balance < (initial_stake): + print("Providing wealth to wallet...") + faucet_api.get_wealth(alice.address()) + wallet_balance = ledger.query_bank_balance(alice.address()) + + # delegate some tokens to this validator + tx = ledger.delegate_tokens(validator.address, initial_stake, alice) + tx.wait_to_complete() + + # set time limit and compounding period in seconds + time_limit = args.time_limit + period = args.period + + time_check = 0 + start_time = time.monotonic() + time.sleep(period) + + # query, claim and stake rewards after time period + while time_check < time_limit: + + begin = time.monotonic() + + summary = ledger.query_staking_summary(alice.address()) + print(f"Staked: {summary.total_staked}") + + balance_before = ledger.query_bank_balance(alice.address()) + + tx = ledger.claim_rewards(validator.address, alice) + tx.wait_to_complete() + + balance_after = ledger.query_bank_balance(alice.address()) + + # reward after any fees + true_reward = balance_after - balance_before + + if true_reward > 0: + + print(f"Staking {true_reward} (reward after fees)") + + tx = ledger.delegate_tokens(validator.address, true_reward, alice) + tx.wait_to_complete() + + else: + print("Fees from claim rewards transaction exceeded reward") + + print() + + end = time.monotonic() + time.sleep(period - (end - begin)) + time_check = time.monotonic() - start_time + + + if __name__ == "__main__": + main() + ``` + + The first line calls the `_parse_commandline()` function we defined earlier. It returns an object with the parsed command-line arguments. We then create two objects: + + - A `ledger` instance of the `Ledger Client` class configured for the Fetch.ai stable testnet. This client will be used to interact with the blockchain network. + - A `faucet_api` instance of the `Faucet API` class configured for the Fetch.ai stable testnet. This API is used for providing additional funds to the wallet if needed. + + We then need to get all the active validators on the network by using the `query_validators()` method. After this, we choose a validator and create a new wallet named `alice` using `LocalWallet.generate()` and check the balance of the `alice` wallet. If the balance is less than the initial stake, it enters a loop to provide wealth to the wallet using the faucet API until the balance reaches the specified initial stake. We can now delegate the initial stake of tokens to the chosen validator using the `delegate_tokens()` method. + + We proceed by setting time limits and periods. `time_limit = args.time_limit` sets the time limit based on the command-line argument, whereas `period = args.period` sets the compounding period based on the command-line argument. After this, we define the compounding loop, similar to what was described in the first part of this guide: it iterates over a specified time period, queries staking summary, claims rewards, and either stakes the rewards or skips if fees exceed rewards. Time management is important here: indeed, the loop keeps track of time using `time.monotonic()` to ensure it does not exceed the specified time limit. It waits for the specified period before starting the next compounding cycle. + +5. Save the script. + +The overall script should look as follows: + +```py copy filename="aerial_compounder.py" +import argparse +import time + +from cosmpy.aerial.client import LedgerClient +from cosmpy.aerial.config import NetworkConfig +from cosmpy.aerial.faucet import FaucetApi +from cosmpy.aerial.wallet import LocalWallet + + +def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "initial_stake", + type=int, + nargs="?", + default=9000000000000000000, + help="Initial amount of atestfet to delegate to validator", + ) + parser.add_argument( + "time_limit", + type=int, + nargs="?", + default=600, + help="total time", + ) + parser.add_argument( + "period", + type=int, + nargs="?", + default=100, + help="compounding period", + ) + + return parser.parse_args() + + +def main(): + """Run main.""" + args = _parse_commandline() + + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + # get all the active validators on the network + validators = ledger.query_validators() + + # choose any validator + validator = validators[0] + + alice = LocalWallet.generate() + + wallet_balance = ledger.query_bank_balance(alice.address()) + initial_stake = args.initial_stake + + while wallet_balance < (initial_stake): + print("Providing wealth to wallet...") + faucet_api.get_wealth(alice.address()) + wallet_balance = ledger.query_bank_balance(alice.address()) + + # delegate some tokens to this validator + tx = ledger.delegate_tokens(validator.address, initial_stake, alice) + tx.wait_to_complete() + + # set time limit and compounding period in seconds + time_limit = args.time_limit + period = args.period + + time_check = 0 + start_time = time.monotonic() + time.sleep(period) + + # query, claim and stake rewards after time period + while time_check < time_limit: + + begin = time.monotonic() + + summary = ledger.query_staking_summary(alice.address()) + print(f"Staked: {summary.total_staked}") + + balance_before = ledger.query_bank_balance(alice.address()) + + tx = ledger.claim_rewards(validator.address, alice) + tx.wait_to_complete() + + balance_after = ledger.query_bank_balance(alice.address()) + + # reward after any fees + true_reward = balance_after - balance_before + + if true_reward > 0: + + print(f"Staking {true_reward} (reward after fees)") + + tx = ledger.delegate_tokens(validator.address, true_reward, alice) + tx.wait_to_complete() + + else: + print("Fees from claim rewards transaction exceeded reward") + + print() + + end = time.monotonic() + time.sleep(period - (end - begin)) + time_check = time.monotonic() - start_time + + +if __name__ == "__main__": + main() +``` diff --git a/pages/guides/fetch-network/cosmpy/use-cases/stake-optimizer.mdx b/pages/guides/fetch-network/cosmpy/use-cases/stake-optimizer.mdx new file mode 100644 index 000000000..fc059efa4 --- /dev/null +++ b/pages/guides/fetch-network/cosmpy/use-cases/stake-optimizer.mdx @@ -0,0 +1,465 @@ +# Stake optimizer + +## Getting started + +Whenever we delegate tokens to a validator for a determined period, you can use the [`auto-compounder` ↗️](/guides/fetch-network/cosmpy/use-cases/stake-auto-compounder) to get increasing rewards. You can maximize your rewards for a given staking period by selecting an optimal compounding period. To do this, you will need to follow these steps: + + - **Set and query variables**: when calculating staking rewards, you need to set and query variables such as staking parameters, transaction fees, and network parameters. + - **Calculate reward rate**: after you select and query all the variables needed, you will calculate the reward rate. + - **Calculate optimal compounding period**: you will calculate the optimal compounding period that will maximize your rewards. + +First of all, we need to define a network to work with: + +```py copy +from cosmpy.aerial.client import LedgerClient +from cosmpy.aerial.config import NetworkConfig + +ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) +``` + +## Set and query variables +### Staking variables + +First, we need to define the desired amount and the total period that we would like to stake in: `initial_stake` and `total_period` variables. Here we will stake 50 TESTFET for 60000 minutes. For this guide, we will work with minutes as a time unit: + +```py copy +initial_stake = 50000000000000000000 +total_period = 60000 +``` + +### Validator selection and variables + +We are now ready to select a validator to delegate our tokens. We can do this by analyzing which one has the lowest commission and a reasonable amount of stake delegated compared to the total stake. + +```py copy +from cosmpy.protos.cosmos.staking.v1beta1.query_pb2 import QueryValidatorsRequest + +req = QueryValidatorsRequest() +resp = ledger.staking.Validators(req) + +# Calculate the total stake currently in the testnet +# Status = 3 means that the validator is bonded +validators_stake = [int(validator.tokens) for validator in resp.validators if validator.status == 3] +total_stake = sum(validators_stake) + +# For every bonded validator, we print commission and percentage of total stake +print("MONIKER COMISSION % of TOTAL STAKE") +for validator in resp.validators: + if validator.status == 3: + moniker = validator.description.moniker + comission = int(validator.commission.commission_rates.rate)/1e18*100 + print(moniker[:10]," ", comission,"% ", round(int(validator.tokens)/total_stake*100,3),"%") +``` + +Once you run the code above, you will observe each validator commission rate and its percentage delegated of the total stake. The most important parameter to observe in each validator is the commission it takes from rewards. We should always select a validator with the lower commission as long as it has a reasonable stake compared with the total stake. + +```py copy +# get all the active validators on the network +validators = ledger.query_validators() + +# Query info of selected validator +selected_validator = "validator0" +validator = [v for v in validators if v.moniker == selected_validator][0] +query_validator = [v for v in resp.validators if v.description.moniker == selected_validator][0] + +# Set the comission % +commission = int(query_validator.commission.commission_rates.rate)/1e18 + +# Set percentage delegated of total stake +pct_delegated = initial_stake/total_stake +``` + +In this case, at the moment the code is run, all validators have the same commission, therefore, we simply select the validator with the highest stake, which is validator0. Feel free to select the most convenient validator when you run the code above. We will save the variables `commission` and the fraction `pct_delegated` of our `initial_stake` to the `total_stake` to use them both later on. + +### Estimate transaction fees + +We now need to know an estimate of the transaction fees we will face every time we claim rewards and delegate tokens. For that, both claim rewards and delegate tokens transactions were combined into a single multi-msg transaction to simulate the total fees. + +```py copy +from cosmpy.aerial.client.distribution import create_withdraw_delegator_reward +from cosmpy.aerial.client.staking import create_delegate_msg +from cosmpy.aerial.tx import SigningCfg +from cosmpy.aerial.wallet import LocalWallet +from cosmpy.crypto.keypairs import PrivateKey +from cosmpy.crypto.address import Address +from cosmpy.aerial.tx import Transaction + +# Use any address with at least the amount of initial_stake available +key = PrivateKey("XZ5BZQcr+FNl2usnSIQYpXsGWvBxKLRDkieUNIvMOV7=") +alice = LocalWallet(key) +alice_address = Address(key)._display + +tx = Transaction() + +# Add delegate msg +tx.add_message(create_delegate_msg(alice_address,validator.address,initial_stake,"atestfet")) + +# Add claim reward msg +tx.add_message(create_withdraw_delegator_reward(alice_address, validator.address)) + +account = ledger.query_account(alice.address()) +tx.seal(SigningCfg.direct(alice.public_key(), account.sequence),fee="",gas_limit=0) +tx.sign(alice.signer(), ledger.network_config.chain_id, account.number) +tx.complete() + +# simulate the fee for the transaction +_, str_tx_fee = ledger.estimate_gas_and_fee_for_tx(tx) +``` + +Since the output of this function is a string, we need to convert it to an int and round it up to get a more conservative estimate for the `fee`. + +```py copy +denom = "atestfet" +tx_fee = str_tx_fee[:-len(denom)] + +# Add a 20% to the fee estimation to get a more conservative estimate +fee = int(tx_fee) * 1.20 +``` + +### Query network variables + +There are three network variables that we need to query since they will contribute to the staking rewards calculation: `total_supply`, `inflation`, and `community_tax`. + +```py copy +# Total Supply of tokens +req = QueryTotalSupplyRequest() +resp = ledger.bank.TotalSupply(req) +total_supply = float(json.loads(resp.supply[0].amount)) + +# Inflation +req = QueryParamsRequest(subspace="mint", key="InflationRate") +resp = ledger.params.Params(req) +inflation = float(json.loads(resp.param.value)) + +# Community Tax +req = QueryParamsRequest(subspace="distribution", key="communitytax") +resp = ledger.params.Params(req) +community_tax = float(json.loads(resp.param.value)) +``` + +### Calculate reward rate + +We can now proceed to calculate a theoretical staking rewards rate using the variables gathered above. These are: `inflation`, `total_supply`, `pct_delegated`, `community_tax` and `commission`: + +```py copy +# Calculate annual reward +anual_reward = (inflation * total_supply) *pct_delegated* (1- community_tax)*(1- commission) + +# Convert from annual reward to minute reward +minute_reward = anual_reward/360/24/60 + +# Set the rate +rate = minute_reward/initial_stake +``` + +### Calculate optimal compounding period + +We can calculate the optimal compounding period that maximizes our staking rewards analytically by using the following formula: + +![](/../../docs_v2/src/images/docs/reward_equation.png) + + Where: + + - M = Total stake at time D + - S = Initial Stake + - f = Transaction Fee + - k = Reward Rate + - m = Number Of Compounding Transactions + - n = Compounding Period + - D = m x n = Total Staking Time + +We will now find the value that maximizes reward by taking the first derivative with respect to _n_ and finding the root in the interval _(0,D]_: + +```py copy +import numpy as np +from sympy.utilities.lambdify import lambdify, implemented_function +from sympy import * +from scipy.optimize import brentq + +f = fee +S = initial_stake +k = rate +D = total_period + +# x will represent n +x = Symbol("x") + +# Define the function +M = (S*(1+(k*x))**(D/x))+((1-((1+(k*x))**(D/x)))/(k*x))*f +Mx = lambdify(x,M) + +# Take the first derivative with respect to x +M_prime = M.diff(x) +Mx_prime = lambdify(x,M_prime) + +# Find the maximum reward value by finding the root of the function +optimal_period = brentq(Mx_prime, 0.1, D) + +print("optimal_period: ", analytical_optimal_period, " minutes") +``` + +You can make use of the `optimal_period` value in the staking [`auto-compounder` ↗️](/guides/fetch-network/cosmpy/use-cases/stake-auto-compounder) to maximize your rewards. + +You can also plot the function along with the optimal period to observe the results in the following manner: + +```py copy +import matplotlib.pyplot as plt + +plot = plt.figure(0,figsize=(6,4), dpi=100) + +y = np.linspace(1,300, 100) +plt.plot(y,Mx(y),"k", label = 'analytical function') +plt.axvline(x = optimal_period, color = 'g', linewidth = 2, label = f'optimal period: {round(optimal_period)}') +plt.legend() + +plt.xlabel("Compounding periods") +plt.ylabel('Total Reward') +plt.title('Maximizing Rewards') +plt.grid() +``` + +![](/../../docs_v2/src/images/docs/cosmpy-graph-optimal-period.png) + +Finally, we can compare the compounding staking rewards to a simple non-compounding strategy: + +```py copy +# Compounding Strategy +comp_rewards = [] +rewards = 0 +period = optimal_period +S = initial_stake +for i in range(total_period): + rewards = rewards + (S*rate) + if i%period == 0: + S = S + rewards - fee + rewards = 0 + comp_rewards.append(S) +S = S + rewards - (fee/2) +comp_rewards.append(S) + +# Simple Strategy +s_reward = initial_stake*rate +simple_rewards = [initial_stake+(s_reward*i) for i in range(comp_period)] + +# Plots +plot = plt.figure(0,figsize=(12,4), dpi=100) + +plt.subplot(1,2,1) +plt.plot(comp_rewards, label = "Compounded Rewards") +plt.plot(simple_rewards, label = "Simple Rewards") +plt.xlabel("time in minutes") +plt.ylabel('Reward') +plt.title('Staking Rewards') +plt.legend() + +plt.subplot(1,2,2) + +plt.plot(total_rewards, label = "Compounded Rewards") +plt.plot(simple_rewards, label = "Simple Rewards") +plt.xlabel("time in minutes") +plt.ylabel('Reward') +plt.title('Staking Rewards (log scale)') +plt.legend() + +plt.yscale('log') +``` + +![](/../../docs_v2/src/images/docs/cosmpy_stake_optimizer.png) + +Now that we have presented the concepts and ideas behind the stake optimizer, have a look at the abbreviated version of the code provided below: + +```py copy filename="aerial_stake_optimizer.py" +import json + +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from cosmpy.aerial.client.distribution import create_withdraw_delegator_reward +from cosmpy.aerial.client.staking import create_delegate_msg +from cosmpy.aerial.faucet import FaucetApi +from cosmpy.aerial.tx import SigningCfg, Transaction +from cosmpy.aerial.wallet import LocalWallet +from cosmpy.protos.cosmos.bank.v1beta1.query_pb2 import QueryTotalSupplyRequest +from cosmpy.protos.cosmos.params.v1beta1.query_pb2 import QueryParamsRequest +from cosmpy.protos.cosmos.staking.v1beta1.query_pb2 import QueryValidatorsRequest + +# This function returns the total reward for given: +# * f -> fee +# * S -> Initial Stake +# * k -> Reward Rate +# * D -> Total staking period +# * x -> Compounding Period +def M(x, f, S, k, D): + """ + Calculate the total reward. + + :param x: Compounding Period + :param f: fee + :param S: Initial Stake + :param k: Reward Rate + :param D: Total staking period + + :return: Total reward + """ + return (S * (1 + (k * x)) ** (D / x)) + ( + (1 - ((1 + (k * x)) ** (D / x))) / (k * x) + ) * f + +def main(): + """Run main.""" + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + # Set initial stake and desired stake period + initial_stake = 50000000000000000000 + total_period = 60000 + + req = QueryValidatorsRequest() + resp = ledger.staking.Validators(req) + + # Calculate the total staked in the testnet + + total_stake = 0 + # validator.status == 3 refers to bonded validators + validators_stake = [ + int(validator.tokens) for validator in resp.validators if validator.status == 3 + ] + total_stake = sum(validators_stake) + + # Get validators commissions + validators_comission = [ + int(validator.commission.commission_rates.rate) + for validator in resp.validators + if validator.status == 3 + ] + + validators = ledger.query_validators() + validator = "not_selected" + + # Choose a threshold for a validators minimum percentage of total stake delegated + stake_threshold = 0.10 + + for _i in range(len(validators_comission)): + + # Choose validator with lower commission + validator_index = validators_comission.index(min(validators_comission)) + + # Verify that it meets the minimum % threshold + validator_stake_pct = validators_stake[validator_index] / total_stake + if validator_stake_pct >= stake_threshold: + + # Set the selected validator + validator = validators[validator_index] + break + + # We omit this validator by setting his commssion to infinity + validators_comission[validator_index] = float("inf") + + if validator == "not_selected": + # Restart validators_comission list with original values + validators_comission = [ + int(validator.commission.commission_rates.rate) + for validator in resp.validators + if validator.status == 3 + ] + + print("No validator meets the minimum stake threshold requirement") + + # Proceed to select the validator with the lowest commission + validator_index = validators_comission.index(min(validators_comission)) + validator = validators[validator_index] + + # Query validator commission + commission = float(resp.validators[0].commission.commission_rates.rate) / 1e18 + + # Set percentage delegated of total stake + pct_delegated = initial_stake / total_stake + + # Estimate fees for claiming and delegating rewards + + alice = LocalWallet.generate() + alice_address = str(alice.address()) + + alice_balance = ledger.query_bank_balance(alice.address()) + + while alice_balance < initial_stake: + print("Providing wealth to alice...") + faucet_api.get_wealth(alice.address()) + alice_balance = ledger.query_bank_balance(alice.address()) + + tx = Transaction() + + # Add delegate msg + tx.add_message( + create_delegate_msg(alice_address, validator.address, initial_stake, "atestfet") + ) + + # Add claim reward msg + tx.add_message(create_withdraw_delegator_reward(alice_address, validator.address)) + + account = ledger.query_account(alice.address()) + + tx.seal( + SigningCfg.direct(alice.public_key(), account.sequence), fee="", gas_limit=0 + ) + tx.sign(alice.signer(), ledger.network_config.chain_id, account.number) + tx.complete() + + # simulate the fee for the transaction + _, str_tx_fee = ledger.estimate_gas_and_fee_for_tx(tx) + + denom = "atestfet" + tx_fee = str_tx_fee[: -len(denom)] + + # Add a 20% to the fee estimation to get a more conservative estimate + fee = int(tx_fee) * 1.20 + + # Query chain variables + + # Total Supply of tokens + req = QueryTotalSupplyRequest() + resp = ledger.bank.TotalSupply(req) + total_supply = float(json.loads(resp.supply[0].amount)) + + # Inflation + req = QueryParamsRequest(subspace="mint", key="InflationRate") + resp = ledger.params.Params(req) + inflation = float(json.loads(resp.param.value)) + + # Community Tax + req = QueryParamsRequest(subspace="distribution", key="communitytax") + resp = ledger.params.Params(req) + community_tax = float(json.loads(resp.param.value)) + + # Annual reward calculation + anual_reward = ( + (inflation * total_supply) + * pct_delegated + * (1 - community_tax) + * (1 - commission) + ) + + # Convert from annual reward to minute reward + minute_reward = anual_reward / 360 / 24 / 60 + rate = minute_reward / initial_stake + + # Compute optimal period + f = fee + S = initial_stake + k = rate + D = total_period + + # List of compounding periods + X = list(range(1, D)) + + # Evaluate function M on each compounding period + R = [M(x, f, S, k, D) for x in X] + + # Fnd the period that maximizes rewards + optimal_period = R.index(max(R)) + 1 + + # These values can be used in aerial_compounder.py to maximize rewards + print("total period: ", total_period, "minutes") + print("optimal compounding period: ", optimal_period, "minutes") + +if __name__ == "__main__": + main() +``` diff --git a/pages/guides/fetch-network/cosmpy/use-cases/swap_automation.mdx b/pages/guides/fetch-network/cosmpy/use-cases/swap_automation.mdx new file mode 100644 index 000000000..b984dbdb4 --- /dev/null +++ b/pages/guides/fetch-network/cosmpy/use-cases/swap_automation.mdx @@ -0,0 +1,474 @@ +# Swap automation 🔄 + +## Introduction + +The following guide demonstrates an automated swapping strategy for a liquidity pool on the Fetch.ai network. It interacts with a liquidity pool contract and performs swaps between two different tokens (atestfet and CW20 tokens) based on specified price thresholds. A mean-reversion strategy expects the prices to return to "normal" levels or a certain moving average following a temporary price spike. We can construct a similar strategy using the Liquidity Pool, where we will set upper and lower bound prices that will trigger a sell and a buy transaction respectively. If the behavior of the LP prices works as expected always returning to a certain moving average, we could profit by selling high and buying low. We will do this by swapping atestfet and CW20 with the Liquidity Pool, we refer to a sell transaction when we sell atestfet and get CW20 tokens, a buy transaction would be exactly the opposite. + +## Walk-through + +1. Let's start by creating a Python script and name it: `touch aerial_swap_automation.py` +2. Let's then import the needed classes: + + ```py copy + import argparse + from time import sleep + + from cosmpy.aerial.client import LedgerClient, NetworkConfig + from cosmpy.aerial.contract import LedgerContract + from cosmpy.aerial.faucet import FaucetApi + from cosmpy.aerial.wallet import LocalWallet + ``` + +3. We then need to define a `swap_native_for_cw20()` function which performs a swap from native tokens (atestfet) to CW20 tokens within a liquidity pool: + + ```py copy + def swap_native_for_cw20(swap_amount, pair_contract, wallet): + """ + Swap Native for cw20. + + :param swap_amount: swap amount + :param pair_contract: pair contract address + :param wallet: wallet address + + """ + tx = pair_contract.execute( + { + "swap": { + "offer_asset": { + "info": {"native_token": {"denom": "atestfet"}}, + "amount": str(swap_amount), + } + } + }, + sender=wallet, + funds=str(swap_amount) + "atestfet", + ) + print("swapping native for cw20 tokens") + tx.wait_to_complete() + ``` + + Within the function, we defined the following parameters: + + - `swap_amount`: this parameter specifies the amount of native tokens to be swapped for CW20 tokens. + + - `pair_contract`: this parameter represents the contract address of the liquidity pool pair where the swap will occur. + + - `wallet`: this parameter represents the wallet address that will perform the swap. + + The function constructs a transaction to execute the swap operation. The `execute()` method is called on the `pair_contract` with a dictionary specifying the `"swap"` operation. Inside the `"swap"` operation, the `offer_asset` field is set to the following: + + - `info`: this field specifies that the swap involves native tokens (`native_token`) with the denomination `"atestfet"`. + - `amount`: this field specifies the amount of native tokens to be swapped, which is converted to a string. + + The `sender` parameter is set to the `wallet` address, indicating that the wallet will initiate the swap. The `funds` parameter is set to a string representing the total amount of funds being used for the swap, which includes the `swap_amount` and `"atestfet"`. Finally, the function waits for the transaction to complete and prints a message indicating that native tokens are being swapped for CW20 tokens. + +4. We then need to define a `swap_cw20_for_native()` function which performs a swap from CW20 tokens to native tokens (atestfet) within a liquidity pool: + + ```py copy + + def swap_cw20_for_native(swap_amount, pair_contract_address, token_contract, wallet): + """ + Swap cw20 for native. + + :param swap_amount: swap amount + :param pair_contract_address: pair contract address + :param token_contract: token contract + :param wallet: wallet address + + """ + tx = token_contract.execute( + { + "send": { + "contract": pair_contract_address, + "amount": str(swap_amount), + "msg": "eyJzd2FwIjp7fX0=", + } + }, + wallet, + ) + print("swapping cw20 for native tokens") + tx.wait_to_complete() + ``` + + Within the function, we defined the following parameters: + + - `swap_amount`: this parameter specifies the amount of CW20 tokens to be swapped for native tokens. + - `pair_contract_address`: this parameter represents the contract address of the liquidity pool pair where the swap will occur. + - `token_contract`: this parameter represents the contract for the CW20 token. + - `wallet`: This parameter represents the wallet address that will perform the swap. + + The function constructs a transaction to execute the swap operation: the `execute()` method is called on the `token_contract` with a dictionary specifying the `"send"` operation. Inside this operation, the contract field is set to `pair_contract_address`, indicating that the CW20 tokens will be sent to the liquidity pool. The `amount` field is set to the `swap_amount`, which is converted to a string. The `msg` field is set to the base64 encoded message `"eyJzd2FwIjp7fX0="`, which likely contains additional instructions or parameters for the swap. The `wallet` address is specified as the sender of the transaction. Finally, the function waits for the transaction to complete and prints a message indicating that CW20 tokens are being swapped for native tokens. + +5. We now would need to proceed by defining a `_parse_commandline()` function: + + ```py copy + def _parse_commandline(): + """Commandline parser.""" + parser = argparse.ArgumentParser() + + parser.add_argument( + "trading_wallet", + type=int, + nargs="?", + default=1000000, + help="initial atestfet balance to perform swaps using the liquidity pool", + ) + parser.add_argument( + "upper_bound", + type=int, + nargs="?", + default=20.5, + help="price upper bound that will trigger a swap from cw20 to native tokens", + ) + parser.add_argument( + "lower_bound", + type=int, + nargs="?", + default=19.5, + help="price lower bound that will trigger a swap from native to cw20 tokens", + ) + parser.add_argument( + "commission", + type=int, + nargs="?", + default=0.003, + help="LP commission, for terraswap the default is 0.3%", + ) + parser.add_argument( + "interval_time", + type=int, + nargs="?", + default=5, + help="interval time in seconds to query liquidity pool price", + ) + + return parser.parse_args() + ``` + + This function is responsible for parsing command line arguments in the script. It uses the `argparse.ArgumentParser()` class to define and handle the expected command line arguments: + + - `trading_wallet`: this argument represents the initial balance of atestfet in the trading wallet. It's an optional argument, and if not provided, it defaults to `1000000`. + - `upper_bound`: this argument specifies the upper price threshold that will trigger a swap from cw20 to native tokens . If not provided, it defaults to `20.5`. + - `lower_bound`: this argument sets the lower price threshold that will trigger a swap from native to cw20 tokens. It defaults to `19.5` if not provided. + - `commission`: this argument defines the commission rate for the liquidity pool. The default is `0.003`, representing 0.3%. + - `interval_time`: this argument determines the interval (in seconds) at which the script queries the liquidity pool price. If not provided, it defaults to `5` seconds. + + The function then returns an object containing the parsed arguments. These arguments can be accessed later in the script to control the behavior of the swap automation. + +6. We are ready to write down our `main()` function: + + ```py copy + def main(): + """Run main.""" + args = _parse_commandline() + + # Define any wallet + wallet = LocalWallet.generate() + + # Network configuration + ledger = LedgerClient(NetworkConfig.latest_stable_testnet()) + + # Add tokens to wallet + faucet_api = FaucetApi(NetworkConfig.latest_stable_testnet()) + + wallet_balance = ledger.query_bank_balance(wallet.address()) + + while wallet_balance < (10**18): + print("Providing wealth to wallet...") + faucet_api.get_wealth(wallet.address()) + wallet_balance = ledger.query_bank_balance(wallet.address()) + + # Define cw20, pair and liquidity token contracts + token_contract_address = ( + "fetch1qr8ysysnfxmqzu7cu7cq7dsq5g2r0kvkg5e2wl2fnlkqss60hcjsxtljxl" + ) + pair_contract_address = ( + "fetch1vgnx2d46uvyxrg9pc5mktkcvkp4uflyp3j86v68pq4jxdc8j4y0s6ulf2a" + ) + + token_contract = LedgerContract( + path=None, client=ledger, address=token_contract_address + ) + pair_contract = LedgerContract( + path=None, client=ledger, address=pair_contract_address + ) + + # tokens in trading wallet (currency will vary [atestfet,cw20] ) + currency = "atestfet" + tokens = args.trading_wallet + + # Swap thresholds + upper_bound = args.upper_bound + lower_bound = args.lower_bound + + # LP commission + commission = args.commission + + # Wait time + interval = args.interval_time + + while True: + + # Query LP status + pool = pair_contract.query({"pool": {}}) + native_amount = int(pool["assets"][1]["amount"]) + cw20_amount = int(pool["assets"][0]["amount"]) + + if currency == "atestfet": + # Calculate received cw20 tokens if atestfet tokens are given to LP + tokens_out = round( + ((cw20_amount * tokens) / (native_amount + tokens)) * (1 - commission) + ) + + # Sell price of atestfet => give atestfet, get cw20 + sell_price = tokens / tokens_out + print("atestfet sell price: ", sell_price) + if sell_price <= lower_bound: + swap_native_for_cw20(tokens, pair_contract, wallet) + tokens = int( + token_contract.query( + {"balance": {"address": str(wallet.address())}} + )["balance"] + ) + + # Trading wallet currency changed to cw20 + currency = "CW20" + else: + # Calculate received atestfet tokens if cw20 tokens are given to LP + tokens_out = round( + ((native_amount * tokens) / (cw20_amount + tokens)) * (1 - commission) + ) + + # Buy price of atestfet => give cw20, get atestfet + buy_price = tokens_out / tokens + print("atestfet buy price: ", buy_price) + if buy_price >= upper_bound: + swap_cw20_for_native( + tokens, pair_contract_address, token_contract, wallet + ) + tokens = tokens_out + + # Trading wallet currency changed to cw20 + currency = "atestfet" + + sleep(interval) + + + if __name__ == "__main__": + main() + ``` + + Within the `main()` function, the `_parse_commandline()` function is used to parse command line arguments. It sets various parameters such as the initial trading wallet balance, upper and lower price bounds for triggering swaps, liquidity pool commission, and interval time for querying the liquidity pool price, and all of these values are store in the `args` variable. After this, a new wallet is generated using the `generate()` method of the `LocalWallet` class, and network configuration is set up using the `LedgerClient()` class. Tokens are added to the wallet by using the Faucet API. This happens within a `while` loop which continues until the wallet balance reaches at least `10**18`. The wallet balance is retrieved using the `query_bank_balance()`. Afterwards, we need to define the addresses of the CW20, pair, and liquidity token contracts, as well as initialise various variables based on the command line arguments, including the initial wallet balance, `upper_bound` and `lower_bound` price bounds for swaps, LP commission rate, and the interval at which to check the liquidity pool price. + + We then define a loop (`while True`), which: + + - Queries the liquidity pool status (`pair_contract.query({"pool": {}})`) to get the current amounts of native tokens (`atestfet`) and CW20 tokens. + - Checks the current currency in the trading wallet (`currency`), which can be either native or CW20 tokens. + - If the current `currency` is `atestfet`, it calculates the potential amount of CW20 tokens that would be received if native tokens were given to the liquidity pool. This is done based on the ratio of CW20 tokens to the total of native tokens and current wallet tokens, with a deduction for the LP commission. It calculates a `sell_price` as the ratio of the current wallet tokens to tokens swapped out. + - If the sell price is lower than or equal to the specified `lower_bound`, it triggers the `swap_native_for_cw20()` function, which swaps atestfet tokens for CW20 tokens. + - After the successful swap, it updates the tokens variable to the new balance of CW20 tokens and changes the currency to `"CW20"`. + - If the current currency is `"CW20"`, it calculates the potential amount of atestfet tokens that would be received if CW20 tokens are given to the liquidity pool. This is done based on the ratio of native tokens to the total of CW20 tokens and current wallet tokens, with a deduction for the LP commission. It calculates a `buy_price` as the ratio of potential atestfet tokens to the current wallet tokens. + - If the `buy_price` is higher than or equal to the specified `upper_bound`, it triggers the `swap_cw20_for_native()` function, which swaps CW20 tokens for atestfet tokens. + - After the successful swap, it updates the tokens variable to the new balance of atestfet tokens and changes the currency to `"atestfet"`. + The loop then waits for the specified `interval` before checking the liquidity pool status and performing the next iteration. + +7. Save the script. + +The overall script should be as follows: + +```py copy filename="aerial_swap_automation.py" +import argparse +from time import sleep + +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from cosmpy.aerial.contract import LedgerContract +from cosmpy.aerial.faucet import FaucetApi +from cosmpy.aerial.wallet import LocalWallet + +def swap_native_for_cw20(swap_amount, pair_contract, wallet): + """ + Swap Native for cw20. + + :param swap_amount: swap amount + :param pair_contract: pair contract address + :param wallet: wallet address + + """ + tx = pair_contract.execute( + { + "swap": { + "offer_asset": { + "info": {"native_token": {"denom": "atestfet"}}, + "amount": str(swap_amount), + } + } + }, + sender=wallet, + funds=str(swap_amount) + "atestfet", + ) + print("swapping native for cw20 tokens") + tx.wait_to_complete() + +def swap_cw20_for_native(swap_amount, pair_contract_address, token_contract, wallet): + """ + Swap cw20 for native. + + :param swap_amount: swap amount + :param pair_contract_address: pair contract address + :param token_contract: token contract + :param wallet: wallet address + + """ + tx = token_contract.execute( + { + "send": { + "contract": pair_contract_address, + "amount": str(swap_amount), + "msg": "eyJzd2FwIjp7fX0=", + } + }, + wallet, + ) + print("swapping cw20 for native tokens") + tx.wait_to_complete() + +def _parse_commandline(): + """Commandline parser.""" + parser = argparse.ArgumentParser() + + parser.add_argument( + "trading_wallet", + type=int, + nargs="?", + default=1000000, + help="initial atestfet balance to perform swaps using the liquidity pool", + ) + parser.add_argument( + "upper_bound", + type=int, + nargs="?", + default=20.5, + help="price upper bound that will trigger a swap from cw20 to native tokens", + ) + parser.add_argument( + "lower_bound", + type=int, + nargs="?", + default=19.5, + help="price lower bound that will trigger a swap from native to cw20 tokens", + ) + parser.add_argument( + "commission", + type=int, + nargs="?", + default=0.003, + help="LP commission, for terraswap the default is 0.3%", + ) + parser.add_argument( + "interval_time", + type=int, + nargs="?", + default=5, + help="interval time in seconds to query liquidity pool price", + ) + + return parser.parse_args() + +def main(): + """Run main.""" + args = _parse_commandline() + + # Define any wallet + wallet = LocalWallet.generate() + + # Network configuration + ledger = LedgerClient(NetworkConfig.latest_stable_testnet()) + + # Add tokens to wallet + faucet_api = FaucetApi(NetworkConfig.latest_stable_testnet()) + + wallet_balance = ledger.query_bank_balance(wallet.address()) + + while wallet_balance < (10**18): + print("Providing wealth to wallet...") + faucet_api.get_wealth(wallet.address()) + wallet_balance = ledger.query_bank_balance(wallet.address()) + + # Define cw20, pair and liquidity token contracts + token_contract_address = ( + "fetch1qr8ysysnfxmqzu7cu7cq7dsq5g2r0kvkg5e2wl2fnlkqss60hcjsxtljxl" + ) + pair_contract_address = ( + "fetch1vgnx2d46uvyxrg9pc5mktkcvkp4uflyp3j86v68pq4jxdc8j4y0s6ulf2a" + ) + + token_contract = LedgerContract( + path=None, client=ledger, address=token_contract_address + ) + pair_contract = LedgerContract( + path=None, client=ledger, address=pair_contract_address + ) + + # tokens in trading wallet (currency will vary [atestfet,cw20] ) + currency = "atestfet" + tokens = args.trading_wallet + + # Swap thresholds + upper_bound = args.upper_bound + lower_bound = args.lower_bound + + # LP commission + commission = args.commission + + # Wait time + interval = args.interval_time + + while True: + + # Query LP status + pool = pair_contract.query({"pool": {}}) + native_amount = int(pool["assets"][1]["amount"]) + cw20_amount = int(pool["assets"][0]["amount"]) + + if currency == "atestfet": + # Calculate received cw20 tokens if atestfet tokens are given to LP + tokens_out = round( + ((cw20_amount * tokens) / (native_amount + tokens)) * (1 - commission) + ) + + # Sell price of atestfet => give atestfet, get cw20 + sell_price = tokens / tokens_out + print("atestfet sell price: ", sell_price) + if sell_price <= lower_bound: + swap_native_for_cw20(tokens, pair_contract, wallet) + tokens = int( + token_contract.query( + {"balance": {"address": str(wallet.address())}} + )["balance"] + ) + + # Trading wallet currency changed to cw20 + currency = "CW20" + else: + # Calculate received atestfet tokens if cw20 tokens are given to LP + tokens_out = round( + ((native_amount * tokens) / (cw20_amount + tokens)) * (1 - commission) + ) + + # Buy price of atestfet => give cw20, get atestfet + buy_price = tokens_out / tokens + print("atestfet buy price: ", buy_price) + if buy_price >= upper_bound: + swap_cw20_for_native( + tokens, pair_contract_address, token_contract, wallet + ) + tokens = tokens_out + + # Trading wallet currency changed to cw20 + currency = "atestfet" + + sleep(interval) + +if __name__ == "__main__": + main() +``` diff --git a/pages/guides/fetch-network/cosmpy/use-cases/wallet-top-up.mdx b/pages/guides/fetch-network/cosmpy/use-cases/wallet-top-up.mdx new file mode 100644 index 000000000..741024fb0 --- /dev/null +++ b/pages/guides/fetch-network/cosmpy/use-cases/wallet-top-up.mdx @@ -0,0 +1,473 @@ +# Wallet top-up 💵 + +## Introduction + +The following guide is about how to top-up a wallet using the CosmPy library. To top-up a wallet, you need to create different wallets. In particular, if you are performing multiple transactions from a certain `task_wallet`, you can set an algorithm to keep that wallet address topped-up. For this use case, we will use three different wallets: `wallet`, `authz_wallet`, and `task_wallet`. `wallet` will be the main wallet address that we don't want to give full access to, therefore we will authorize `authz_wallet` to send a certain amount of tokens from `wallet` to `task_wallet` every time `task_wallet` balance falls below a certain `minimum_balance` threshold. This way, `task_wallet` can keep performing transactions using the main wallet's tokens by being topped-up by `authz_wallet`. + +## Walk-through + +### Aerial authorization: authorization address and authorization wallet + +1. Let's start by creating a Python script for this: `touch aerial_authz.py` +2. First of all, we need to import the necessary classes: + + ```py copy + import argparse + from datetime import datetime, timedelta + + from google.protobuf import any_pb2, timestamp_pb2 + + from cosmpy.aerial.client import LedgerClient, NetworkConfig + from cosmpy.aerial.client.utils import prepare_and_broadcast_basic_transaction + from cosmpy.aerial.faucet import FaucetApi + from cosmpy.aerial.tx import Transaction + from cosmpy.aerial.wallet import LocalWallet + from cosmpy.protos.cosmos.authz.v1beta1.authz_pb2 import Grant + from cosmpy.protos.cosmos.authz.v1beta1.tx_pb2 import MsgGrant + from cosmpy.protos.cosmos.bank.v1beta1.authz_pb2 import SendAuthorization + from cosmpy.protos.cosmos.base.v1beta1.coin_pb2 import Coin + ``` + +3. We then proceed and define a `_parse_commandline()` function: + + ```py copy + def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "authz_address", + help="address that will be granted authorization to send tokens from wallet", + ) + parser.add_argument( + "total_authz_time", + type=int, + nargs="?", + default=10, + help="authorization time for authz_address in minutes", + ) + parser.add_argument( + "spend_limit", + type=int, + nargs="?", + default=1000000000000000000, + help="maximum tokens that authz_wallet will be able to spend from wallet", + ) + + return parser.parse_args() + ``` + + + The `_parse_commandline()` function is using the `argparse` module to define a command-line interface for this script. It expects and processes three command-line arguments: + + 1. `authz_address`: this is a required argument. It expects the user to provide an address that will be granted authorization to send tokens from a wallet. + + 2. `total_authz_time`: this is an optional argument. If provided, it should be an integer representing the authorization time for the `authz_address` in minutes. If not provided, it defaults to `10` minutes. + + 3. `spend_limit`: this is another optional argument. If provided, it should be an integer representing the maximum tokens that the `authz_wallet` will be able to spend from the wallet. If not provided, it defaults to `1000000000000000000`. + + The `help` parameter provides a description of what each argument is for, which can be helpful for users who might not be familiar with the script. After defining these arguments, the function uses `parser.parse_args()` to process the command-line arguments provided by the user and return them as an object containing the values provided for `authz_address`, `total_authz_time`, and `spend_limit`. + +4. We can the define our `main()` function: + + ```py copy + def main(): + """Run main.""" + args = _parse_commandline() + + wallet = LocalWallet.generate() + + authz_address = args.authz_address + + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + total_authz_time = args.total_authz_time + wallet_balance = ledger.query_bank_balance(wallet.address()) + + amount = args.spend_limit + + while wallet_balance < (amount): + print("Providing wealth to wallet...") + faucet_api.get_wealth(wallet.address()) + wallet_balance = ledger.query_bank_balance(wallet.address()) + + spend_amount = Coin(amount=str(amount), denom="atestfet") + + # Authorize authz_wallet to send tokens from wallet + authz_any = any_pb2.Any() + authz_any.Pack( + SendAuthorization(spend_limit=[spend_amount]), + "", + ) + + expiry = timestamp_pb2.Timestamp() + expiry.FromDatetime(datetime.now() + timedelta(seconds=total_authz_time * 60)) + grant = Grant(authorization=authz_any, expiration=expiry) + + msg = MsgGrant( + granter=str(wallet.address()), + grantee=authz_address, + grant=grant, + ) + + tx = Transaction() + tx.add_message(msg) + + tx = prepare_and_broadcast_basic_transaction(ledger, tx, wallet) + tx.wait_to_complete() + + + if __name__ == "__main__": + main() + ``` + + In the first line we define a variable `args` using `_parse_commandline()` function defined previously to retrieve the command-line arguments `authz_address`, `total_authz_time`, and `spend_limit`. The values are stored in the `args` variable. We then generate a new wallet using the `generate()` method of the `LocalWallet` class, and then set the `authz_address` variable to retrieve the `authz_address` from the command-line arguments previously defined. This is the address that will be granted authorization to send tokens from the wallet. We then initialize a `ledger` object using the `LedgerClient` class and configure it to connect to the Fetch.ai stable testnet. We also initialize a `faucet_api` object using the `FaucetApi` class to provide access to a faucet API on the Fetch.ai stable testnet. + + `total_authz_time` retrieves the total authorization time (in minutes) from the command-line arguments. We proceed by checking the balance of the wallet by querying the ledger, using the `query_bank_balance()` method. We can then define a loop that continues until the wallet balance is greater than the specified spend amount (`amount`). Within the loop, it requests additional tokens from the faucet using `faucet_api.get_wealth()` and updates the wallet balance. + + Below, we define the `spend_amount` variable using a Coin object representing the spend amount. In this case, it's specified in the "atestfet" denomination. We then constructs an authorization object (`authz_any`) using `SendAuthorization`. It sets an expiration time for the authorization, and creates an instance of `MsgGrant` message type, specifying the `granter` (the wallet's address), `grantee` (the `authz_address`), and the `grant` (the authorization object). A new transaction (`tx`) is finally created, and `msg` is added to it. The transaction is then prepared and broadcasted using `prepare_and_broadcast_basic_transaction()`. Finally, the script waits for the transaction to complete. + +5. Save the script. + +The overall script should be as follows. + +```py copy filename="aerial_authz.py" +import argparse +from datetime import datetime, timedelta + +from google.protobuf import any_pb2, timestamp_pb2 + +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from cosmpy.aerial.client.utils import prepare_and_broadcast_basic_transaction +from cosmpy.aerial.faucet import FaucetApi +from cosmpy.aerial.tx import Transaction +from cosmpy.aerial.wallet import LocalWallet +from cosmpy.protos.cosmos.authz.v1beta1.authz_pb2 import Grant +from cosmpy.protos.cosmos.authz.v1beta1.tx_pb2 import MsgGrant +from cosmpy.protos.cosmos.bank.v1beta1.authz_pb2 import SendAuthorization +from cosmpy.protos.cosmos.base.v1beta1.coin_pb2 import Coin + + +def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument( + "authz_address", + help="address that will be granted authorization to send tokens from wallet", + ) + parser.add_argument( + "total_authz_time", + type=int, + nargs="?", + default=10, + help="authorization time for authz_address in minutes", + ) + parser.add_argument( + "spend_limit", + type=int, + nargs="?", + default=1000000000000000000, + help="maximum tokens that authz_wallet will be able to spend from wallet", + ) + + return parser.parse_args() + + +def main(): + """Run main.""" + args = _parse_commandline() + + wallet = LocalWallet.generate() + + authz_address = args.authz_address + + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + total_authz_time = args.total_authz_time + wallet_balance = ledger.query_bank_balance(wallet.address()) + + amount = args.spend_limit + + while wallet_balance < (amount): + print("Providing wealth to wallet...") + faucet_api.get_wealth(wallet.address()) + wallet_balance = ledger.query_bank_balance(wallet.address()) + + spend_amount = Coin(amount=str(amount), denom="atestfet") + + # Authorize authz_wallet to send tokens from wallet + authz_any = any_pb2.Any() + authz_any.Pack( + SendAuthorization(spend_limit=[spend_amount]), + "", + ) + + expiry = timestamp_pb2.Timestamp() + expiry.FromDatetime(datetime.now() + timedelta(seconds=total_authz_time * 60)) + grant = Grant(authorization=authz_any, expiration=expiry) + + msg = MsgGrant( + granter=str(wallet.address()), + grantee=authz_address, + grant=grant, + ) + + tx = Transaction() + tx.add_message(msg) + + tx = prepare_and_broadcast_basic_transaction(ledger, tx, wallet) + tx.wait_to_complete() + + +if __name__ == "__main__": + main() +``` + +### Aerial top-up + +We are now ready to write a Python script which automates the process of topping-up the designated wallet (`task_wallet`) from the main wallet (`wallet`) after authorization from `authz_wallet`, whenever the balance of `task_wallet` falls below a certain threshold (`minimum_balance`). + +1. Let's create a Python script for this and name it: `touch aerial_topup.py` +2. Let's then import the needed modules such as `argparse` for command-line argument parsing and modules from the `cosmpy` library for blockchain interaction: + + ```py copy + import argparse + import time + from google.protobuf import any_pb2 + from cosmpy.aerial.client import LedgerClient, NetworkConfig + from cosmpy.aerial.client.utils import prepare_and_broadcast_basic_transaction + from cosmpy.aerial.faucet import FaucetApi + from cosmpy.aerial.tx import Transaction + from cosmpy.aerial.wallet import LocalWallet + from cosmpy.protos.cosmos.authz.v1beta1.tx_pb2 import MsgExec + from cosmpy.protos.cosmos.bank.v1beta1.tx_pb2 import MsgSend + from cosmpy.protos.cosmos.base.v1beta1.coin_pb2 import Coin + ``` + +3. We then define a `_parse_commandline()` function that sets up command-line arguments: + + ```py copy + def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument("wallet_address", help="main wallet address") + parser.add_argument( + "task_wallet_address", help="wallet address that will perform transactions" + ) + parser.add_argument( + "top_up_amount", + type=int, + nargs="?", + default=10000000000000000, + help="top-up amount from wallet address to task_wallet address", + ) + parser.add_argument( + "minimum_balance", + type=int, + nargs="?", + default=1000000000000000, + help="minimum task_wallet address balance that will trigger top-up", + ) + parser.add_argument( + "interval_time", + type=int, + nargs="?", + default=5, + help="interval time in seconds to query task_wallet's balance", + ) + + return parser.parse_args() + ``` + + Above we defined different arguments including the addresses of the main wallet (`wallet_address`) and the task wallet (`task_wallet_address`), the top-up amount from `wallet_address` to `task_wallet_address` (`top_up_amount`), the minimum balance for `task_wallet_address` (`minimum_balance`), and the interval time in seconds to query `task_wallet_address`'s balance (`interval_time`). + + After these arguments are defined, the function uses `parser.parse_args()` to process the command-line arguments provided by the user. The values are then returned as an object, where each attribute corresponds to the name of the argument. This allows the script to access and utilize these values during execution. + +4. We are now ready to define our `main()` function: + + ```py copy + def main(): + """Run main.""" + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + args = _parse_commandline() + + wallet_address = args.wallet_address + + task_wallet_address = args.task_wallet_address + + # Use aerial_authz.py to authorize authz_wallet address to send tokens from wallet + authz_wallet = LocalWallet.generate() + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + wallet_balance = ledger.query_bank_balance(authz_wallet.address()) + + while wallet_balance < (10**18): + print("Providing wealth to wallet...") + faucet_api.get_wealth(authz_wallet.address()) + wallet_balance = ledger.query_bank_balance(authz_wallet.address()) + + ledger = LedgerClient(NetworkConfig.latest_stable_testnet()) + + # Top-up amount + amount = args.top_up_amount + top_up_amount = Coin(amount=str(amount), denom="atestfet") + + # Minimum balance for task_wallet + minimum_balance = args.minimum_balance + + # Interval to query task_wallet's balance + interval_time = args.interval_time + + while True: + + wallet_balance = ledger.query_bank_balance(wallet_address) + + if wallet_balance < amount: + print("Wallet doesn't have enough balance to top-up task_wallet") + break + + task_wallet_balance = ledger.query_bank_balance(task_wallet_address) + + if task_wallet_balance < minimum_balance: + + print("topping up task wallet") + # Top-up task_wallet + msg = any_pb2.Any() + msg.Pack( + MsgSend( + from_address=wallet_address, + to_address=task_wallet_address, + amount=[top_up_amount], + ), + "", + ) + + tx = Transaction() + tx.add_message(MsgExec(grantee=str(authz_wallet.address()), msgs=[msg])) + + tx = prepare_and_broadcast_basic_transaction(ledger, tx, authz_wallet) + tx.wait_to_complete() + + time.sleep(interval_time) + + + if __name__ == "__main__": + main() + ``` + + Here we defined the `main()` function which orchestrates all of the operations. It first initializes a `ledger` object to interact with the blockchain using the `LedgerClient()` class. It then parses command-line arguments using `_parse_commandline()` and stores them in the `args` variable. The function then retrieves wallet addresses for `wallet` and `task_wallet` from `args`. The function then uses `aerial_authz.py` script previously created above to authorize `authz_wallet` address to send tokens from `wallet`. If the balance of `authz_wallet` is below `10**18`, it uses a faucet API to provide wealth to the wallet until it reaches this threshold. Within the script, we then re-initialize the ledger object with the latest stable testnet configuration. We then proceed to set the top-up amount, the minimum balance, and interval timer thresholds from `args`. The script then enters an infinite loop (`while True`) in which it queries the balance of the main `wallet`. Checks if the main wallet has enough balance to top-up `task_wallet`. Queries the balance of `task_wallet`: if its balance falls below the specified minimum, it initiates a top-up by first creating a message to send tokens from `wallet_address` to `task_wallet_address`, then constructing a transaction (`tx`) with the authorization and message. It then prepares, broadcasts the transaction, and waits for a specified interval before repeating the process. + +5. Save the script. + +The overall script should be as follows: + +```py copy filename="aerial_topup.py" +import argparse +import time + +from google.protobuf import any_pb2 + +from cosmpy.aerial.client import LedgerClient, NetworkConfig +from cosmpy.aerial.client.utils import prepare_and_broadcast_basic_transaction +from cosmpy.aerial.faucet import FaucetApi +from cosmpy.aerial.tx import Transaction +from cosmpy.aerial.wallet import LocalWallet +from cosmpy.protos.cosmos.authz.v1beta1.tx_pb2 import MsgExec +from cosmpy.protos.cosmos.bank.v1beta1.tx_pb2 import MsgSend +from cosmpy.protos.cosmos.base.v1beta1.coin_pb2 import Coin + +def _parse_commandline(): + parser = argparse.ArgumentParser() + parser.add_argument("wallet_address", help="main wallet address") + parser.add_argument( + "task_wallet_address", help="wallet address that will perform transactions" + ) + parser.add_argument( + "top_up_amount", + type=int, + nargs="?", + default=10000000000000000, + help="top-up amount from wallet address to task_wallet address", + ) + parser.add_argument( + "minimum_balance", + type=int, + nargs="?", + default=1000000000000000, + help="minimum task_wallet address balance that will trigger top-up", + ) + parser.add_argument( + "interval_time", + type=int, + nargs="?", + default=5, + help="interval time in seconds to query task_wallet's balance", + ) + + return parser.parse_args() + +def main(): + """Run main.""" + ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) + args = _parse_commandline() + + wallet_address = args.wallet_address + + task_wallet_address = args.task_wallet_address + + # Use aerial_authz.py to authorize authz_wallet address to send tokens from wallet + authz_wallet = LocalWallet.generate() + faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) + + wallet_balance = ledger.query_bank_balance(authz_wallet.address()) + + while wallet_balance < (10**18): + print("Providing wealth to wallet...") + faucet_api.get_wealth(authz_wallet.address()) + wallet_balance = ledger.query_bank_balance(authz_wallet.address()) + + ledger = LedgerClient(NetworkConfig.latest_stable_testnet()) + + # Top-up amount + amount = args.top_up_amount + top_up_amount = Coin(amount=str(amount), denom="atestfet") + + # Minimum balance for task_wallet + minimum_balance = args.minimum_balance + + # Interval to query task_wallet's balance + interval_time = args.interval_time + + while True: + + wallet_balance = ledger.query_bank_balance(wallet_address) + + if wallet_balance < amount: + print("Wallet doesn't have enough balance to top-up task_wallet") + break + + task_wallet_balance = ledger.query_bank_balance(task_wallet_address) + + if task_wallet_balance < minimum_balance: + + print("topping up task wallet") + # Top-up task_wallet + msg = any_pb2.Any() + msg.Pack( + MsgSend( + from_address=wallet_address, + to_address=task_wallet_address, + amount=[top_up_amount], + ), + "", + ) + + tx = Transaction() + tx.add_message(MsgExec(grantee=str(authz_wallet.address()), msgs=[msg])) + + tx = prepare_and_broadcast_basic_transaction(ledger, tx, authz_wallet) + tx.wait_to_complete() + + time.sleep(interval_time) + +if __name__ == "__main__": + main() +``` diff --git a/src/images/docs/cosmpy-graph-optimal-period.png b/src/images/docs/cosmpy-graph-optimal-period.png new file mode 100644 index 000000000..f393157f6 Binary files /dev/null and b/src/images/docs/cosmpy-graph-optimal-period.png differ diff --git a/src/images/docs/cosmpy_stake_optimizer.png b/src/images/docs/cosmpy_stake_optimizer.png new file mode 100644 index 000000000..b08b4d97a Binary files /dev/null and b/src/images/docs/cosmpy_stake_optimizer.png differ diff --git a/src/images/docs/reward_equation.png b/src/images/docs/reward_equation.png new file mode 100644 index 000000000..d5337f4a0 Binary files /dev/null and b/src/images/docs/reward_equation.png differ