CosmPy

repository·main·Indexed 41 days ago

https://github.com/fetchai/cosmpy

A Python library for interacting with Cosmos-based blockchain networks, specifically optimized for the Fetch.ai network. Version 0.13.0rc1 provides tools for querying account balances, performing staking operations, sending tokens, and retrieving block information via the LedgerClient and AsyncLedgerClient.

Tokens
32.8K
Snippets
122
Records
142
Agent score
100%

What's inside CosmPy

  1. Overview of CosmPy capabilities

    main

    CosmPy is a Python library designed for interacting with Cosmos-SDK based blockchain networks. It provides three primary levels of interaction:

    1. Simplified API: High-level methods for querying the network and sending transactions.
    2. CosmWasm Interface: An easy-to-use interface specifically for deploying and interacting with Cosmwasm smart contracts.
    3. Low-level Ledger APIs: Direct access to low-level APIs for advanced or specialized use-cases.
  2. How AsyncLedgerClient works

    main
    The AsyncLedgerClient is designed to be a drop-in asynchronous replacement for the synchronous LedgerClient. It reuses the same generated protobuf stubs, but because these stubs are constructed over an aio (asyncio) channel, they return awaitables instead of blocking calls. This allows for high-performance, non-blocking network operations specifically for gRPC endpoints.
  3. Implement a wallet top-up algorithm using Authz

    main

    To maintain a task_wallet balance for continuous transactions without exposing a main wallet's full control, you can use the Cosmos authz module.

    Workflow:

    1. Main Wallet (wallet): Holds the primary funds.
    2. Authorized Wallet (authz_wallet): Granted permission via MsgGrant to spend a specific amount from the main wallet.
    3. Task Wallet (task_wallet_address): The address performing the actual tasks. It is topped up by the authz_wallet whenever its balance falls below a minimum_balance threshold.

    This pattern ensures that the authz_wallet can only move a limited spend_amount from the main wallet, providing a layer of security.

  4. Handle nested messages using protobuf.Any

    main

    Some messages, such as MsgExec from the authz module, require nesting one message inside another. Because protobuf is strongly typed, you must pack the inner message into a google.protobuf.any_pb2.Any object before including it in the outer message.

    Workflow:

    1. Create an instance of any_pb2.Any().
    2. Use msg.Pack(inner_message, "") to wrap your specific protobuf message.
    3. Pass the Any object into the field of the outer message (e.g., the msgs list in MsgExec).
    from cosmpy.aerial.client import LedgerClient, NetworkConfig
    from cosmpy.aerial.wallet import LocalWallet
    from cosmpy.aerial.tx import Transaction
    from cosmpy.aerial.client.utils import prepare_and_broadcast_basic_transaction
    from cosmpy.crypto.keypairs import PrivateKey
    from cosmpy.protos.cosmos.distribution.v1beta1.tx_pb2 import MsgWithdrawValidatorCommission
    from cosmpy.protos.cosmos.authz.v1beta1.tx_pb2 import MsgExec
    from google.protobuf import any_pb2
    
    client = LedgerClient(NetworkConfig.fetchai_dorado_testnet())
    wallet = LocalWallet(PrivateKey("rBDA3Q0vK5T+JVQmXSoooqUY/mSO4mmhMHQJI31+h1o="))
    
    # Pack the nested message into Any
    msg = any_pb2.Any()
    msg.Pack(
        MsgWithdrawValidatorCommission(
            validator_address="fetchvaloper1rsane988vksrgp2mlqzclmt8wucxv0ej4hrn2k"
        ),
        "",
    )
    
    tx = Transaction()
    tx.add_message(MsgExec(grantee=str(wallet.address()), msgs=[msg]))
    
    tx = prepare_and_broadcast_basic_transaction(client, tx, wallet)
    tx.wait_to_complete()
  5. Understand GasStrategy abstractions

    main

    In cosmpy.aerial.gas, a GasStrategy is an abstract base class (ABC) that defines how gas for a transaction is estimated and how the block gas limit is retrieved.

    There are three main implementation patterns:

    1. Simulation-based: Uses a live client to simulate the transaction (SimulationGasStrategy or AsyncSimulationGasStrategy).
    2. Offline/Table-based: Uses a pre-defined mapping of transaction types to gas values (OfflineMessageTableStrategy).
    3. Async: Specialized versions of strategies designed to work with AsyncLedgerClient (AsyncGasStrategy).
  6. Manage the Transaction lifecycle

    main

    The Transaction class manages the lifecycle of a Cosmos transaction through several states defined by TxState (Draft, Sealed, Final).

    Workflow:

    1. Draft: Start with a Transaction object and add messages using .add_message(msg). This method raises a RuntimeError if the transaction is not in the Draft state.
    2. Seal: Use .seal(signing_cfgs, fee, memo, timeout_height) to transition the transaction to the Sealed state. This requires signing_cfgs (a SigningCfg or a list of them) and a TxFee object.
    3. Sign: Use .sign(signer, chain_id, account_number, deterministic) to sign the sealed transaction. This raises a RuntimeError if the transaction is not yet Sealed.
    4. Complete: Use .complete() to update the transaction state to Final.

    Accessing Data:

    • .state: Returns the current TxState.
    • .msgs: Returns the transaction messages.
    • .fee: Returns the transaction fee.
    • .tx: Returns the transaction (raises RuntimeError if not completed).
    from cosmpy.aerial.tx import Transaction, TxFee, SigningCfg
    
    # 1. Draft
    tx = Transaction().add_message(my_msg)
    
    # 2. Seal
    fee = TxFee(amount="1000afet", gas_limit=200000)
    sealed_tx = tx.seal(signing_cfgs=my_signing_cfg, fee=fee, memo="my memo")
    
    # 3. Sign
    signed_tx = sealed_tx.sign(signer=my_signer, chain_id="fetchai", account_number=1)
    
    # 4. Complete
    final_tx = signed_tx.complete()
  7. Understand the role of LedgerClientBase

    main
    In CosmPy, LedgerClientBase provides the shared, I/O-free logic used by both the synchronous LedgerClient and the asynchronous AsyncLedgerClient. It handles core logic such as configuration management, request/response translation, and fee arithmetic. It does not perform network calls itself; instead, it operates on the protobuf stubs used by the concrete client implementations.
  8. Set up a new CosmPy development environment

    main

    To start developing with CosmPy, ensure you have Python (version 3.10, 3.11, 3.12, or 3.13) and poetry installed. Use the following commands to create a virtual environment with all development dependencies and enter it:

    1. Create the environment:
      make new-env
    2. Enter the virtual environment:
      poetry shell
    make new-env
    poetry shell
  9. Authorize a wallet to spend tokens using MsgGrant

    main

    You can authorize a grantee (the authz_wallet) to perform specific actions on behalf of a granter (the wallet) using MsgGrant. This requires defining a Grant object containing a SendAuthorization with a spend_limit and an expiration timestamp.

    To implement this:

    1. Create a SendAuthorization with the allowed spend_limit (a list of Coin objects).
    2. Pack the authorization into a google.protobuf.any_pb2.Any message.
    3. Define the Grant with an expiration time.
    4. Construct a MsgGrant specifying the granter, grantee, and the grant.
    5. Broadcast the transaction using the granter's wallet.
    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.aerial.client.utils import prepare_and_broadcast_basic_transaction
    from cosmpy.aerial.tx import Transaction
    from google.protobuf import any_pb2, timestamp_pb2
    from datetime import datetime, timedelta
    
    # Configuration
    total_authz_time = 10000
    amount = 1000000000000000000
    spend_amount = Coin(amount=str(amount), denom="atestfet")
    
    # 1. Prepare SendAuthorization
    authz_any = any_pb2.Any()
    authz_any.Pack(
        SendAuthorization(spend_limit=[spend_amount]),
        "",
    )
    
    # 2. Set Expiration
    expiry = timestamp_pb2.Timestamp()
    expiry.FromDatetime(datetime.now() + timedelta(seconds=total_authz_time * 60))
    grant = Grant(authorization=authz_any, expiration=expiry)
    
    # 3. Create MsgGrant
    msg = MsgGrant(
        granter=str(wallet.address()),
        grantee=str(authz_wallet.address()),
        grant=grant,
    )
    
    # 4. Broadcast
    tx = Transaction()
    tx.add_message(msg)
    tx = prepare_and_broadcast_basic_transaction(ledger, tx, wallet)
    tx.wait_to_complete()
  10. Query an Oracle value using an Oracle Client

    main

    An Oracle Client is a contract that allows other users to request the oracle's data in exchange for a fee. To use an Oracle Client:

    1. Deploy Client: Create a LedgerContract using oracle_client.wasm. The instantiation message must include the oracle_contract_address of the deployed oracle.
    2. Request Value: Execute the query_oracle_value message on the client contract. This requires sending funds to cover the fee.
    3. Retrieve Data: Use contract.query() with the key {"oracle_value": {}} to read the current value from the contract state.

    Example workflow:

    # Deploying the client
    contract = LedgerContract("oracle_client.wasm", ledger)
    instantiation_message = {"oracle_contract_address": str(ORACLE_CONTRACT_ADDRESS)}
    contract.deploy(instantiation_message, wallet)
    
    # Requesting and querying
    request_message = {"query_oracle_value": {}}
    contract.execute(request_message, wallet, funds="100atestfet").wait_to_complete()
    result = contract.query({"oracle_value": {}})
    # Requesting and querying
    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}")
  11. Grant authz permissions via CLI or Python

    main

    To allow an account to execute messages on behalf of another (using authz), you must first establish a grant.

    Using FetchD CLI

    Use the fetchd tx authz grant command to authorize a specific message type:

    fetchd tx authz grant $(fetchd keys show grantee --output json | jq -r .address) generic --msg-type "/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission" --from=$(fetchd keys show grantor --output json | .address) --gas auto --gas-adjustment 1.5 --gas-prices 5000000000atestfet

    Using Python

    If you have access to the keys in Python, use MsgGrant with a Grant object containing a GenericAuthorization and an expiration Timestamp.

    from cosmpy.aerial.client import LedgerClient, NetworkConfig
    from cosmpy.aerial.wallet import LocalWallet
    from cosmpy.aerial.tx import Transaction
    from cosmpy.aerial.client.utils import prepare_and_broadcast_basic_transaction
    from cosmpy.crypto.keypairs import PrivateKey
    from cosmpy.protos.cosmos.distribution.v1beta1.tx_pb2 import MsgWithdrawValidatorCommission
    from cosmpy.protos.cosmos.authz.v1beta1.tx_pb2 import MsgGrant
    from cosmpy.protos.cosmos.authz.v1beta1.authz_pb2 import GenericAuthorization, Grant
    
    from google.protobuf import any_pb2, timestamp_pb2
    from datetime import datetime, timedelta
    
    client = LedgerClient(NetworkConfig.fetchai_dorado_testnet())
    wallet = LocalWallet(PrivateKey("rBDA3Q0vK5T+JVQmXSoooqUY/mSO4mmhMHQJI31+h1o="))
    validator = LocalWallet(PrivateKey("<redacted>private key of dorado validator0"))
    
    # Prepare authorization
    authz_any = any_pb2.Any()
    authz_any.Pack(
        GenericAuthorization(
            msg="/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission"
        ),
        "",
    )
    
    expiry = timestamp_pb2.Timestamp()
    expiry.FromDatetime(datetime.now() + timedelta(seconds=60))
    grant = Grant(authorization=authz_any, expiration=expiry)
    
    # Create the grant message
    msg = MsgGrant(
        granter=str(validator.address()),
        grantee=str(wallet.address()),
        grant=grant,
    )
    
    tx = Transaction()
    tx.add_message(msg)
    
    tx = prepare_and_broadcast_basic_transaction(client, tx, validator)
    tx.wait_to_complete()